mirror of
https://github.com/xcat2/confluent.git
synced 2026-09-29 08:41:00 +00:00
Compare commits
1065 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a7b11d1e15 | |||
| 3660cf18cc | |||
| 078afaf7d7 | |||
| 3ab1eb7f01 | |||
| fbfdc9322b | |||
| ef68656bde | |||
| 0e04b853f6 | |||
| 63256e37b6 | |||
| a1b81e978b | |||
| 06c53e9d89 | |||
| ab0168b2ec | |||
| 473bb39c54 | |||
| a48ecd8c78 | |||
| 30398fc434 | |||
| 0ed7e4eac3 | |||
| 0d2999c7a8 | |||
| 75010ac6f0 | |||
| a748cc3032 | |||
| 28bb143880 | |||
| 4b6899d4af | |||
| a175fd7345 | |||
| 12bd473c0d | |||
| a967c224fe | |||
| 72a5f37232 | |||
| 5c081a8e2c | |||
| c0b69f64b8 | |||
| f7fe38f498 | |||
| 5ae949b4eb | |||
| cec6918c1a | |||
| 3df7a36c48 | |||
| 559e96f7ff | |||
| 6f568919e1 | |||
| c441739f68 | |||
| a43361bb98 | |||
| 620263db3e | |||
| dd096104cc | |||
| 191ae762ab | |||
| f866b7379c | |||
| aab6160d49 | |||
| e1dffe7c3a | |||
| 97ca6dc48e | |||
| 1d59e1da8c | |||
| 7e6b4fa4d0 | |||
| 186e89cd87 | |||
| 73d6511d9e | |||
| 065ee48325 | |||
| 4a35cc25f0 | |||
| ceced11ae8 | |||
| 62084b3605 | |||
| a84b88e269 | |||
| fc626d36ba | |||
| 606a308046 | |||
| bed0f76dba | |||
| 8c4d04a7cd | |||
| bfbde0f7f3 | |||
| ba96aea2a6 | |||
| d9f1d6c033 | |||
| 1dac61adca | |||
| e008932389 | |||
| b81e5fb3ce | |||
| e2a08e7e73 | |||
| eb48728bc1 | |||
| c6255fa13b | |||
| 2a37d64dc9 | |||
| 8101672c3a | |||
| 453c344f7f | |||
| 9eceda0636 | |||
| f28b91499e | |||
| 8cab591a8b | |||
| 6ce6740b77 | |||
| 8a4a219a14 | |||
| a39d45d03c | |||
| bc85d93cf4 | |||
| 1e963106fe | |||
| a9f0e345db | |||
| 5353b479d9 | |||
| dbc6747c38 | |||
| 0edd1efe0d | |||
| 8e87f5b9e5 | |||
| 0633b2ca67 | |||
| c1953bdad3 | |||
| 578ba06aa3 | |||
| 0975881d3b | |||
| 8fc3b7c9c0 | |||
| 6ea6ebd80e | |||
| 744f8899f7 | |||
| 5c288a27dd | |||
| 74f18d5571 | |||
| d9be6ae2e9 | |||
| 59789bae7d | |||
| 521be5d44d | |||
| 6fb82bbbad | |||
| 90e546bcac | |||
| 147d59cba7 | |||
| 6a6fd3184e | |||
| c532cf9ecf | |||
| 8909fb16d6 | |||
| 44d6bde3ff | |||
| 56fa13279e | |||
| 5008128d57 | |||
| ed320f4a17 | |||
| de8292f6dd | |||
| 8e071a2568 | |||
| 7d84d0a021 | |||
| c5f75bfa15 | |||
| 99d01d707f | |||
| 3e1690c860 | |||
| a3f5630535 | |||
| 8d6744947e | |||
| 21b3c89974 | |||
| 9718881c7b | |||
| fcbbdc8a8d | |||
| f03bb36dbe | |||
| 97a950b145 | |||
| a0e445d1b1 | |||
| 65629548c9 | |||
| 6cfbf4533c | |||
| 5794dd7f8c | |||
| 48fa74e7a9 | |||
| 59d5110b8f | |||
| 051d79727b | |||
| 4cdbc7807b | |||
| feecee82db | |||
| 395c0d4697 | |||
| a963a8ca35 | |||
| d95464df6f | |||
| 3f6e6d4c39 | |||
| c3176ab86a | |||
| 4dab5fc527 | |||
| 8897842fc4 | |||
| a251a538b0 | |||
| 480a747dcf | |||
| af025f7304 | |||
| 21edd82177 | |||
| 8641885f86 | |||
| 64cc2416d1 | |||
| 2787e1d862 | |||
| 514a121c15 | |||
| 00ce48b046 | |||
| 44929e7975 | |||
| da82fef0cb | |||
| efcac0b181 | |||
| 46e2f53018 | |||
| cf51928b3d | |||
| 151ba2e567 | |||
| bc87077397 | |||
| a77b65737e | |||
| 19c2963cf9 | |||
| a0ea8eeae3 | |||
| 6ad1ce4df5 | |||
| c53264872a | |||
| d9f2c0b266 | |||
| d528d45820 | |||
| 4eeac8d71a | |||
| 6cc0eb0797 | |||
| bfd0de1a4a | |||
| a787ac62c3 | |||
| fd9b4a8650 | |||
| 373bf3dca7 | |||
| 0ad0c626c2 | |||
| fbc4fc6846 | |||
| 3efc153615 | |||
| b7ff093e48 | |||
| 7275e98039 | |||
| 2b0c50dc23 | |||
| 54439d5f18 | |||
| 65b4cbe8cc | |||
| c8931ae6e7 | |||
| 083f5c8654 | |||
| 8bbeeafa49 | |||
| 422f210f74 | |||
| 2e6029bd2c | |||
| 81c0adbbe3 | |||
| 6d5f0cdb16 | |||
| c633286019 | |||
| ba113d6445 | |||
| d2efb16c71 | |||
| 739e302506 | |||
| ae181b7753 | |||
| b76b415a6e | |||
| ef2b324eed | |||
| ffe9606de1 | |||
| 8ed2d5a551 | |||
| 3501b3c347 | |||
| e55314d759 | |||
| 27410a9b6b | |||
| 2db01746d5 | |||
| 208be0beef | |||
| d34f8af798 | |||
| b2013e93c5 | |||
| 2a72a6184d | |||
| 7e4dcfa99c | |||
| ee82831370 | |||
| 0869669ef6 | |||
| 6a77a13539 | |||
| 56e9a67ef8 | |||
| 52d5eb9876 | |||
| b819a488f1 | |||
| 3fc31f7332 | |||
| 21c3579287 | |||
| 4a094f669e | |||
| 67eecffd29 | |||
| e288e8bad5 | |||
| a8cad7a70f | |||
| 6de605c298 | |||
| e09c2ed8eb | |||
| cd5366e73f | |||
| 509f8c30d5 | |||
| c63c8076bb | |||
| 6800c8055c | |||
| ffc55b1594 | |||
| 481342340e | |||
| d33c6be758 | |||
| 44f3630cf5 | |||
| 3eaba23e6f | |||
| 5ac0a6e650 | |||
| 9ac83665c6 | |||
| 30f9d28c2c | |||
| 0168e46f24 | |||
| 067e99d6ce | |||
| ad828e609d | |||
| cc5a5c9972 | |||
| cd2361b80b | |||
| c042583a64 | |||
| e32d3cf4cc | |||
| 2b86c878a8 | |||
| 3564de8c6d | |||
| 7b5361a019 | |||
| 65e1dfcc57 | |||
| ba039e9e3e | |||
| a6809aae98 | |||
| 4d5bfb13bf | |||
| 93e9a54e86 | |||
| 25028c8acc | |||
| 906c671d90 | |||
| 8fbd99cf5c | |||
| c86ac2885f | |||
| 952fa3d022 | |||
| 90e0f93d37 | |||
| d78adc334d | |||
| 31f2161b57 | |||
| 571a34cba2 | |||
| 52fa5158f6 | |||
| 907f66ae8b | |||
| c8c275f804 | |||
| 7a08fee4b5 | |||
| 36f0d888cd | |||
| 81451a6451 | |||
| 02eb195e3f | |||
| 87e7a90c37 | |||
| bafc25005f | |||
| 33c1137ccf | |||
| abfeef5a0a | |||
| e81579f414 | |||
| 47edb1dbd1 | |||
| 6a8cb8deaa | |||
| c6516f9d62 | |||
| 72448aa0b4 | |||
| 6290c169f5 | |||
| e5bbd226ff | |||
| 037ed43c70 | |||
| 0a816acf4f | |||
| 2c9c778ca7 | |||
| bf005eace6 | |||
| 34c6d6a4d7 | |||
| b402ddd656 | |||
| 1ae055fa8f | |||
| 89cf255ae7 | |||
| c070148aed | |||
| d6097ca706 | |||
| c3eed19309 | |||
| 40dbe63336 | |||
| d6ecee955b | |||
| d7d3ae344c | |||
| 5c4944a1e4 | |||
| 06b31f4845 | |||
| 7fecd0ac5c | |||
| 36d5d60edc | |||
| 6e26b19c67 | |||
| 16430e1ec9 | |||
| 5d572f17f9 | |||
| ae49cf290e | |||
| 5ead803c8a | |||
| 7232a0c1b3 | |||
| dce25d802e | |||
| 835d1fc0ab | |||
| c28a963d62 | |||
| 9fd091daad | |||
| 3c21ca8739 | |||
| 996b1ba45b | |||
| 4af1f998fb | |||
| b2c1137321 | |||
| d484e9db43 | |||
| 6397709e47 | |||
| e0c0f0f1f3 | |||
| ca29f6ae35 | |||
| 2c8681a9f3 | |||
| b927572872 | |||
| b5df380ee4 | |||
| 5c61430ccc | |||
| 2b275cd369 | |||
| f79dac7bd2 | |||
| fc5f16fb01 | |||
| 907d25164f | |||
| 0b85fab529 | |||
| adb4ce919e | |||
| 9379c85d0e | |||
| 11ffa7a091 | |||
| bacba8972a | |||
| 5404497e70 | |||
| 70690517de | |||
| a3162daf62 | |||
| 8c886b751c | |||
| 69630edfa9 | |||
| cdce1f1833 | |||
| 48079f297b | |||
| bf24d0f501 | |||
| 1d6111b8dd | |||
| e59d237d11 | |||
| fb3dc9a200 | |||
| b0d2d44b75 | |||
| f1e83d938b | |||
| e643b7ed7d | |||
| 163b29a07c | |||
| c5fa0bfd79 | |||
| a258653186 | |||
| 656e82c3fe | |||
| 898ff065e0 | |||
| 779b5c9ede | |||
| 5be08ddb1d | |||
| 16abf7cb64 | |||
| 269acf9943 | |||
| c649ae5fec | |||
| c3f2e131b2 | |||
| 4124e0fcc0 | |||
| 0e2e6267cd | |||
| e8119d330d | |||
| 5ae6717709 | |||
| 0cd94447f7 | |||
| ce4f8c1837 | |||
| 8ad06f79e7 | |||
| 5481da269e | |||
| 6ea307d415 | |||
| 59aa23b2f5 | |||
| 4446308030 | |||
| 7703c6c2ab | |||
| f5b6d434f3 | |||
| 8ce5a7dccf | |||
| 23c9e6315a | |||
| 38f9583be3 | |||
| 1b355ec468 | |||
| 2bbf4b9e98 | |||
| 1248894cf3 | |||
| c43365d2dd | |||
| 4e7c098e75 | |||
| ef6c89b883 | |||
| 99c06813d9 | |||
| 686b59c2b4 | |||
| 46b909c291 | |||
| 9abb163c7e | |||
| 7e25dd805f | |||
| 31220292e5 | |||
| 161cf37f46 | |||
| ad64cda249 | |||
| db812ac292 | |||
| a322118877 | |||
| ebfbbcca23 | |||
| 275525d3f3 | |||
| 938a6e44df | |||
| ca6b203a09 | |||
| c478cb5d6e | |||
| ca9e7d1d93 | |||
| 2691722f48 | |||
| e194222553 | |||
| 8f611f0e59 | |||
| 1fdcf19563 | |||
| add1a1b32a | |||
| 8abe384e1a | |||
| 14577be963 | |||
| e6b8d0dabc | |||
| b1a91ad409 | |||
| 996fd82920 | |||
| 5e6c66826f | |||
| 22d79867c8 | |||
| 52d25d563b | |||
| 68eeb95ea3 | |||
| 95d5ff6a4c | |||
| b42114bea0 | |||
| e0877bc0b1 | |||
| 5289d34206 | |||
| f7f8247d02 | |||
| 57e23a6f52 | |||
| 73b234d29e | |||
| bfe55e276d | |||
| 44bcca99b6 | |||
| 4cb595684e | |||
| b153a14ff3 | |||
| b620838189 | |||
| 4540354ff2 | |||
| 74963a73cc | |||
| 9fe200b525 | |||
| b07d4e9736 | |||
| f649efa110 | |||
| 521013e50a | |||
| 25c8f93336 | |||
| 59f00dd10b | |||
| 2e93af9b5e | |||
| 337ab3b1a0 | |||
| f4cf74b699 | |||
| 085981f74c | |||
| 8a5f1c6dc5 | |||
| 09cb6963f0 | |||
| 188feec0b4 | |||
| 1902a333ae | |||
| f6c46ddcb8 | |||
| e23253815c | |||
| d979d29b0b | |||
| bca676ed15 | |||
| deed8b4b9b | |||
| 2c94a10e23 | |||
| 299181223e | |||
| 1378b01feb | |||
| 18713797b9 | |||
| 6588ac0990 | |||
| efd13c960f | |||
| 4f14de0136 | |||
| 6cecf6635c | |||
| 5b7807fe3d | |||
| 5f52ee9578 | |||
| 88275f5110 | |||
| 55c5bff0f9 | |||
| 3782127590 | |||
| 74355bd6f9 | |||
| bc995520b7 | |||
| 2d63e68494 | |||
| ceba3ff34f | |||
| 334ec00f3f | |||
| 35380e5bac | |||
| 66471624bc | |||
| 8a03bc48de | |||
| 5831be091a | |||
| 8197c750bb | |||
| a9f0312acd | |||
| 21700e914a | |||
| 5e15ae8e30 | |||
| 5eef76f5d3 | |||
| df72bfa715 | |||
| a6333459c4 | |||
| 075891bf74 | |||
| a937511354 | |||
| 22503e7e11 | |||
| 01d73308ed | |||
| cb16385ddb | |||
| 853585f942 | |||
| 53996a1e9d | |||
| 558171124c | |||
| 05d028e9e9 | |||
| b0a3bb18fe | |||
| 7207013abc | |||
| b511a02f20 | |||
| c60cb3a027 | |||
| 9f137fa6d4 | |||
| aa92898249 | |||
| 2cb694b374 | |||
| 24fd1e72e1 | |||
| 5b21a5b2c3 | |||
| c8ad94313e | |||
| f71d51769b | |||
| b5213e6972 | |||
| 8d72f4d64a | |||
| fcd46c7b72 | |||
| 6efb8ff1b7 | |||
| c227c2353b | |||
| cb2133e871 | |||
| c56c1948a9 | |||
| 34b30a1bd8 | |||
| 0e7bfe4f40 | |||
| a93a759b72 | |||
| cba514692c | |||
| 2bb0a70a24 | |||
| 1b84cd43fe | |||
| 39839a9f2a | |||
| 8473cb1cf8 | |||
| 1a10b5d747 | |||
| 82a7012527 | |||
| 199a948b33 | |||
| 6fc305d6a0 | |||
| 75b6221dcf | |||
| bd5032268e | |||
| 0aa0289b06 | |||
| 96df7871cb | |||
| 82ee69b56c | |||
| 37899ad39e | |||
| 43fb4467b7 | |||
| f5f9403eff | |||
| 78e7381442 | |||
| 005cefc914 | |||
| 5f38cce51e | |||
| 5d358eaeb0 | |||
| 115a6e9dd1 | |||
| a034d05cc8 | |||
| 6378f823f3 | |||
| 079dfed11e | |||
| d836bf7298 | |||
| 25b969a4db | |||
| 950abde20e | |||
| 37be696f23 | |||
| 73cab3774d | |||
| c2d52d4f83 | |||
| fd17559a73 | |||
| 3945ccd5c3 | |||
| 7c006b33bc | |||
| c96b5f0270 | |||
| 7e72240d98 | |||
| c150a848fd | |||
| bc7a197a8c | |||
| 6973736c6a | |||
| d70c3dcad7 | |||
| f9055a258e | |||
| 76a96fa054 | |||
| c784a4ec9b | |||
| 2ff47b886d | |||
| 51037e2487 | |||
| 614b916ca4 | |||
| 4077346d30 | |||
| 8a4bf22a7e | |||
| f2dd501de9 | |||
| f835057ae4 | |||
| 48913218a6 | |||
| a11e4b71ce | |||
| e9ba49a4aa | |||
| deb90fbca9 | |||
| 8cae5ea101 | |||
| f830658818 | |||
| 3105b9b1f9 | |||
| f525c25ba6 | |||
| 3012de1fe4 | |||
| be930fc076 | |||
| 2d0199a4e9 | |||
| 6b70a4322a | |||
| 6a784e3a1c | |||
| 3b2b96a4cf | |||
| 32ddb33de3 | |||
| b77ed8dbff | |||
| d5c093a30d | |||
| cf9d2a43e8 | |||
| 2f566fb81d | |||
| 92a3b02dc5 | |||
| 94c8cf3ff2 | |||
| 049897fc15 | |||
| 8741a27c24 | |||
| 1c494fc4fc | |||
| 1ee418392f | |||
| 2a7eeb6e08 | |||
| 5c83c78a90 | |||
| 6a466b0100 | |||
| 67b93ee6b0 | |||
| 6b4a21d613 | |||
| 5f46899358 | |||
| a3bc17b465 | |||
| 20a37f8db5 | |||
| c6b8aaf372 | |||
| 5baab5bef4 | |||
| 73c06fd25e | |||
| 8d9a082739 | |||
| 32602fbba3 | |||
| 2f616d4586 | |||
| 15dc4937ee | |||
| 10cb1b77dd | |||
| d86e1fc4eb | |||
| 29fcd59f8b | |||
| 78a1741e0e | |||
| 4329c1d388 | |||
| c13ebe36ae | |||
| 9495a41f85 | |||
| b0b5493ff7 | |||
| 484435c732 | |||
| 326f56219b | |||
| e098c0ba91 | |||
| a5d5f4a6a4 | |||
| 61e7c90ad1 | |||
| 51194b275b | |||
| e57cdf9a7b | |||
| e634817b34 | |||
| 10ce7a9de9 | |||
| 0724ad812b | |||
| a3b0b0240d | |||
| 7f06c69d3c | |||
| b5af770f21 | |||
| 247650868e | |||
| 9e8b2dd973 | |||
| 99fdb20f87 | |||
| 60bb4c89fb | |||
| 5d52fd2fc1 | |||
| 18bebde337 | |||
| c68c4d8cf7 | |||
| 1de84f0417 | |||
| 44bf2872b7 | |||
| c209010126 | |||
| 21b4a2f6f3 | |||
| 36fc23d692 | |||
| f601032a66 | |||
| 7c550bd68e | |||
| db5f861dc5 | |||
| d04be19ae5 | |||
| 07532e2a3f | |||
| e7be24d478 | |||
| 34b7abcb2d | |||
| 47a53a51e4 | |||
| abc15974da | |||
| b3bf6929df | |||
| 2a8d61ecf6 | |||
| cf3e9037ab | |||
| 03135543a6 | |||
| 38228ebc9b | |||
| d6110c7118 | |||
| f92b1ed4a3 | |||
| 368087fb51 | |||
| 46d62e67de | |||
| 118d1aec0d | |||
| 7c9089c87d | |||
| ba18b9936f | |||
| 3b7ecd0095 | |||
| 19e9583b47 | |||
| 8352007570 | |||
| f7965d235a | |||
| 6aec9534e7 | |||
| 3ee6334db2 | |||
| 582a4de62d | |||
| c9959d4082 | |||
| fa11fb54cb | |||
| ee3b824870 | |||
| 55f5b30369 | |||
| 784e4bed2f | |||
| df7cba00fd | |||
| dfb720d0ee | |||
| f5d5cbd67b | |||
| 319fec2145 | |||
| 8787d23b3a | |||
| 9b48110155 | |||
| 3064e7bef6 | |||
| 1d4df8af3a | |||
| 2aba6e469c | |||
| de58593f14 | |||
| ecbe1a86b1 | |||
| 2972374da8 | |||
| 81dd6202d3 | |||
| 36a202842a | |||
| 6a8e24dd0e | |||
| d3afeb3414 | |||
| 1bf4c0ac0a | |||
| 8e422ef822 | |||
| f0edbbad39 | |||
| 5cf1671350 | |||
| e5c4219ee9 | |||
| 3ff7e42074 | |||
| fab177e077 | |||
| a1ba5f59a8 | |||
| 9bcca6bfad | |||
| 96671ace4e | |||
| bcff3fc962 | |||
| 54d93571d1 | |||
| f2f902de7b | |||
| a09792f969 | |||
| 7d16c943a8 | |||
| b053d41cd8 | |||
| 200569e7af | |||
| c3c0e1570a | |||
| 10c82a72b5 | |||
| 79cdf65a72 | |||
| 497ca40492 | |||
| fd33e6ae01 | |||
| 32f944e67c | |||
| dcad9f5a75 | |||
| 2a34388d09 | |||
| 6993e0b496 | |||
| b7fe72673d | |||
| 0159bf1b1d | |||
| cf9ad11290 | |||
| ddd7ef5eba | |||
| 73da8ec8b5 | |||
| eac4d97732 | |||
| fa9ecfbb94 | |||
| fc5472065a | |||
| cb0845596e | |||
| 0d936e0059 | |||
| a7b8f0ab0c | |||
| 3ab4203104 | |||
| 13aa2e9aae | |||
| 7462bc28e8 | |||
| 18f1c07d65 | |||
| 0016077bee | |||
| 1dad69097b | |||
| fd7c428d1f | |||
| 80a1bd72e7 | |||
| 042d7ab5cf | |||
| c74fdf5924 | |||
| 58bf226d23 | |||
| 6f012b69a1 | |||
| 7f1e5d2302 | |||
| 3e2a827ff9 | |||
| 1d16534c16 | |||
| c80ebb0e8d | |||
| efaf1dae70 | |||
| 1de82936ed | |||
| b0c384c9ca | |||
| 61dd71778f | |||
| 0f3014957b | |||
| c925353f02 | |||
| 9edb225bd3 | |||
| 7cdc3c1400 | |||
| bd2a3f14e6 | |||
| 87d00b7447 | |||
| beedfb0600 | |||
| ce59a36351 | |||
| a1e612968e | |||
| ce1a58bf58 | |||
| b386084f2d | |||
| 8e9bcbb44f | |||
| 704aaeecf9 | |||
| 11968faffc | |||
| 8769c438c0 | |||
| f1489bf527 | |||
| c03781c022 | |||
| 298e11f60f | |||
| 67d6e9a6c7 | |||
| a4edf9afb8 | |||
| 2342fe717e | |||
| 08cf698609 | |||
| a905ae4865 | |||
| 1eaf5357ca | |||
| 39378170b1 | |||
| 2156e99ae4 | |||
| bba12ed9e7 | |||
| 282043ed97 | |||
| 09c239b294 | |||
| 0b26d12837 | |||
| 956faee052 | |||
| 8e77466875 | |||
| f01c7e19f7 | |||
| 5d894912ac | |||
| 93e78d1f03 | |||
| 97f932b946 | |||
| 61f24f4d8a | |||
| 68a79695fc | |||
| 401352998c | |||
| 9eeede651e | |||
| fb98dd5636 | |||
| 0843b991ea | |||
| 11e6145a46 | |||
| 7433dd3e38 | |||
| 4de1fea7aa | |||
| f6342dd31f | |||
| 0499a14fe8 | |||
| f301cd272f | |||
| bf4f5ad5ae | |||
| 5a5f0169a7 | |||
| 03adec089d | |||
| f065fafd61 | |||
| b315042850 | |||
| 5588712320 | |||
| c6a0aeca3b | |||
| f45228c067 | |||
| d34e65f9b7 | |||
| baef8c2c42 | |||
| 1543e145b7 | |||
| 2d403f7d68 | |||
| 78117a1b1a | |||
| 624012774c | |||
| 003358da5f | |||
| 6b24b9691b | |||
| 6ba8ca2fa2 | |||
| 38898ca921 | |||
| 810be71720 | |||
| b877a95645 | |||
| efd5732682 | |||
| 4906d6e9c4 | |||
| f2500d9d27 | |||
| 0507e89da8 | |||
| 8cea5a4fed | |||
| 8515d43dad | |||
| 5c12dc2cba | |||
| a08eace00c | |||
| cdb20c0302 | |||
| 3eed46ec08 | |||
| daef9fa60b | |||
| a7a4ede580 | |||
| a560dc1974 | |||
| e8cea66a85 | |||
| 8fc29d1b46 | |||
| 6e0186947f | |||
| a45a0e31f1 | |||
| 8ea532053e | |||
| 366c955956 | |||
| d968fb6b04 | |||
| db1d5d6dff | |||
| 867dd0dda7 | |||
| 22b63df036 | |||
| 54f419d9b4 | |||
| 5d6e241287 | |||
| 417bc5acda | |||
| 57ffe166d3 | |||
| 5718c60a51 | |||
| 31effcc025 | |||
| cefca49128 | |||
| 41a5eaa464 | |||
| 46c4065b81 | |||
| 57e323786e | |||
| 3b2a18a650 | |||
| 3ace7747ab | |||
| 8ede0fd8ef | |||
| be3ecf60a5 | |||
| 8807fcfd22 | |||
| 33fe0a3db4 | |||
| ca7711b373 | |||
| 8b37199654 | |||
| ff2bc89fae | |||
| 5fe2d2a31c | |||
| a4fed0601c | |||
| 41298a8e01 | |||
| caa4000b7e | |||
| fde2c7a8e0 | |||
| fbbb5d048f | |||
| 32d60145f7 | |||
| 1db781852c | |||
| 0dbf82b0f1 | |||
| 675dc966c7 | |||
| a9485706d1 | |||
| db1ae03415 | |||
| 9826235d4d | |||
| 232140899e | |||
| 5dddae0ebf | |||
| 39e9bf0be5 | |||
| d6b7c536d5 | |||
| f21db46cdd | |||
| 2d1ba7cc9b | |||
| 22049002bb | |||
| 727aa9a56c | |||
| dcb1c2b32b | |||
| d705d6320a | |||
| 52e2038fdf | |||
| 9d58a2d382 | |||
| a2187087f7 | |||
| c5b5178f39 | |||
| ff026ee034 | |||
| 1cc659a3b0 | |||
| 8bc8faf0bc | |||
| 1c2c9931a8 | |||
| 778a153170 | |||
| 34c510e30a | |||
| d4babbffa4 | |||
| 1c930eba9d | |||
| c4b564123f | |||
| 6d728df4dc | |||
| f1e29393df | |||
| 81bb16476c | |||
| 035f10e7d7 | |||
| 830e6bb4e4 | |||
| 1eb542f6a8 | |||
| b733049a0c | |||
| a4d80e4e3a | |||
| a69b5fbb50 | |||
| 789dfe94d0 | |||
| 73a376fd74 | |||
| 57f390fd0a | |||
| 641bc7344a | |||
| af940c972f | |||
| 468f00cd1c | |||
| ce635068aa | |||
| 348c6a7c38 | |||
| a41a42ffd0 | |||
| 3a354a6300 | |||
| fb9925bb84 | |||
| 78bf1d5acd | |||
| 8165b645d9 | |||
| c2783b6734 | |||
| 033d59b04a | |||
| 4155954d1c | |||
| 1b912c4365 | |||
| 5f9ee3d3c5 | |||
| cc9becea3b | |||
| 1e0cf7e9fb | |||
| 7ebe9da24b | |||
| 6cba560f6a | |||
| 08dcab4c72 | |||
| 4f73ddc41e | |||
| 7a912b31cb | |||
| 297513bba7 | |||
| aa2be98dc3 | |||
| 3cc9ee1a17 | |||
| 173f1eaf7e | |||
| 0d4b1a4213 | |||
| 58155a47b5 | |||
| 7fa431dbc9 | |||
| e98ecd9867 | |||
| 5087c8bed5 | |||
| 0477ab7d85 | |||
| 267d83e6e4 | |||
| c962d10222 | |||
| e5f553801b | |||
| d11c716b6a | |||
| aec4e746e9 | |||
| a78aa6816c | |||
| 5abaddfe63 | |||
| ecfc56efde | |||
| ecfb4d68c5 | |||
| 855241a043 | |||
| ce4c72eae2 | |||
| c8961377ed | |||
| 1b17c42cae | |||
| 196e8d0d58 | |||
| dbb50f0807 | |||
| 1200f7b7a1 | |||
| a2a0b5de2c | |||
| c7b01e00b6 | |||
| 06fdc648b8 | |||
| de89803b9c | |||
| d38d9204a7 | |||
| 1deb44021f | |||
| 619bbbca96 | |||
| 8246ebdd2b | |||
| a94a724fe0 | |||
| 6b9aed3722 | |||
| a3de8b9374 | |||
| c8e5808daf | |||
| f3a0ccbff8 | |||
| 27c1355a4f | |||
| 7909f9e003 | |||
| 97d38efb29 | |||
| 14ff33a44a | |||
| 78bdac474f | |||
| 0481f7889b | |||
| 123a1a9dc1 | |||
| fa24622704 | |||
| a1156097d2 | |||
| af72d0e71a | |||
| 008f8e22ae | |||
| 39ee0da879 | |||
| fc7b26eaf7 | |||
| 91238f1dcb | |||
| 7f29d3a48f | |||
| 76a4a91351 | |||
| 5ca52ff03b | |||
| bd40f2f4a6 | |||
| 66e8ce2dde | |||
| 3dd86c71fd | |||
| f97c39cea4 | |||
| 6671b9aad3 | |||
| f88e0bca4c | |||
| afd366f134 | |||
| ad0a7de1e3 | |||
| 026a027603 | |||
| 308db99dbb | |||
| a20b0abb43 | |||
| 7413c44df8 | |||
| 463f61fac7 | |||
| 0f60fc6df7 | |||
| 110820e7b7 | |||
| 71214eb613 | |||
| 889eda3d96 | |||
| 7593d21a87 | |||
| 972801d41f | |||
| b49531dfa5 | |||
| 3ae7d85820 | |||
| 0d06eedc81 | |||
| ca27385eaa | |||
| 9269c9feff | |||
| 7736056bf2 | |||
| 74e34f874c | |||
| 530c6553fd | |||
| 2e3cd53ded | |||
| 4cc80f6c40 | |||
| ff0ab4086a | |||
| 337e8d6337 | |||
| 1dd922c262 | |||
| dae6f74acc | |||
| 2323afc093 | |||
| 9c9dffc385 | |||
| 62ac582b61 | |||
| d473d23725 | |||
| c5ce302717 | |||
| 22d79598b3 | |||
| d7b3859460 | |||
| 34f03cb217 | |||
| 0fd2d26f82 | |||
| 453f6d8016 | |||
| ee84622e7d | |||
| 2bfa890faf | |||
| 94bce2f65b | |||
| cc16aed27a | |||
| 414572f626 | |||
| bac37dfa8d | |||
| d3783aaa98 | |||
| 7dbdf2a6aa | |||
| d7322f013b | |||
| 024d37d633 | |||
| 8ef90a457a | |||
| bd5d8c85d8 | |||
| 0afd9beeac | |||
| b37ef7e90c | |||
| 685f5c6803 | |||
| 7f3763f9eb | |||
| 290ccecfb9 | |||
| 0431e42452 | |||
| 36e68ca852 | |||
| ba0e03d454 | |||
| e193e92fbf | |||
| a7b0a449d6 | |||
| 2e7a8bee18 | |||
| 9e467c5e57 | |||
| 68d469788d | |||
| 7ea99ecbf5 | |||
| 49a444959d | |||
| 2628683881 | |||
| 1cf472470e | |||
| e7a70f390e | |||
| 4ae32c1219 | |||
| d04f6cc858 | |||
| 03703250e6 | |||
| 01dd48ccc2 | |||
| 91d0c8ed7a | |||
| a2163244db | |||
| f615232a95 | |||
| e7aeece7f4 | |||
| 19e733f325 | |||
| 924f679f79 | |||
| 9baa1f5652 | |||
| eccc7803a9 | |||
| cd260a769e | |||
| 56b6babed6 | |||
| 37f0345553 | |||
| 98a763eb35 | |||
| 9b7db2a924 | |||
| 87696a7b0d | |||
| c2a15fc74d | |||
| 97401e306b | |||
| 48b9d735f2 | |||
| 6444756b3c | |||
| af82e868d2 | |||
| 76818135a6 | |||
| 0f4940cd7c | |||
| 8226c2bd77 | |||
| a80ae622f6 | |||
| ea5165d2c5 | |||
| 30e24cc768 | |||
| b1951b3d86 | |||
| c649aa2a40 | |||
| e7b97afc3c | |||
| 8acb59d967 | |||
| a70e3a2e19 | |||
| a11a0cd543 | |||
| 3398acaf52 | |||
| 0c0a450fc2 | |||
| 0337962cd9 | |||
| 73f40ecbf8 | |||
| 9f5b88eb9f | |||
| 3265d812ba | |||
| 673fb02896 | |||
| 0d40a0cac6 | |||
| 6845f64d46 | |||
| ade1d93071 | |||
| b18ad89672 | |||
| 09d20ea1ff | |||
| ae39a84a30 | |||
| 0ae315a12f |
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python
|
||||
#!/usr/bin/python2
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2017 Lenovo
|
||||
@@ -21,6 +21,7 @@
|
||||
|
||||
import optparse
|
||||
import os
|
||||
import select
|
||||
import sys
|
||||
|
||||
path = os.path.dirname(os.path.realpath(__file__))
|
||||
@@ -34,6 +35,9 @@ import confluent.client
|
||||
argparser = optparse.OptionParser(usage="Usage: <other command> | %prog [options]")
|
||||
argparser.add_option('-a', '--abbreviate', action='store_true',
|
||||
help='Attempt to use confluent server to shorten noderanges')
|
||||
argparser.add_option('-b', '--base',
|
||||
help='Use given node as reference for comparison when '
|
||||
'using -d, instead of using the most common result')
|
||||
argparser.add_option('-d', '--diff', action='store_true',
|
||||
help='Show what differs between most common '
|
||||
'output group and others')
|
||||
@@ -47,6 +51,8 @@ argparser.add_option('-c', '--count', action='store_true',
|
||||
argparser.add_option('-r', '--reverse', action='store_true',
|
||||
help='Reverse sort order to show biggest output group '
|
||||
'last')
|
||||
argparser.add_option('-l', '--log', action='store', type='string', dest='log',
|
||||
help='Log each output to file, using {node} as a placeholder for node.')
|
||||
(options, args) = argparser.parse_args()
|
||||
if sys.stdin.isatty():
|
||||
argparser.print_help()
|
||||
@@ -62,7 +68,7 @@ else:
|
||||
def print_current():
|
||||
if options.diff:
|
||||
grouped.print_deviants(skipmodal=options.skipcommon, count=options.count,
|
||||
reverse=options.reverse)
|
||||
reverse=options.reverse, basenode=options.base)
|
||||
else:
|
||||
grouped.print_all(skipmodal=options.skipcommon,
|
||||
count=options.count,
|
||||
@@ -70,17 +76,39 @@ def print_current():
|
||||
sys.stdout.flush()
|
||||
|
||||
fullline = sys.stdin.readline()
|
||||
printpending = True
|
||||
clearpending = False
|
||||
holdoff = 0
|
||||
while fullline:
|
||||
for line in fullline.split('\n'):
|
||||
if not line:
|
||||
continue
|
||||
if ': ' not in line:
|
||||
line = 'UNKNOWN: ' + line
|
||||
if options.log:
|
||||
node, output = line.split(': ', 1)
|
||||
currlog = options.log.format(node=node, nodename=node)
|
||||
with open(currlog, mode='a') as log:
|
||||
log.write(output + '\n')
|
||||
continue
|
||||
grouped.add_line(*line.split(': ', 1))
|
||||
if options.watch:
|
||||
sys.stdout.write('\x1b[2J\x1b[;H') # clear screen
|
||||
print_current()
|
||||
if not holdoff:
|
||||
holdoff = os.times()[4] + 0.250
|
||||
if (holdoff < os.times()[4] or
|
||||
not select.select((sys.stdin,), (), (), 0.250)[0]):
|
||||
# print now, nothing pending
|
||||
holdoff = 0
|
||||
sys.stdout.write('\x1b[2J\x1b[;H') # clear screen
|
||||
print_current()
|
||||
printpending = False
|
||||
clearpending = True
|
||||
else:
|
||||
printpending = True
|
||||
fullline = sys.stdin.readline()
|
||||
if not options.watch:
|
||||
|
||||
if printpending:
|
||||
if clearpending:
|
||||
sys.stdout.write('\x1b[2J\x1b[;H') # clear screen
|
||||
print_current()
|
||||
|
||||
|
||||
+202
-134
@@ -1,8 +1,8 @@
|
||||
#!/usr/bin/env python
|
||||
#!/usr/bin/python2
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2014 IBM Corporation
|
||||
# Copyright 2015-2016 Lenovo
|
||||
# Copyright 2015-2018 Lenovo
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -49,6 +49,7 @@ import select
|
||||
import shlex
|
||||
import signal
|
||||
import socket
|
||||
import struct
|
||||
import sys
|
||||
import time
|
||||
try:
|
||||
@@ -88,6 +89,15 @@ except NameError:
|
||||
netserver = None
|
||||
laststate = {}
|
||||
|
||||
try:
|
||||
input = raw_input
|
||||
except NameError:
|
||||
pass
|
||||
|
||||
class BailOut(Exception):
|
||||
def __init__(self, errorcode=0):
|
||||
self.errorcode = errorcode
|
||||
|
||||
|
||||
def print_help():
|
||||
print("confetty provides a filesystem like interface to confluent. "
|
||||
@@ -189,9 +199,9 @@ def prompt():
|
||||
if os.environ.get('TERM', '') not in ('linux'):
|
||||
sys.stdout.write('\x1b]0;confetty: %s\x07' % target)
|
||||
try:
|
||||
return raw_input(target + ' -> ')
|
||||
return input(target + ' -> ')
|
||||
except KeyboardInterrupt:
|
||||
print ""
|
||||
print("")
|
||||
return ""
|
||||
except EOFError: # ctrl-d
|
||||
print("exit")
|
||||
@@ -290,7 +300,7 @@ currchildren = None
|
||||
|
||||
def print_result(res):
|
||||
if 'errorcode' in res or 'error' in res:
|
||||
print res['error']
|
||||
print(res['error'])
|
||||
return
|
||||
if 'databynode' in res:
|
||||
print_result(res['databynode'])
|
||||
@@ -303,9 +313,9 @@ def print_result(res):
|
||||
attrstr = '%s=%s' % (key, recurse_format(res[key]))
|
||||
elif not isinstance(res[key], dict):
|
||||
try:
|
||||
print '{0}: {1}'.format(key, res[key])
|
||||
print('{0}: {1}'.format(key, res[key]))
|
||||
except UnicodeEncodeError:
|
||||
print '{0}: {1}'.format(key, repr(res[key]))
|
||||
print('{0}: {1}'.format(key, repr(res[key])))
|
||||
continue
|
||||
elif 'value' in res[key] and res[key]['value'] is not None:
|
||||
attrstr = '%s="%s"' % (key, res[key]['value'])
|
||||
@@ -318,7 +328,7 @@ def print_result(res):
|
||||
else:
|
||||
sys.stdout.write('{0}: '.format(key))
|
||||
if isinstance(res[key], str) or isinstance(res[key], unicode):
|
||||
print res[key]
|
||||
print(res[key])
|
||||
else:
|
||||
print_result(res[key])
|
||||
continue
|
||||
@@ -353,7 +363,7 @@ def do_command(command, server):
|
||||
if argv[0] == 'exit':
|
||||
if os.environ.get('TERM', '') not in ('linux'):
|
||||
sys.stdout.write('\x1b]0;\x07')
|
||||
sys.exit(0)
|
||||
raise Bailout()
|
||||
elif argv[0] in ('help', '?'):
|
||||
return print_help()
|
||||
elif argv[0] == 'cd':
|
||||
@@ -417,10 +427,10 @@ def do_command(command, server):
|
||||
for res in session.read(targpath):
|
||||
if 'item' in res: # a link relation
|
||||
if type(res['item']) == dict:
|
||||
print res['item']["href"]
|
||||
print(res['item']["href"])
|
||||
else:
|
||||
for item in res['item']:
|
||||
print item["href"]
|
||||
print(item["href"])
|
||||
else: # generic attributes to list
|
||||
if 'error' in res:
|
||||
sys.stderr.write(res['error'] + '\n')
|
||||
@@ -434,6 +444,10 @@ def do_command(command, server):
|
||||
currconsole = targpath
|
||||
startrequest = {'operation': 'start', 'path': targpath,
|
||||
'parameters': {}}
|
||||
height, width = struct.unpack(
|
||||
'hh', fcntl.ioctl(sys.stdout, termios.TIOCGWINSZ, b'....'))[:2]
|
||||
startrequest['parameters']['width'] = width
|
||||
startrequest['parameters']['height'] = height
|
||||
for param in argv[2:]:
|
||||
(parmkey, parmval) = param.split("=")
|
||||
startrequest['parameters'][parmkey] = parmval
|
||||
@@ -478,7 +492,8 @@ def createresource(args):
|
||||
collection = targpath
|
||||
else:
|
||||
collection, _, resname = targpath.rpartition('/')
|
||||
keydata['name'] = resname
|
||||
if 'name' not in keydata:
|
||||
keydata['name'] = resname
|
||||
makecall(session.create, (collection, keydata))
|
||||
|
||||
|
||||
@@ -493,6 +508,13 @@ def makecall(callout, args):
|
||||
if 'errorcode' in response:
|
||||
exitcode = response['errorcode']
|
||||
sys.stderr.write('Error: ' + response['error'] + '\n')
|
||||
if 'databynode' in response:
|
||||
lresponse = response['databynode']
|
||||
for node in lresponse:
|
||||
if 'errorcode' in lresponse[node]:
|
||||
exitcode = lresponse[node]['errorcode']
|
||||
if 'error' in lresponse[node]:
|
||||
sys.stderr.write('{0}: Error - {1}\n'.format(node, lresponse[node]['error']))
|
||||
|
||||
|
||||
def clearvalues(resource, attribs):
|
||||
@@ -575,11 +597,20 @@ def fullpath_target(currpath, forcepath=False):
|
||||
ntarget += '/'
|
||||
return ntarget
|
||||
|
||||
def do_resize(a, b):
|
||||
if not inconsole:
|
||||
return
|
||||
height, width = struct.unpack(
|
||||
'hh', fcntl.ioctl(sys.stdout, termios.TIOCGWINSZ, b'....'))[:2]
|
||||
tlvdata.send(session.connection, {'operation': 'resize', 'width': width,
|
||||
'height': height})
|
||||
|
||||
|
||||
def startconsole(nodename):
|
||||
global inconsole
|
||||
global consolename
|
||||
global didconsole
|
||||
signal.signal(signal.SIGWINCH, do_resize)
|
||||
didconsole = True
|
||||
consolename = nodename
|
||||
tty.setraw(sys.stdin.fileno())
|
||||
@@ -598,11 +629,12 @@ def quitconfetty(code=0, fullexit=False, fixterm=True):
|
||||
if oldtcattr is not None:
|
||||
termios.tcsetattr(sys.stdin.fileno(), termios.TCSANOW, oldtcattr)
|
||||
# Request default color scheme, to undo potential weirdness of terminal
|
||||
sys.stdout.write('\x1b[m')
|
||||
if sys.stdout.isatty():
|
||||
sys.stdout.write('\x1b[m')
|
||||
if fullexit:
|
||||
if os.environ.get('TERM', '') not in ('linux'):
|
||||
if sys.stdout.isatty() and os.environ.get('TERM', '') not in ('linux'):
|
||||
sys.stdout.write('\x1b]0;\x07')
|
||||
sys.exit(code)
|
||||
raise BailOut(code)
|
||||
else:
|
||||
tlvdata.send(session.connection, {'operation': 'stop',
|
||||
'path': currconsole})
|
||||
@@ -763,7 +795,10 @@ def conserver_command(filehandle, localcommand):
|
||||
|
||||
def get_command_bytes(filehandle, localcommand, cmdlen):
|
||||
while len(localcommand) < cmdlen:
|
||||
ready, _, _ = select.select((filehandle,), (), (), 1)
|
||||
try:
|
||||
ready, _, _ = select.select((filehandle,), (), (), 1)
|
||||
except select.error:
|
||||
ready = ()
|
||||
if ready:
|
||||
localcommand += filehandle.read()
|
||||
return localcommand
|
||||
@@ -776,7 +811,10 @@ def check_escape_seq(currinput, filehandle):
|
||||
sys.stdout.flush()
|
||||
return conserver_command(
|
||||
filehandle, currinput[len(conserversequence):])
|
||||
ready, _, _ = select.select((filehandle,), (), (), 3)
|
||||
try:
|
||||
ready, _, _ = select.select((filehandle,), (), (), 3)
|
||||
except select.error:
|
||||
ready = ()
|
||||
if not ready: # 3 seconds of no typing
|
||||
break
|
||||
currinput += filehandle.read()
|
||||
@@ -789,6 +827,10 @@ parser.add_option("-s", "--server", dest="netserver",
|
||||
parser.add_option("-c", "--control", dest="controlpath",
|
||||
help="Path to offer terminal control",
|
||||
metavar="PATH")
|
||||
parser.add_option(
|
||||
'-m', '--mintime', default=0,
|
||||
help='Minimum time to run or else pause for input (used to keep a '
|
||||
'terminal from closing quickly on error)')
|
||||
opts, shellargs = parser.parse_args()
|
||||
|
||||
username = None
|
||||
@@ -813,49 +855,10 @@ def server_connect():
|
||||
passphrase = os.environ['CONFLUENT_PASSPHRASE']
|
||||
session.authenticate(username, passphrase)
|
||||
while not session.authenticated:
|
||||
username = raw_input("Name: ")
|
||||
username = input("Name: ")
|
||||
passphrase = getpass.getpass("Passphrase: ")
|
||||
session.authenticate(username, passphrase)
|
||||
|
||||
|
||||
try:
|
||||
server_connect()
|
||||
except EOFError, KeyboardInterrupt:
|
||||
sys.exit(0)
|
||||
except socket.gaierror:
|
||||
sys.stderr.write('Could not connect to confluent\n')
|
||||
sys.exit(1)
|
||||
# clear on start can help with readable of TUI, but it
|
||||
# can be annoying, so for now don't do it.
|
||||
# sys.stdout.write('\x1b[H\x1b[J')
|
||||
# sys.stdout.flush()
|
||||
|
||||
if sys.stdout.isatty():
|
||||
import readline
|
||||
|
||||
readline.parse_and_bind("tab: complete")
|
||||
readline.parse_and_bind("set bell-style none")
|
||||
dl = readline.get_completer_delims().replace('-', '')
|
||||
readline.set_completer_delims(dl)
|
||||
readline.set_completer(completer)
|
||||
|
||||
doexit = False
|
||||
inconsole = False
|
||||
pendingcommand = ""
|
||||
session_node = get_session_node(shellargs)
|
||||
if session_node is not None:
|
||||
consoleonly = True
|
||||
do_command("start /nodes/%s/console/session" % session_node, netserver)
|
||||
doexit = True
|
||||
elif shellargs:
|
||||
command = " ".join(shellargs)
|
||||
do_command(command, netserver)
|
||||
quitconfetty(fullexit=True, fixterm=False)
|
||||
|
||||
powerstate = None
|
||||
powertime = None
|
||||
|
||||
|
||||
def check_power_state():
|
||||
tlvdata.send(
|
||||
session.connection,
|
||||
@@ -864,83 +867,148 @@ def check_power_state():
|
||||
return
|
||||
|
||||
|
||||
while inconsole or not doexit:
|
||||
if inconsole:
|
||||
rdylist, _, _ = select.select(
|
||||
(sys.stdin, session.connection), (), (), 10)
|
||||
for fh in rdylist:
|
||||
if fh == session.connection:
|
||||
# this only should get called in the
|
||||
# case of a console session
|
||||
# each command should slurp up all relevant
|
||||
# recv potential
|
||||
#fh.read()
|
||||
try:
|
||||
data = tlvdata.recv(fh)
|
||||
except Exception:
|
||||
data = None
|
||||
if type(data) == dict:
|
||||
updatestatus(data)
|
||||
continue
|
||||
if data is not None:
|
||||
if clearpowermessage:
|
||||
sys.stdout.write("\x1b[2J\x1b[;H")
|
||||
clearpowermessage = False
|
||||
try:
|
||||
sys.stdout.write(data)
|
||||
except IOError: # Some times circumstances are bad
|
||||
# resort to byte at a time...
|
||||
for d in data:
|
||||
sys.stdout.write(d)
|
||||
now = time.time()
|
||||
if ('showtime' not in laststate or
|
||||
(now // 60) != laststate['showtime'] // 60):
|
||||
# don't bother churning if minute does not change
|
||||
laststate['showtime'] = now
|
||||
updatestatus()
|
||||
sys.stdout.flush()
|
||||
else:
|
||||
deadline = 5
|
||||
connected = False
|
||||
while not connected and deadline > 0:
|
||||
try:
|
||||
server_connect()
|
||||
connected = True
|
||||
except (socket.gaierror, socket.error):
|
||||
pass
|
||||
if not connected:
|
||||
time.sleep(1)
|
||||
deadline -=1
|
||||
if connected:
|
||||
do_command(
|
||||
"start /nodes/%s/console/session skipreplay=True" % consolename,
|
||||
netserver)
|
||||
else:
|
||||
doexit = True
|
||||
inconsole = False
|
||||
sys.stdout.write("\r\n[remote disconnected]\r\n")
|
||||
break
|
||||
else:
|
||||
try:
|
||||
myinput = fh.read()
|
||||
myinput = check_escape_seq(myinput, fh)
|
||||
if myinput:
|
||||
tlvdata.send(session.connection, myinput)
|
||||
except IOError:
|
||||
pass
|
||||
if powerstate is None or powertime < time.time() - 10: # Check powerstate every 10 seconds
|
||||
powertime = time.time()
|
||||
powerstate = True
|
||||
check_power_state()
|
||||
else:
|
||||
currcommand = prompt()
|
||||
try:
|
||||
do_command(currcommand, netserver)
|
||||
except socket.error:
|
||||
if sys.stdout.isatty():
|
||||
import readline
|
||||
|
||||
|
||||
def main():
|
||||
global inconsole
|
||||
try:
|
||||
server_connect()
|
||||
except (EOFError, KeyboardInterrupt) as _:
|
||||
raise BailOut(0)
|
||||
except socket.gaierror:
|
||||
sys.stderr.write('Could not connect to confluent\n')
|
||||
raise BailOut(1)
|
||||
# clear on start can help with readable of TUI, but it
|
||||
# can be annoying, so for now don't do it.
|
||||
# sys.stdout.write('\x1b[H\x1b[J')
|
||||
# sys.stdout.flush()
|
||||
global powerstate, powertime, clearpowermessage
|
||||
|
||||
|
||||
if sys.stdout.isatty():
|
||||
|
||||
readline.parse_and_bind("tab: complete")
|
||||
readline.parse_and_bind("set bell-style none")
|
||||
dl = readline.get_completer_delims().replace('-', '')
|
||||
readline.set_completer_delims(dl)
|
||||
readline.set_completer(completer)
|
||||
|
||||
doexit = False
|
||||
inconsole = False
|
||||
pendingcommand = ""
|
||||
session_node = get_session_node(shellargs)
|
||||
if session_node is not None:
|
||||
consoleonly = True
|
||||
do_command("start /nodes/%s/console/session" % session_node, netserver)
|
||||
doexit = True
|
||||
elif shellargs:
|
||||
command = " ".join(shellargs)
|
||||
do_command(command, netserver)
|
||||
quitconfetty(fullexit=True, fixterm=False)
|
||||
|
||||
powerstate = None
|
||||
powertime = None
|
||||
|
||||
while inconsole or not doexit:
|
||||
if inconsole:
|
||||
try:
|
||||
rdylist, _, _ = select.select(
|
||||
(sys.stdin, session.connection), (), (), 10)
|
||||
except select.error:
|
||||
rdylist = ()
|
||||
for fh in rdylist:
|
||||
if fh == session.connection:
|
||||
# this only should get called in the
|
||||
# case of a console session
|
||||
# each command should slurp up all relevant
|
||||
# recv potential
|
||||
#fh.read()
|
||||
try:
|
||||
data = tlvdata.recv(fh)
|
||||
except Exception:
|
||||
data = None
|
||||
if type(data) == dict:
|
||||
updatestatus(data)
|
||||
continue
|
||||
if data is not None:
|
||||
data = client.stringify(data)
|
||||
if clearpowermessage:
|
||||
sys.stdout.write("\x1b[2J\x1b[;H")
|
||||
clearpowermessage = False
|
||||
try:
|
||||
sys.stdout.write(data)
|
||||
except IOError: # Some times circumstances are bad
|
||||
# resort to byte at a time...
|
||||
for d in data:
|
||||
sys.stdout.write(d)
|
||||
now = time.time()
|
||||
if ('showtime' not in laststate or
|
||||
(now // 60) != laststate['showtime'] // 60):
|
||||
# don't bother churning if minute does not change
|
||||
laststate['showtime'] = now
|
||||
updatestatus()
|
||||
sys.stdout.flush()
|
||||
else:
|
||||
deadline = 5
|
||||
connected = False
|
||||
while not connected and deadline > 0:
|
||||
try:
|
||||
server_connect()
|
||||
connected = True
|
||||
except (socket.gaierror, socket.error):
|
||||
pass
|
||||
if not connected:
|
||||
time.sleep(1)
|
||||
deadline -=1
|
||||
if connected:
|
||||
do_command(
|
||||
"start /nodes/%s/console/session skipreplay=True" % consolename,
|
||||
netserver)
|
||||
else:
|
||||
doexit = True
|
||||
inconsole = False
|
||||
sys.stdout.write("\r\n[remote disconnected]\r\n")
|
||||
break
|
||||
else:
|
||||
try:
|
||||
myinput = fh.read()
|
||||
myinput = check_escape_seq(myinput, fh)
|
||||
if myinput:
|
||||
tlvdata.send(session.connection, myinput)
|
||||
except IOError:
|
||||
pass
|
||||
if powerstate is None or powertime < time.time() - 10: # Check powerstate every 10 seconds
|
||||
powertime = time.time()
|
||||
powerstate = True
|
||||
check_power_state()
|
||||
else:
|
||||
currcommand = prompt()
|
||||
try:
|
||||
server_connect()
|
||||
do_command(currcommand, netserver)
|
||||
except socket.error:
|
||||
doexit = True
|
||||
sys.stdout.write('Lost connection to server')
|
||||
quitconfetty(fullexit=True)
|
||||
try:
|
||||
server_connect()
|
||||
do_command(currcommand, netserver)
|
||||
except socket.error:
|
||||
doexit = True
|
||||
sys.stdout.write('Lost connection to server')
|
||||
quitconfetty(fullexit=True)
|
||||
|
||||
if __name__ == '__main__':
|
||||
errcode = 0
|
||||
deadline = 0
|
||||
if opts.mintime:
|
||||
deadline = os.times()[4] + float(opts.mintime)
|
||||
try:
|
||||
main()
|
||||
except BailOut as e:
|
||||
errcode = e.errorcode
|
||||
finally:
|
||||
if deadline and os.times()[4] < deadline:
|
||||
sys.stderr.write('[Exited early, hit enter to continue]')
|
||||
sys.stdin.readline()
|
||||
if errcode == 0:
|
||||
errcode = exitcode
|
||||
sys.exit(errcode)
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
#!/usr/bin/python2
|
||||
import optparse
|
||||
import signal
|
||||
import sys
|
||||
import os
|
||||
try:
|
||||
signal.signal(signal.SIGPIPE, signal.SIG_DFL)
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
path = os.path.dirname(os.path.realpath(__file__))
|
||||
path = os.path.realpath(os.path.join(path, '..', 'lib', 'python'))
|
||||
if path.startswith('/opt'):
|
||||
sys.path.append(path)
|
||||
|
||||
import confluent.client as client
|
||||
import confluent.sortutil as sortutil
|
||||
|
||||
|
||||
def lookupdata(data, key):
|
||||
ret = data.get(key, {}).get('value', '')
|
||||
if ret is None:
|
||||
ret = ''
|
||||
return ret
|
||||
|
||||
def main():
|
||||
argparser = optparse.OptionParser(
|
||||
usage='''\n %prog noderange -o ansible.hosts
|
||||
\n ''')
|
||||
argparser.add_option('-o', '--output',
|
||||
help='Ansible hosts file')
|
||||
(options, args) = argparser.parse_args()
|
||||
try:
|
||||
noderange = args[0]
|
||||
except IndexError:
|
||||
argparser.print_help()
|
||||
sys.exit(1)
|
||||
if not options.output:
|
||||
sys.stderr.write('Output file must be specified by -o\n')
|
||||
sys.exit(1)
|
||||
sess = client.Command()
|
||||
databynode = {}
|
||||
for res in sess.read('/noderange/{0}/attributes/all'.format(noderange)):
|
||||
for node in res.get('databynode', {}):
|
||||
if node not in databynode:
|
||||
databynode[node] = {}
|
||||
databynode[node].update(res['databynode'][node])
|
||||
nodesbygroup = {}
|
||||
with open(options.output, 'w') as importfile:
|
||||
needempty = False
|
||||
for node in sortutil.natural_sort(databynode):
|
||||
data = databynode[node]
|
||||
if not data.get('groups', []):
|
||||
importfile.write(node + '\n')
|
||||
needempty = True
|
||||
for g in data.get('groups', []):
|
||||
if g not in nodesbygroup:
|
||||
nodesbygroup[g] = set([node])
|
||||
else:
|
||||
nodesbygroup[g].add(node)
|
||||
if needempty:
|
||||
importfile.write('\n')
|
||||
for group in sortutil.natural_sort(nodesbygroup):
|
||||
importfile.write('[{0}]\n'.format(group))
|
||||
for node in sortutil.natural_sort(nodesbygroup[group]):
|
||||
importfile.write('{0}\n'.format(node))
|
||||
importfile.write('\n')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,76 @@
|
||||
#!/usr/bin/python2
|
||||
import csv
|
||||
import optparse
|
||||
import signal
|
||||
import sys
|
||||
import os
|
||||
try:
|
||||
signal.signal(signal.SIGPIPE, signal.SIG_DFL)
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
path = os.path.dirname(os.path.realpath(__file__))
|
||||
path = os.path.realpath(os.path.join(path, '..', 'lib', 'python'))
|
||||
if path.startswith('/opt'):
|
||||
sys.path.append(path)
|
||||
|
||||
import confluent.client as client
|
||||
import confluent.sortutil as sortutil
|
||||
|
||||
headers = '# Type,Serial Number,Current IP,Current username,Current password,New password,Recovery password,Switch enable password,New IPv4,IPv4 Subnet mask,IPv4 Default gateway,IPv4 DNS1,IPv4 DNS2,New IPv6,IPv6 Prefix,IPv6 Default gateway,IPv6 DNS1,IPv6 DNS2,Domain,Host name,Display name,Rack,Lowest Rack Unit,Height,Force,Stored credentials ID,Managed authentication,Group Name'.split(',')
|
||||
|
||||
def lookupdata(data, key):
|
||||
ret = data.get(key, {}).get('value', '')
|
||||
if ret is None:
|
||||
ret = ''
|
||||
return ret
|
||||
|
||||
def main():
|
||||
argparser = optparse.OptionParser(
|
||||
usage='''\n %prog noderange -o bulkimport.csv
|
||||
\n ''')
|
||||
argparser.add_option('-o', '--output',
|
||||
help='File to write for bulk import into xClarity Administrator')
|
||||
(options, args) = argparser.parse_args()
|
||||
try:
|
||||
noderange = args[0]
|
||||
except IndexError:
|
||||
argparser.print_help()
|
||||
sys.exit(1)
|
||||
if not options.output:
|
||||
sys.stderr.write('Output file must be specified by -o\n')
|
||||
sys.exit(1)
|
||||
if 'XCCUSER' not in os.environ or 'XCCPASS' not in os.environ:
|
||||
sys.stderr.write('Must specify XCCUSER and XCCPASS in environment variables\n')
|
||||
sys.exit(1)
|
||||
xccuser = os.environ['XCCUSER']
|
||||
xccpass = os.environ['XCCPASS']
|
||||
sess = client.Command()
|
||||
databynode = {}
|
||||
for res in sess.read('/noderange/{0}/attributes/all'.format(noderange)):
|
||||
for node in res.get('databynode', {}):
|
||||
if node not in databynode:
|
||||
databynode[node] = {}
|
||||
databynode[node].update(res['databynode'][node])
|
||||
for res in sess.read(
|
||||
'/noderange/{0}/configuration/management_controller/net_interfaces/management'.format(
|
||||
noderange)):
|
||||
for node in res.get('databynode', {}):
|
||||
currip = res['databynode'][node].get('ipv4_address', {}).get(
|
||||
'value', '')
|
||||
if currip:
|
||||
databynode[node]['hardwaremanagement.manager'] = {
|
||||
'value': currip.split('/', 1)[0]}
|
||||
with open(options.output, 'w') as importfile:
|
||||
bulkimport = csv.writer(importfile)
|
||||
bulkimport.writerow(headers)
|
||||
for node in sortutil.natural_sort(databynode):
|
||||
data = databynode[node]
|
||||
row = ['server', lookupdata(data, 'id.serial'), lookupdata(data, 'hardwaremanagement.manager'), xccuser, xccpass, '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', node, lookupdata(data, 'location.rack'), lookupdata(data, 'location.u'), '', '', '','', '']
|
||||
bulkimport.writerow(row)
|
||||
|
||||
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,103 @@
|
||||
#!/usr/bin/python2
|
||||
import csv
|
||||
import optparse
|
||||
import signal
|
||||
import sys
|
||||
import os
|
||||
try:
|
||||
signal.signal(signal.SIGPIPE, signal.SIG_DFL)
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
path = os.path.dirname(os.path.realpath(__file__))
|
||||
path = os.path.realpath(os.path.join(path, '..', 'lib', 'python'))
|
||||
if path.startswith('/opt'):
|
||||
sys.path.append(path)
|
||||
|
||||
import confluent.client as client
|
||||
import confluent.sortutil as sortutil
|
||||
|
||||
|
||||
def lookupdata(data, key):
|
||||
ret = data.get(key, {}).get('value', '')
|
||||
if ret is None:
|
||||
ret = ''
|
||||
return ret
|
||||
|
||||
def main():
|
||||
argparser = optparse.OptionParser(
|
||||
usage='''\n %prog noderange -o xcatnodes.def
|
||||
\n ''')
|
||||
argparser.add_option('-o', '--output',
|
||||
help='xCAT stanza file')
|
||||
argparser.add_option('-m', '--macs',
|
||||
help='xCAT macs.csv to write')
|
||||
(options, args) = argparser.parse_args()
|
||||
try:
|
||||
noderange = args[0]
|
||||
except IndexError:
|
||||
argparser.print_help()
|
||||
sys.exit(1)
|
||||
if not (options.output or options.macs):
|
||||
sys.stderr.write('Output file must be specified by -o or -m\n')
|
||||
sys.exit(1)
|
||||
sess = client.Command()
|
||||
databynode = {}
|
||||
for res in sess.read('/noderange/{0}/attributes/all'.format(noderange)):
|
||||
for node in res.get('databynode', {}):
|
||||
if node not in databynode:
|
||||
databynode[node] = {}
|
||||
databynode[node].update(res['databynode'][node])
|
||||
if options.output:
|
||||
with open(options.output, 'w') as importfile:
|
||||
for node in sortutil.natural_sort(databynode):
|
||||
xcatattrs = collect_attribute_data(databynode, node)
|
||||
importfile.write('{0}:\n'.format(node))
|
||||
importfile.write(' objtype=node\n')
|
||||
importfile.write(' arch=x86_64\n')
|
||||
importfile.write(' netboot=xnba\n')
|
||||
importfile.write(' mgt=ipmi\n')
|
||||
for attr in xcatattrs:
|
||||
if xcatattrs[attr] is None:
|
||||
continue
|
||||
importfile.write(' {0}={1}\n'.format(attr,
|
||||
xcatattrs[attr]))
|
||||
importfile.write('\n')
|
||||
if options.macs:
|
||||
with open(options.macs, 'w') as importfile:
|
||||
macsv = csv.writer(importfile)
|
||||
macsv.writerow(['#node', 'mac'])
|
||||
for node in sortutil.natural_sort(databynode):
|
||||
xcatattrs = collect_attribute_data(databynode, node)
|
||||
macsv.writerow([node, xcatattrs['mac']])
|
||||
|
||||
|
||||
|
||||
def collect_attribute_data(databynode, node):
|
||||
data = databynode[node]
|
||||
xcatattrs = {}
|
||||
xcatattrs['groups'] = ','.join(data.get('groups', []))
|
||||
xcatattrs['bmc'] = data.get('hardwaremanagement.manager', {}).get(
|
||||
'value', None)
|
||||
xcatattrs['mpa'] = data.get('enclosure.manager', {}).get(
|
||||
'value', None)
|
||||
xcatattrs['slotid'] = data.get('enclosure.bay', {}).get(
|
||||
'value', None)
|
||||
gotmac = False
|
||||
for key in data:
|
||||
if key.startswith('net.') and 'hwaddr' in key:
|
||||
currmac = data[key].get('value', None)
|
||||
if currmac:
|
||||
if gotmac:
|
||||
sys.stderr.write(
|
||||
'Ignoring {0} and using only {1} for mac, '
|
||||
'multiple macs not supported by '
|
||||
'confluent2xcat\n'.format(key, gotmac))
|
||||
continue
|
||||
gotmac = key
|
||||
xcatattrs['mac'] = currmac
|
||||
return xcatattrs
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,19 @@
|
||||
#!/bin/sh
|
||||
# This will take a given directory and make a 'big floppy image'
|
||||
# out of it, suitable for nodemedia upload.
|
||||
|
||||
if [ -z "$1" -o -z "$2" ]; then
|
||||
echo "Usage: $0 <directory> <imagefile>"
|
||||
exit 1
|
||||
fi
|
||||
# Get the needed payload side
|
||||
SIZE=$(du -sB 512 $1|awk '{print $1}')
|
||||
ENTRIES=$(find $1 |wc -l)
|
||||
# Also, each file and directory has overhead, pad size by 32kb per file,
|
||||
# which should be overkill but other values proved to be inadequate
|
||||
# A deeper understanding of VFAT would probably allow for more precise value
|
||||
SIZE=$((SIZE + ENTRIES * 64))
|
||||
dd if=/dev/zero of=$2 bs=512 count=$SIZE
|
||||
# Make a big single sided floppy with many many tracks, saves on complex math
|
||||
mformat -i $2 -d 1 -t $SIZE -s 1 -h 1 ::
|
||||
mcopy -i $2 -s $1/* ::
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python
|
||||
#!/usr/bin/python2
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2017 Lenovo
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
__author__ = 'alin37'
|
||||
|
||||
from getpass import getpass
|
||||
import optparse
|
||||
import os
|
||||
import signal
|
||||
@@ -35,14 +36,23 @@ if path.startswith('/opt'):
|
||||
import confluent.client as client
|
||||
|
||||
argparser = optparse.OptionParser(
|
||||
usage='''\n %prog [-b] noderange [list of attributes] \
|
||||
usage='''\n %prog [-b] noderange [list of attributes or 'all'] \
|
||||
\n %prog -c noderange <list of attributes> \
|
||||
\n %prog -e noderange <attribute names to set> \
|
||||
\n %prog noderange attribute1=value1 attribute2=value,...
|
||||
\n ''')
|
||||
argparser.add_option('-b', '--blame', action='store_true',
|
||||
help='Show information about how attributes inherited')
|
||||
argparser.add_option('-e', '--environment', action='store_true',
|
||||
help='Set attributes, but from environment variable of '
|
||||
'same name')
|
||||
argparser.add_option('-c', '--clear', action='store_true',
|
||||
help='Clear attributes')
|
||||
argparser.add_option('-p', '--prompt', action='store_true',
|
||||
help='Prompt for attribute values interactively')
|
||||
argparser.add_option('-m', '--maxnodes', type='int',
|
||||
help='Prompt if trying to set attributes on more '
|
||||
'than specified number of nodes')
|
||||
(options, args) = argparser.parse_args()
|
||||
|
||||
|
||||
@@ -63,12 +73,25 @@ exitcode = 0
|
||||
nodetype="noderange"
|
||||
|
||||
if len(args) > 1:
|
||||
if "=" in args[1] or options.clear:
|
||||
if "=" in args[1] or options.clear or options.environment or options.prompt:
|
||||
if "=" in args[1] and options.clear:
|
||||
print("Can not clear and set at the same time!")
|
||||
argparser.print_help()
|
||||
sys.exit(1)
|
||||
exitcode=client.updateattrib(session,args,nodetype, noderange, options)
|
||||
argassign = None
|
||||
if options.prompt:
|
||||
argassign = {}
|
||||
for arg in args[1:]:
|
||||
oneval = 1
|
||||
twoval = 2
|
||||
while oneval != twoval:
|
||||
oneval = getpass('Enter value for {0}: '.format(arg))
|
||||
twoval = getpass('Confirm value for {0}: '.format(arg))
|
||||
if oneval != twoval:
|
||||
print('Values did not match.')
|
||||
argassign[arg] = twoval
|
||||
session.stop_if_noderange_over(noderange, options.maxnodes)
|
||||
exitcode=client.updateattrib(session,args,nodetype, noderange, options, argassign)
|
||||
try:
|
||||
# setting user output to what the user inputs
|
||||
if args[1] == 'all':
|
||||
@@ -107,6 +130,6 @@ else:
|
||||
sys.stderr.write(res['error'] + '\n')
|
||||
exitcode = 1
|
||||
else:
|
||||
print res['item']['href'].replace('/', '')
|
||||
print(res['item']['href'].replace('/', ''))
|
||||
|
||||
sys.exit(exitcode)
|
||||
sys.exit(exitcode)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python
|
||||
#!/usr/bin/python2
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2015-2017 Lenovo
|
||||
@@ -32,6 +32,8 @@ if path.startswith('/opt'):
|
||||
import confluent.client as client
|
||||
|
||||
argparser = optparse.OptionParser(usage="Usage: %prog <noderange>")
|
||||
argparser.add_option('-m', '--maxnodes', type='int',
|
||||
help='Number of nodes to affect before prompting for confirmation')
|
||||
(options, args) = argparser.parse_args()
|
||||
try:
|
||||
noderange = args[0]
|
||||
@@ -43,7 +45,7 @@ session = client.Command()
|
||||
exitcode = 0
|
||||
|
||||
errorNodes = set([])
|
||||
|
||||
session.stop_if_noderange_over(noderange, options.maxnodes)
|
||||
success = session.simple_noderange_command(noderange, 'configuration/management_controller/reset', 'reset', key='state', errnodes=errorNodes) # = 0 if successful
|
||||
|
||||
# Determine which nodes were successful and print them
|
||||
@@ -56,7 +58,7 @@ for node in session.read('/noderange/{0}/nodes/'.format(noderange)):
|
||||
goodNodes = allNodes - errorNodes
|
||||
|
||||
for node in goodNodes:
|
||||
print node + ": BMC Reset Successful"
|
||||
print(node + ": BMC Reset Successful")
|
||||
|
||||
|
||||
sys.exit(success)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python
|
||||
#!/usr/bin/python2
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2015 Lenovo
|
||||
@@ -35,10 +35,17 @@ argparser = optparse.OptionParser()
|
||||
argparser.add_option('-b', '--bios', dest='biosmode',
|
||||
action='store_true', default=False,
|
||||
help='Request BIOS style boot (rather than UEFI)')
|
||||
argparser.add_option('-u', '--uefi', dest='uefimode',
|
||||
action='store_true', default=False,
|
||||
help='Request UEFI style boot (rather than BIOS)')
|
||||
argparser.add_option('-p', '--persist', dest='persist', action='store_true',
|
||||
default=False,
|
||||
help='Request the boot device be persistent rather than '
|
||||
'one time')
|
||||
argparser.add_option('-m', '--maxnodes', type='int',
|
||||
help='Specify a maximum number of '
|
||||
'nodes to boot, '
|
||||
'prompting if over the threshold')
|
||||
|
||||
(options, args) = argparser.parse_args()
|
||||
|
||||
@@ -63,6 +70,7 @@ else:
|
||||
bootmode = 'uefi'
|
||||
|
||||
errnodes = set([])
|
||||
session.stop_if_noderange_over(noderange, options.maxnodes)
|
||||
rc = session.simple_noderange_command(noderange, '/boot/nextdevice', bootdev,
|
||||
bootmode=bootmode,
|
||||
persistent=options.persist,
|
||||
|
||||
+193
-47
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python
|
||||
#!/usr/bin/python2
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2017 Lenovo
|
||||
@@ -19,6 +19,7 @@
|
||||
import os
|
||||
import signal
|
||||
import optparse
|
||||
import shlex
|
||||
import sys
|
||||
|
||||
try:
|
||||
@@ -42,7 +43,35 @@ def bailout(msg, code=1):
|
||||
sys.exit(code)
|
||||
|
||||
|
||||
argparser = optparse.OptionParser()
|
||||
argparser = optparse.OptionParser(usage="Usage: %prog [options] noderange [option|option=value]")
|
||||
argparser.add_option('-c', '--comparedefault', dest='comparedefault',
|
||||
action='store_true', default=False,
|
||||
help='Compare given settings to default or list settings '
|
||||
'that are non default')
|
||||
argparser.add_option('-b', '--batch', dest='batch', metavar='settings.batch',
|
||||
default=False, help='Provide settings in a batch file')
|
||||
argparser.add_option('-d', '--detail', dest='detail',
|
||||
action='store_true', default=False,
|
||||
help='Provide verbose information as available, such as '
|
||||
'help text and possible valid values')
|
||||
argparser.add_option('-x', '--exclude', dest='exclude',
|
||||
action='store_true', default=False,
|
||||
help='Treat positional arguments as items to not '
|
||||
'examine, compare, or restore default')
|
||||
argparser.add_option('-a', '--advanced', dest='advanced',
|
||||
action='store_true', default=False,
|
||||
help='Include advanced settings, which are normally not '
|
||||
'intended to be used without direction from the '
|
||||
'relevant server vendor.')
|
||||
argparser.add_option('-r', '--restoredefault', default=False,
|
||||
dest='restoredefault', metavar="COMPONENT",
|
||||
help='Restore the configuration of the node '
|
||||
'to factory default for given component. '
|
||||
'Currently only uefi is supported')
|
||||
argparser.add_option('-m', '--maxnodes', type='int',
|
||||
help='Specify a maximum number of '
|
||||
'nodes to configure, '
|
||||
'prompting if over the threshold')
|
||||
(options, args) = argparser.parse_args()
|
||||
|
||||
cfgpaths = {
|
||||
@@ -55,6 +84,8 @@ cfgpaths = {
|
||||
'bmc.ipv4_gateway': (
|
||||
'configuration/management_controller/net_interfaces/management',
|
||||
'ipv4_gateway'),
|
||||
'bmc.hostname': (
|
||||
'configuration/management_controller/hostname', 'hostname'),
|
||||
}
|
||||
|
||||
autodeps = {
|
||||
@@ -70,60 +101,151 @@ client.check_globbing(noderange)
|
||||
setmode = None
|
||||
assignment = {}
|
||||
queryparms = {}
|
||||
printsys = []
|
||||
printbmc = []
|
||||
printallbmc = False
|
||||
setsys = {}
|
||||
forceset = False
|
||||
needval = None
|
||||
|
||||
|
||||
if len(args) == 1:
|
||||
if len(args) == 1 or options.exclude:
|
||||
if not options.exclude:
|
||||
printsys = 'all'
|
||||
for candidate in cfgpaths:
|
||||
path, attrib = cfgpaths[candidate]
|
||||
path = '/noderange/{0}/{1}'.format(noderange, path)
|
||||
if path not in queryparms:
|
||||
queryparms[path] = {}
|
||||
queryparms[path][attrib] = candidate
|
||||
for param in args[1:]:
|
||||
if '=' in param:
|
||||
if setmode is None:
|
||||
|
||||
|
||||
def _assign_value():
|
||||
if key not in cfgpaths:
|
||||
setsys[key] = value
|
||||
for depkey, depval in autodeps.get(key, []):
|
||||
assignment[depkey] = depval
|
||||
assignment[key] = value
|
||||
|
||||
|
||||
def parse_config_line(arguments):
|
||||
global setmode, printallbmc, forceset, key, value, needval, candidate, path, attrib
|
||||
for param in arguments:
|
||||
if param == 'show':
|
||||
continue # forgive muscle memory of pasu users
|
||||
if param == 'set':
|
||||
setmode = True
|
||||
if setmode != True:
|
||||
bailout('Cannot do set and query in same command')
|
||||
key, _, value = param.partition('=')
|
||||
if key not in cfgpaths:
|
||||
bailout('Unrecognized setting: {0}'.format(key))
|
||||
for depkey, depval in autodeps.get(key, []):
|
||||
assignment[depkey] = depval
|
||||
assignment[key] = value
|
||||
else:
|
||||
if setmode is None:
|
||||
setmode = False
|
||||
if setmode != False:
|
||||
bailout('Cannot do set and query in same command')
|
||||
if '.' not in param:
|
||||
matchedparms = False
|
||||
for candidate in cfgpaths:
|
||||
if candidate.startswith('{0}.'.format(param)):
|
||||
matchedparms = True
|
||||
path, attrib = cfgpaths[candidate]
|
||||
path = '/noderange/{0}/{1}'.format(noderange, path)
|
||||
if path not in queryparms:
|
||||
queryparms[path] = {}
|
||||
queryparms[path][attrib] = candidate
|
||||
if not matchedparms:
|
||||
bailout('Unrecognized settings category: {0}'.format(param))
|
||||
elif param not in cfgpaths:
|
||||
bailout('Unrecognized parameter: {0}'.format(param))
|
||||
forceset = True
|
||||
continue
|
||||
if needval:
|
||||
key = needval
|
||||
needval = None
|
||||
value = param
|
||||
_assign_value()
|
||||
continue
|
||||
if '=' in param or param[-1] == ':' or forceset:
|
||||
if setmode is None:
|
||||
setmode = True
|
||||
if setmode != True:
|
||||
bailout('Cannot do set and query in same command')
|
||||
if '=' in param:
|
||||
key, _, value = param.partition('=')
|
||||
_assign_value()
|
||||
elif param[-1] == ':':
|
||||
needval = param[:-1]
|
||||
else:
|
||||
needval = param
|
||||
else:
|
||||
path, attrib = cfgpaths[param]
|
||||
path = '/noderange/{0}/{1}'.format(noderange, path)
|
||||
if path not in queryparms:
|
||||
queryparms[path] = {}
|
||||
queryparms[path][attrib] = param
|
||||
if setmode is None:
|
||||
setmode = False
|
||||
if setmode != False:
|
||||
bailout('Cannot do set and query in same command')
|
||||
if '.' not in param:
|
||||
if param == 'bmc':
|
||||
printallbmc = True
|
||||
matchedparms = False
|
||||
for candidate in cfgpaths:
|
||||
if candidate.startswith('{0}.'.format(param)):
|
||||
matchedparms = True
|
||||
if not options.exclude:
|
||||
path, attrib = cfgpaths[candidate]
|
||||
path = '/noderange/{0}/{1}'.format(noderange, path)
|
||||
if path not in queryparms:
|
||||
queryparms[path] = {}
|
||||
queryparms[path][attrib] = candidate
|
||||
else:
|
||||
try:
|
||||
del queryparms[path]
|
||||
except KeyError:
|
||||
pass
|
||||
if not matchedparms:
|
||||
printsys.append(param)
|
||||
elif param not in cfgpaths:
|
||||
if param.startswith('bmc.'):
|
||||
printbmc.append(param.replace('bmc.', ''))
|
||||
else:
|
||||
printsys.append(param)
|
||||
else:
|
||||
path, attrib = cfgpaths[param]
|
||||
path = '/noderange/{0}/{1}'.format(noderange, path)
|
||||
if path not in queryparms:
|
||||
queryparms[path] = {}
|
||||
queryparms[path][attrib] = param
|
||||
|
||||
if options.batch:
|
||||
printsys = []
|
||||
argfile = open(options.batch, 'r')
|
||||
argset = argfile.readline()
|
||||
while argset:
|
||||
try:
|
||||
argset = argset[:argset.index('#')]
|
||||
except ValueError:
|
||||
pass
|
||||
argset = argset.strip()
|
||||
if argset:
|
||||
parse_config_line(shlex.split(argset))
|
||||
argset = argfile.readline()
|
||||
else:
|
||||
parse_config_line(args[1:])
|
||||
session = client.Command()
|
||||
rcode = 0
|
||||
if options.restoredefault:
|
||||
session.stop_if_noderange_over(noderange, options.maxnodes)
|
||||
if options.restoredefault.lower() in (
|
||||
'sys', 'system', 'uefi', 'bios'):
|
||||
for fr in session.update(
|
||||
'/noderange/{0}/configuration/system/clear'.format(noderange),
|
||||
{'clear': True}):
|
||||
rcode |= client.printerror(fr)
|
||||
sys.exit(rcode)
|
||||
elif options.restoredefault.lower() in (
|
||||
'bmc', 'imm', 'xcc'):
|
||||
for fr in session.update(
|
||||
'/noderange/{0}/configuration/management_controller/clear'.format(noderange),
|
||||
{'clear': True}):
|
||||
rcode |= client.printerror(fr)
|
||||
sys.exit(rcode)
|
||||
else:
|
||||
sys.stderr.write(
|
||||
'Unrecognized component to restore defaults: {0}\n'.format(
|
||||
options.restoredefault))
|
||||
sys.exit(1)
|
||||
if setmode:
|
||||
session.stop_if_noderange_over(noderange, options.maxnodes)
|
||||
if options.exclude:
|
||||
sys.stderr.write('Cannot use exclude and assign at the same time\n')
|
||||
sys.exit(1)
|
||||
updatebypath = {}
|
||||
attrnamebypath = {}
|
||||
for key in assignment:
|
||||
if key not in cfgpaths:
|
||||
bailout('Unknown settings key: {0}'.format(key))
|
||||
path, attrib = cfgpaths[key]
|
||||
if key.startswith('bmc.'):
|
||||
path = 'configuration/management_controller/extended/all'
|
||||
attrib = key.replace('bmc.', '')
|
||||
else:
|
||||
path = 'configuration/system/all'
|
||||
attrib = key
|
||||
else:
|
||||
path, attrib = cfgpaths[key]
|
||||
if path not in updatebypath:
|
||||
updatebypath[path] = {}
|
||||
attrnamebypath[path] = {}
|
||||
@@ -132,16 +254,40 @@ if setmode:
|
||||
# well, we want to expand things..
|
||||
# check ipv4, if requested change method to static
|
||||
for path in updatebypath:
|
||||
for r in session.update('/noderange/{0}/{1}'.format(noderange, path),
|
||||
for fr in session.update('/noderange/{0}/{1}'.format(noderange, path),
|
||||
updatebypath[path]):
|
||||
for node in r:
|
||||
keyval = r[node]['value']
|
||||
rcode |= client.printerror(fr)
|
||||
for node in fr.get('databynode', []):
|
||||
r = fr['databynode'][node]
|
||||
rcode |= client.printerror(r, node)
|
||||
if 'value' not in r:
|
||||
continue
|
||||
keyval = r['value']
|
||||
key, val = keyval.split('=')
|
||||
if key in attrnamebypath[path]:
|
||||
key = attrnamebypath[path][key]
|
||||
print('{0}: {1}: {2}'.format(node, key, val))
|
||||
else:
|
||||
for path in queryparms:
|
||||
client.print_attrib_path(path, session, list(queryparms[path]),
|
||||
NullOpt(), queryparms[path])
|
||||
|
||||
if options.comparedefault:
|
||||
continue
|
||||
rc = client.print_attrib_path(path, session, list(queryparms[path]),
|
||||
NullOpt(), queryparms[path])
|
||||
if rc:
|
||||
sys.exit(rc)
|
||||
if printbmc or printallbmc:
|
||||
rcode = client.print_attrib_path(
|
||||
'/noderange/{0}/configuration/management_controller/extended/all'.format(noderange),
|
||||
session, printbmc, options, attrprefix='bmc.')
|
||||
if printsys or options.exclude:
|
||||
if printsys == 'all':
|
||||
printsys = []
|
||||
if (options.comparedefault or printsys == []) and not options.advanced:
|
||||
path = '/noderange/{0}/configuration/system/all'.format(noderange)
|
||||
else:
|
||||
path = '/noderange/{0}/configuration/system/advanced'.format(
|
||||
noderange)
|
||||
|
||||
rcode = client.print_attrib_path(path, session, printsys,
|
||||
options)
|
||||
sys.exit(rcode)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python
|
||||
#!/usr/bin/python2
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2015 Lenovo
|
||||
@@ -17,7 +17,14 @@
|
||||
|
||||
import optparse
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
path = os.path.dirname(os.path.realpath(__file__))
|
||||
path = os.path.realpath(os.path.join(path, '..', 'lib', 'python'))
|
||||
if path.startswith('/opt'):
|
||||
sys.path.append(path)
|
||||
import confluent.client as client
|
||||
import confluent.sortutil as sortutil
|
||||
|
||||
confettypath = os.path.join(os.path.dirname(sys.argv[0]), 'confetty')
|
||||
argparser = optparse.OptionParser(
|
||||
@@ -26,9 +33,45 @@ argparser = optparse.OptionParser(
|
||||
"ctrl-'e', then release ctrl, then 'c', then '?' for a full list. "
|
||||
"For example, ctrl-'e', then 'c', then '.' will exit the current "
|
||||
"console")
|
||||
argparser.add_option('-t', '--tile', action='store_true', default=False,
|
||||
help='Tile console windows in the terminal')
|
||||
(options, args) = argparser.parse_args()
|
||||
if len(args) != 1:
|
||||
argparser.print_help()
|
||||
sys.exit(1)
|
||||
os.execl(confettypath, confettypath, 'start',
|
||||
'/nodes/{0}/console/session'.format(args[0]))
|
||||
if options.tile:
|
||||
null = open('/dev/null', 'w')
|
||||
nodes = []
|
||||
sess = client.Command()
|
||||
for res in sess.read('/noderange/{0}/nodes/'.format(args[0])):
|
||||
node = res.get('item', {}).get('href', '/').replace('/', '')
|
||||
if not node:
|
||||
sys.stderr.write(res.get('error', repr(res)) + '\n')
|
||||
sys.exit(1)
|
||||
nodes.append(node)
|
||||
initial = True
|
||||
pane = 0
|
||||
for node in sortutil.natural_sort(nodes):
|
||||
if initial:
|
||||
initial = False
|
||||
subprocess.call(
|
||||
['tmux', 'new-session', '-d', '-s',
|
||||
'nodeconsole_{0}'.format(os.getpid()), '-x', '800', '-y',
|
||||
'800', '{0} -m 5 start /nodes/{1}/console/session'.format(
|
||||
confettypath, node)])
|
||||
else:
|
||||
subprocess.call(['tmux', 'select-pane', '-t', str(pane)])
|
||||
subprocess.call(['tmux', 'set-option', 'pane-border-status', 'top'], stderr=null)
|
||||
pane += 1
|
||||
subprocess.call(
|
||||
['tmux', 'split', '-h',
|
||||
'{0} -m 5 start /nodes/{1}/console/session'.format(
|
||||
confettypath, node)])
|
||||
subprocess.call(['tmux', 'select-layout', 'tiled'], stdout=null)
|
||||
subprocess.call(['tmux', 'select-pane', '-t', '0'])
|
||||
subprocess.call(['tmux', 'set-option', 'pane-border-status', 'top'], stderr=null)
|
||||
os.execlp('tmux', 'tmux', 'attach', '-t', 'nodeconsole_{0}'.format(
|
||||
os.getpid()))
|
||||
else:
|
||||
os.execl(confettypath, confettypath, 'start',
|
||||
'/nodes/{0}/console/session'.format(args[0]))
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python
|
||||
#!/usr/bin/python2
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2017 Lenovo
|
||||
@@ -47,7 +47,7 @@ session = client.Command()
|
||||
exitcode = 0
|
||||
attribs = {'name': noderange}
|
||||
for arg in args[1:]:
|
||||
key, val = arg.split('=')
|
||||
key, val = arg.split('=', 1)
|
||||
attribs[key] = val
|
||||
for r in session.create('/noderange/', attribs):
|
||||
if 'error' in r:
|
||||
@@ -55,4 +55,4 @@ for r in session.create('/noderange/', attribs):
|
||||
exitcode |= 1
|
||||
if 'created' in r:
|
||||
print('{0}: created'.format(r['created']))
|
||||
sys.exit(exitcode)
|
||||
sys.exit(exitcode)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python
|
||||
#!/usr/bin/python2
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2017 Lenovo
|
||||
@@ -19,6 +19,7 @@ import csv
|
||||
import optparse
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
path = os.path.dirname(os.path.realpath(__file__))
|
||||
path = os.path.realpath(os.path.join(path, '..', 'lib', 'python'))
|
||||
@@ -26,31 +27,49 @@ if path.startswith('/opt'):
|
||||
sys.path.append(path)
|
||||
|
||||
import confluent.client as client
|
||||
import confluent.sortutil as sortutil
|
||||
|
||||
tabformat = '{0:>15}|{1:>15}|{2:>15}|{3:>36}|{4:>17}|{5:>12}|{6:>48}'
|
||||
columns = ['Node', 'Model', 'Serial', 'UUID', 'Mac Address', 'Type',
|
||||
'Current IP Addresses']
|
||||
delimit = ['-' * 15, '-' * 15, '-' * 15, '-' * 36, '-' * 17, '-' * 12,
|
||||
'-' * 48]
|
||||
defcolumns = ['Node', 'Model', 'Serial', 'UUID', 'Mac Address', 'Type',
|
||||
'Current IP Addresses']
|
||||
columnmapping = {
|
||||
'Node': 'nodename',
|
||||
'Model': 'modelnumber',
|
||||
'Serial': 'serialnumber',
|
||||
'UUID': 'uuid',
|
||||
'Type': 'types',
|
||||
'Current IP Addresses': 'ipaddrs',
|
||||
'IP': 'ipaddrs',
|
||||
'Bay': 'bay',
|
||||
'Mac Address': 'macs',
|
||||
'Mac': 'macs',
|
||||
'Switch': 'switch',
|
||||
'Port': 'port',
|
||||
'Advertised IP': 'otheripaddrs',
|
||||
'Other IP': 'otheripaddrs',
|
||||
}
|
||||
|
||||
|
||||
def dumpmacs(procinfo):
|
||||
return ','.join(procinfo['macs']) # + procinfo.get('relatedmacs', []))
|
||||
|
||||
|
||||
def print_disco(options, session, currmac):
|
||||
def print_disco(options, session, currmac, outhandler, columns):
|
||||
procinfo = {}
|
||||
for tmpinfo in session.read('/discovery/by-mac/{0}'.format(currmac)):
|
||||
|
||||
procinfo.update(tmpinfo)
|
||||
record = [procinfo['nodename'], procinfo['modelnumber'],
|
||||
procinfo['serialnumber'], procinfo['uuid'], dumpmacs(procinfo),
|
||||
','.join(procinfo['types']),
|
||||
','.join(sorted(procinfo['ipaddrs']))]
|
||||
if options.csv:
|
||||
csv.writer(sys.stdout).writerow(record)
|
||||
else:
|
||||
print(tabformat.format(*record))
|
||||
if 'Switch' in columns or 'Port' in columns:
|
||||
for tmpinfo in session.read(
|
||||
'/networking/macs/by-mac/{0}'.format(currmac)):
|
||||
if 'ports' in tmpinfo:
|
||||
# The api sorts so that the most specific available value
|
||||
# is last
|
||||
procinfo.update(tmpinfo['ports'][-1])
|
||||
record = []
|
||||
for col in columns:
|
||||
rawcol = columnmapping[col]
|
||||
rawval = procinfo.get(rawcol, '')
|
||||
if isinstance(rawval, list):
|
||||
record.append(','.join(rawval))
|
||||
else:
|
||||
record.append(str(rawval))
|
||||
outhandler.add_row(record)
|
||||
|
||||
|
||||
def process_header(header):
|
||||
@@ -72,7 +91,9 @@ def process_header(header):
|
||||
elif datum in ('bmc', 'imm', 'xcc'):
|
||||
fields.append('hardwaremanagement.manager')
|
||||
elif datum in ('bmc gateway', 'xcc gateway', 'imm gateway'):
|
||||
fields.append('net.bmc.gateway')
|
||||
fields.append('net.bmc.ipv4_gateway')
|
||||
elif datum in ('bmc_gateway', 'xcc_gateway', 'imm_gateway'):
|
||||
fields.append('net.bmc.ipv4_gateway')
|
||||
elif datum in ('bmcuser', 'username', 'user'):
|
||||
fields.append('secret.hardwaremanagementuser')
|
||||
elif datum in ('bmcpass', 'password', 'pass'):
|
||||
@@ -122,24 +143,44 @@ def datum_to_attrib(datum):
|
||||
del datum['node']
|
||||
return datum
|
||||
|
||||
unique_fields = frozenset(['serial', 'mac', 'uuid'])
|
||||
|
||||
def import_csv(options, session):
|
||||
nodedata = []
|
||||
unique_data = {}
|
||||
with open(options.importfile, 'r') as datasrc:
|
||||
records = csv.reader(datasrc)
|
||||
fields = process_header(next(records))
|
||||
for field in fields:
|
||||
if field in unique_fields:
|
||||
unique_data[field] = set([])
|
||||
broken = False
|
||||
for record in records:
|
||||
currfields = list(fields)
|
||||
nodedatum = {}
|
||||
for datum in record:
|
||||
nodedatum[currfields.pop(0)] = datum
|
||||
currfield = currfields.pop(0)
|
||||
if currfield in unique_fields:
|
||||
if datum in unique_data[currfield]:
|
||||
sys.stderr.write(
|
||||
"Import contains duplicate values "
|
||||
"({0} with value {1}\n".format(currfield, datum)
|
||||
)
|
||||
sys.exit(1)
|
||||
unique_data[currfield].add(datum)
|
||||
nodedatum[currfield] = datum
|
||||
if not datum_complete(nodedatum):
|
||||
sys.exit(1)
|
||||
if not search_record(nodedatum, options, session) and not broken:
|
||||
blocking_scan(session)
|
||||
if not search_record(nodedatum, options, session):
|
||||
sys.stderr.write(
|
||||
"Could not match the following data: " +
|
||||
repr(nodedatum) + '\n')
|
||||
sys.exit(1)
|
||||
broken = True
|
||||
nodedata.append(nodedatum)
|
||||
if broken:
|
||||
sys.exit(1)
|
||||
for datum in nodedata:
|
||||
maclist = search_record(datum, options, session)
|
||||
datum = datum_to_attrib(datum)
|
||||
@@ -165,17 +206,41 @@ def import_csv(options, session):
|
||||
|
||||
|
||||
def list_discovery(options, session):
|
||||
if options.csv:
|
||||
csv.writer(sys.stdout).writerow(columns)
|
||||
orderby = None
|
||||
if options.fields:
|
||||
columns = []
|
||||
for field in options.fields.split(','):
|
||||
for cdt in columnmapping:
|
||||
if cdt.lower().replace(
|
||||
' ', '') == field.lower().replace(' ', ''):
|
||||
columns.append(cdt)
|
||||
else:
|
||||
print(tabformat.format(*columns))
|
||||
print(tabformat.format(*delimit))
|
||||
columns = defcolumns
|
||||
if options.order:
|
||||
for field in columns:
|
||||
if options.order.lower() == field.lower():
|
||||
orderby = field
|
||||
outhandler = client.Tabulator(columns)
|
||||
for mac in list_matching_macs(options, session):
|
||||
print_disco(options, session, mac)
|
||||
print_disco(options, session, mac, outhandler, columns)
|
||||
if options.csv:
|
||||
outhandler.write_csv(sys.stdout, orderby)
|
||||
else:
|
||||
for row in outhandler.get_table(orderby):
|
||||
print(row)
|
||||
|
||||
def clear_discovery(options, session):
|
||||
for mac in list_matching_macs(options, session):
|
||||
for res in session.delete('/discovery/by-mac/{0}'.format(mac)):
|
||||
if 'deleted' in res:
|
||||
print('Cleared info for {0}'.format(res['deleted']))
|
||||
else:
|
||||
print(repr(res))
|
||||
|
||||
def list_matching_macs(options, session):
|
||||
def list_matching_macs(options, session, node=None):
|
||||
path = '/discovery/'
|
||||
if node:
|
||||
path += 'by-node/{0}/'.format(node)
|
||||
if options.model:
|
||||
path += 'by-model/{0}/'.format(options.model)
|
||||
if options.serial:
|
||||
@@ -184,6 +249,10 @@ def list_matching_macs(options, session):
|
||||
path += 'by-uuid/{0}/'.format(options.uuid)
|
||||
if options.type:
|
||||
path += 'by-type/{0}/'.format(options.type)
|
||||
if options.state:
|
||||
if options.state == 'unknown':
|
||||
options.state = 'unidentified'
|
||||
path += 'by-state/{0}/'.format(options.state).lower()
|
||||
if options.mac:
|
||||
path += 'by-mac/{0}'.format(options.mac)
|
||||
result = list(session.read(path))[0]
|
||||
@@ -194,21 +263,25 @@ def list_matching_macs(options, session):
|
||||
path += 'by-mac/'
|
||||
return [x['item']['href'] for x in session.read(path)]
|
||||
|
||||
def assign_discovery(options, session):
|
||||
def assign_discovery(options, session, needid=True):
|
||||
abort = False
|
||||
if options.importfile:
|
||||
return import_csv(options, session)
|
||||
if not (options.serial or options.uuid or options.mac):
|
||||
if not options.node:
|
||||
sys.stderr.write("Node (-n) must be specified for assignment\n")
|
||||
abort = True
|
||||
if needid and not (options.serial or options.uuid or options.mac):
|
||||
sys.stderr.write(
|
||||
"UUID (-u), serial (-s), or ether address (-e) required for "
|
||||
"assignment\n")
|
||||
abort = True
|
||||
if not options.node:
|
||||
sys.stderr.write("Node (-n) must be specified for assignment\n")
|
||||
abort = True
|
||||
if abort:
|
||||
sys.exit(1)
|
||||
matches = list_matching_macs(options, session)
|
||||
matches = list_matching_macs(options, session, None if needid else options.node)
|
||||
if not matches:
|
||||
# Do a rescan to catch missing requested data
|
||||
blocking_scan(session)
|
||||
matches = list_matching_macs(options, session, None if needid else options.node)
|
||||
if not matches:
|
||||
sys.stderr.write("No matching discovery candidates found\n")
|
||||
sys.exit(1)
|
||||
@@ -219,11 +292,16 @@ def assign_discovery(options, session):
|
||||
else:
|
||||
print(repr(res))
|
||||
|
||||
def blocking_scan(session):
|
||||
list(session.update('/discovery/rescan', {'rescan': 'start'}))
|
||||
while(list(session.read('/discovery/rescan'))[0].get('scanning', False)):
|
||||
time.sleep(0.5)
|
||||
list(session.update('/networking/macs/rescan', {'rescan': 'start'}))
|
||||
|
||||
|
||||
def main():
|
||||
parser = optparse.OptionParser(
|
||||
usage='Usage: %prog [list|assign|rescan] [options]')
|
||||
usage='Usage: %prog [list|assign|rescan|clear] [options]')
|
||||
# -a for 'address' maybe?
|
||||
# order by
|
||||
# show state (discovered or..
|
||||
@@ -234,6 +312,9 @@ def main():
|
||||
parser.add_option('-m', '--model', dest='model',
|
||||
help='Operate with nodes matching the specified model '
|
||||
'number', metavar='MODEL')
|
||||
parser.add_option('-d', '--discoverystate', dest='state',
|
||||
help='The discovery state of the entries (discovered, '
|
||||
'identified, and unidentified)')
|
||||
parser.add_option('-s', '--serial', dest='serial',
|
||||
help='Operate against the system matching the specified '
|
||||
'serial number', metavar='SERIAL')
|
||||
@@ -252,19 +333,28 @@ def main():
|
||||
parser.add_option('-i', '--import', dest='importfile',
|
||||
help='Import bulk assignment data from given CSV file',
|
||||
metavar='IMPORT.CSV')
|
||||
parser.add_option('-f', '--fields', dest='fields',
|
||||
help='Select fields for output',
|
||||
metavar='FIELDS')
|
||||
parser.add_option('-o', '--order', dest='order',
|
||||
help='Order output by given field', metavar='ORDER')
|
||||
(options, args) = parser.parse_args()
|
||||
if len(args) == 0 or args[0] not in ('list', 'assign', 'rescan'):
|
||||
if len(args) == 0 or args[0] not in ('list', 'assign', 'reassign', 'rescan', 'clear'):
|
||||
parser.print_help()
|
||||
sys.exit(1)
|
||||
session = client.Command()
|
||||
if args[0] == 'list':
|
||||
list_discovery(options, session)
|
||||
if args[0] == 'clear':
|
||||
clear_discovery(options, session)
|
||||
if args[0] == 'assign':
|
||||
assign_discovery(options, session)
|
||||
if args[0] == 'reassign':
|
||||
assign_discovery(options, session, False)
|
||||
if args[0] == 'rescan':
|
||||
list(session.update('/discovery/rescan', {'rescan': 'start'}))
|
||||
print("Rescan initiated")
|
||||
blocking_scan(session)
|
||||
print("Rescan complete")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
main()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/python
|
||||
#!/usr/bin/python2
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2015-2017 Lenovo
|
||||
# Copyright 2015-2019 Lenovo
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -33,10 +33,15 @@ if path.startswith('/opt'):
|
||||
|
||||
import confluent.client as client
|
||||
|
||||
sys.stdout = codecs.getwriter('utf8')(sys.stdout)
|
||||
if sys.version_info[0] < 3:
|
||||
sys.stdout = codecs.getwriter('utf8')(sys.stdout)
|
||||
|
||||
argparser = optparse.OptionParser(
|
||||
usage="Usage: %prog [options] noderange [clear]")
|
||||
argparser.add_option('-m', '--maxnodes', type='int',
|
||||
help='Specify a maximum number of '
|
||||
'nodes to clear if clearing log, '
|
||||
'prompting if over the threshold')
|
||||
(options, args) = argparser.parse_args()
|
||||
try:
|
||||
noderange = args[0]
|
||||
@@ -45,11 +50,11 @@ except IndexError:
|
||||
sys.exit(1)
|
||||
client.check_globbing(noderange)
|
||||
deletemode = False
|
||||
if len(sys.argv) > 3:
|
||||
if len(args) > 2:
|
||||
argparser.print_help()
|
||||
sys.exit(1)
|
||||
if len(sys.argv) == 3:
|
||||
if sys.argv[2] == 'clear':
|
||||
if len(args) == 2:
|
||||
if args[1] == 'clear':
|
||||
deletemode = True
|
||||
else:
|
||||
argparser.print_help()
|
||||
@@ -64,6 +69,8 @@ def format_event(evt):
|
||||
display = dt.strptime(evt['timestamp'], '%Y-%m-%dT%H:%M:%S')
|
||||
retparts.append(display.strftime('%m/%d/%Y %H:%M:%S'))
|
||||
dscparts = []
|
||||
if evt.get('log_id', None):
|
||||
retparts.append(evt['log_id'] + ':')
|
||||
if 'component_type' in evt and evt['component_type'] is not None:
|
||||
dscparts.append(evt['component_type'])
|
||||
if 'component' in evt and evt['component'] is not None:
|
||||
@@ -77,11 +84,15 @@ def format_event(evt):
|
||||
pass
|
||||
dscparts.append(evttext)
|
||||
retparts.append(' - '.join(dscparts))
|
||||
return ' '.join(retparts)
|
||||
msg = evt.get('message')
|
||||
if not msg:
|
||||
msg = ''
|
||||
return ' '.join(retparts) + msg
|
||||
|
||||
|
||||
if deletemode:
|
||||
func = session.delete
|
||||
session.stop_if_noderange_over(noderange, options.maxnodes)
|
||||
else:
|
||||
func = session.read
|
||||
for rsp in func('/noderange/{0}/events/hardware/log'.format(noderange)):
|
||||
@@ -98,4 +109,4 @@ for rsp in func('/noderange/{0}/events/hardware/log'.format(noderange)):
|
||||
if 'events' in thisdata:
|
||||
evtdata = thisdata['events']
|
||||
for evt in evtdata:
|
||||
print '{0}: {1}'.format(node, format_event(evt))
|
||||
print('{0}: {1}'.format(node, format_event(evt)))
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/python
|
||||
#!/usr/bin/python2
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2016-2017 Lenovo
|
||||
@@ -35,28 +35,16 @@ import confluent.screensqueeze as sq
|
||||
|
||||
exitcode = 0
|
||||
|
||||
def printerror(res, node=None):
|
||||
global exitcode
|
||||
if 'errorcode' in res:
|
||||
exitcode = res['errorcode']
|
||||
if 'error' in res:
|
||||
if node:
|
||||
sys.stderr.write('{0}: {1}\n'.format(node, res['error']))
|
||||
else:
|
||||
sys.stderr.write('{0}\n'.format(res['error']))
|
||||
if 'errorcode' not in res:
|
||||
exitcode = 1
|
||||
|
||||
|
||||
def printfirm(node, prefix, data):
|
||||
if 'model' in data:
|
||||
if 'model' in data and data['model']:
|
||||
prefix += ' ' + data['model']
|
||||
builddesc = []
|
||||
if 'build' in data:
|
||||
if 'build' in data and data['build']:
|
||||
builddesc.append(data['build'])
|
||||
if 'date' in data:
|
||||
if 'date' in data and data['date']:
|
||||
builddesc.append(data['date'])
|
||||
if 'version' in data:
|
||||
if 'version' in data and data['version']:
|
||||
version = data['version']
|
||||
if builddesc:
|
||||
version += ' ({0})'.format(' '.join(builddesc))
|
||||
@@ -64,20 +52,35 @@ def printfirm(node, prefix, data):
|
||||
version = ' '.join(builddesc)
|
||||
print('{0}: {1}: {2}'.format(node, prefix, version))
|
||||
|
||||
components = ['all']
|
||||
|
||||
argparser = optparse.OptionParser(
|
||||
usage="Usage: %prog <noderange> [update [--backup <file>]")
|
||||
usage="Usage: "
|
||||
"%prog <noderange> [list][update [--backup <file>]]|[<components>]")
|
||||
argparser.add_option('-b', '--backup', action='store_true',
|
||||
help='Target a backup bank rather than primary')
|
||||
argparser.add_option('-m', '--maxnodes', type='int',
|
||||
help='When updating, prompt if more than the specified '
|
||||
'number of servers will be affected')
|
||||
|
||||
(options, args) = argparser.parse_args()
|
||||
upfile = None
|
||||
try:
|
||||
noderange = args[0]
|
||||
if len(args) > 1:
|
||||
if args[1] != 'update':
|
||||
argparser.print_help()
|
||||
sys.exit(1)
|
||||
upfile = args[2]
|
||||
if args[1] == 'update':
|
||||
upfile = args[2]
|
||||
else:
|
||||
if args[1] == 'list':
|
||||
comps = args[2:]
|
||||
else:
|
||||
comps = args[1:]
|
||||
components = []
|
||||
for arg in comps:
|
||||
components += arg.split(',')
|
||||
if not components:
|
||||
components = ['all']
|
||||
|
||||
except IndexError:
|
||||
argparser.print_help()
|
||||
sys.exit(1)
|
||||
@@ -96,6 +99,7 @@ def get_update_progress(session, url):
|
||||
|
||||
def update_firmware(session, filename):
|
||||
global exitcode
|
||||
session.stop_if_noderange_over(noderange, options.maxnodes)
|
||||
output = sq.ScreenPrinter(noderange, session)
|
||||
nodeurls = {}
|
||||
filename = os.path.abspath(filename)
|
||||
@@ -135,18 +139,25 @@ def update_firmware(session, filename):
|
||||
sys.stderr.write('{0}: {1}\n'.format(node, noderrs[node]))
|
||||
|
||||
def show_firmware(session):
|
||||
for res in session.read('/noderange/{0}/inventory/firmware/all/all'.format(
|
||||
noderange)):
|
||||
printerror(res)
|
||||
if 'databynode' not in res:
|
||||
continue
|
||||
for node in res['databynode']:
|
||||
printerror(res['databynode'][node], node)
|
||||
if 'firmware' not in res['databynode'][node]:
|
||||
global exitcode
|
||||
firmware_shown = False
|
||||
for component in components:
|
||||
for res in session.read(
|
||||
'/noderange/{0}/inventory/firmware/all/{1}'.format(
|
||||
noderange, component)):
|
||||
exitcode |= client.printerror(res)
|
||||
if 'databynode' not in res:
|
||||
continue
|
||||
for inv in res['databynode'][node]['firmware']:
|
||||
for prefix in inv:
|
||||
printfirm(node, prefix, inv[prefix])
|
||||
for node in res['databynode']:
|
||||
exitcode |= client.printerror(res['databynode'][node], node)
|
||||
if 'firmware' not in res['databynode'][node]:
|
||||
continue
|
||||
for inv in res['databynode'][node]['firmware']:
|
||||
for prefix in inv:
|
||||
firmware_shown = True
|
||||
printfirm(node, prefix, inv[prefix])
|
||||
if not firmware_shown and not exitcode:
|
||||
argparser.print_help()
|
||||
|
||||
|
||||
try:
|
||||
@@ -157,4 +168,4 @@ try:
|
||||
update_firmware(session, upfile)
|
||||
except KeyboardInterrupt:
|
||||
print('')
|
||||
sys.exit(exitcode)
|
||||
sys.exit(exitcode)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python
|
||||
#!/usr/bin/python2
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2017 Lenovo
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
__author__ = 'alin37'
|
||||
|
||||
from getpass import getpass
|
||||
import optparse
|
||||
import os
|
||||
import signal
|
||||
@@ -35,14 +36,20 @@ import confluent.client as client
|
||||
|
||||
argparser = optparse.OptionParser(
|
||||
usage='''\n %prog [options] \
|
||||
\n %prog [options] nodegroup [list of attributes] \
|
||||
\n %prog [options] nodegroup group=value1,value2 \
|
||||
\n %prog [options] nodegroup group=value1,value2
|
||||
\n %prog [options] nodegroup [list of attributes|all] \
|
||||
\n %prog [options] nodegroup nodes=value1,value2 \
|
||||
\n %prog -e nodegroup <attribute names to set> \
|
||||
\n %prog [options] nodegroup nodes=value1,value2
|
||||
\n ''')
|
||||
argparser.add_option('-b', '--blame', action='store_true',
|
||||
help='Show information about how attributes inherited')
|
||||
argparser.add_option('-e', '--environment', action='store_true',
|
||||
help='Set attributes, but from environment variable of '
|
||||
'same name')
|
||||
argparser.add_option('-c', '--clear', action='store_true',
|
||||
help='Clear variables')
|
||||
argparser.add_option('-p', '--prompt', action='store_true',
|
||||
help='Prompt for attribute values interactively')
|
||||
(options, args) = argparser.parse_args()
|
||||
|
||||
|
||||
@@ -68,7 +75,19 @@ if len(args) > 1:
|
||||
print("Can not clear and set at the same time!")
|
||||
argparser.print_help()
|
||||
sys.exit(1)
|
||||
exitcode=client.updateattrib(session,args,nodetype, nodegroups, options)
|
||||
argassign = None
|
||||
if options.prompt:
|
||||
argassign = {}
|
||||
for arg in args[1:]:
|
||||
oneval = 1
|
||||
twoval = 2
|
||||
while oneval != twoval:
|
||||
oneval = getpass('Enter value for {0}: '.format(arg))
|
||||
twoval = getpass('Confirm value for {0}: '.format(arg))
|
||||
if oneval != twoval:
|
||||
print('Values did not match.')
|
||||
argassign[arg] = twoval
|
||||
exitcode=client.updateattrib(session,args,nodetype, nodegroups, options, argassign)
|
||||
try:
|
||||
# setting user output to what the user inputs
|
||||
if args[1] == 'all':
|
||||
@@ -78,7 +97,7 @@ if len(args) > 1:
|
||||
|
||||
requestargs=args[1:]
|
||||
except Exception as e:
|
||||
print str(e)
|
||||
print(str(e))
|
||||
|
||||
if exitcode != 0:
|
||||
sys.exit(exitcode)
|
||||
@@ -104,6 +123,6 @@ else:
|
||||
sys.stderr.write(res['error'] + '\n')
|
||||
exitcode = 1
|
||||
else:
|
||||
print res['item']['href'].replace('/', '')
|
||||
print(res['item']['href'].replace('/', ''))
|
||||
|
||||
sys.exit(exitcode)
|
||||
sys.exit(exitcode)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python
|
||||
#!/usr/bin/python2
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2017 Lenovo
|
||||
@@ -47,7 +47,7 @@ session = client.Command()
|
||||
exitcode = 0
|
||||
attribs = {'name': noderange}
|
||||
for arg in args[1:]:
|
||||
key, val = arg.split('=')
|
||||
key, val = arg.split('=', 1)
|
||||
attribs[key] = val
|
||||
for r in session.create('/nodegroups/', attribs):
|
||||
if 'error' in r:
|
||||
@@ -55,4 +55,4 @@ for r in session.create('/nodegroups/', attribs):
|
||||
exitcode |= 1
|
||||
if 'created' in r:
|
||||
print('{0}: created'.format(r['created']))
|
||||
sys.exit(exitcode)
|
||||
sys.exit(exitcode)
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
#!/usr/bin/python2
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2015-2017 Lenovo
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
__author__ = 'jjohnson2,alin37,andywray'
|
||||
|
||||
import optparse
|
||||
import os
|
||||
import signal
|
||||
import sys
|
||||
|
||||
try:
|
||||
signal.signal(signal.SIGPIPE, signal.SIG_DFL)
|
||||
except AttributeError:
|
||||
pass
|
||||
path = os.path.dirname(os.path.realpath(__file__))
|
||||
path = os.path.realpath(os.path.join(path, '..', 'lib', 'python'))
|
||||
if path.startswith('/opt'):
|
||||
sys.path.append(path)
|
||||
|
||||
import confluent.client as client
|
||||
|
||||
def main():
|
||||
argparser = optparse.OptionParser(
|
||||
usage="Usage: %prog\n")
|
||||
(options, args) = argparser.parse_args()
|
||||
noderange=""
|
||||
nodelist=""
|
||||
nodelist = '/nodegroups/'
|
||||
session = client.Command()
|
||||
exitcode = 0
|
||||
showtype='all'
|
||||
requestargs=args[1:]
|
||||
nodetype='noderange'
|
||||
if len(args) > 0:
|
||||
argparser.print_help()
|
||||
sys.exit(1)
|
||||
else:
|
||||
for res in session.read(nodelist):
|
||||
if 'error' in res:
|
||||
sys.stderr.write(res['error'] + '\n')
|
||||
exitcode = 1
|
||||
else:
|
||||
print(res['item']['href'].replace('/', ''))
|
||||
|
||||
sys.exit(exitcode)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python
|
||||
#!/usr/bin/python2
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2017 Lenovo
|
||||
@@ -49,4 +49,4 @@ for r in session.delete('/nodegroups/{0}'.format(noderange)):
|
||||
exitcode |= 1
|
||||
if 'deleted' in r:
|
||||
print('{0}: deleted'.format(r['deleted']))
|
||||
sys.exit(exitcode)
|
||||
sys.exit(exitcode)
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
#!/usr/bin/python2
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2019 Lenovo
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
__author__ = 'jjohnson2,alin37,andywray'
|
||||
|
||||
import optparse
|
||||
import os
|
||||
import signal
|
||||
import sys
|
||||
|
||||
try:
|
||||
signal.signal(signal.SIGPIPE, signal.SIG_DFL)
|
||||
except AttributeError:
|
||||
pass
|
||||
path = os.path.dirname(os.path.realpath(__file__))
|
||||
path = os.path.realpath(os.path.join(path, '..', 'lib', 'python'))
|
||||
if path.startswith('/opt'):
|
||||
sys.path.append(path)
|
||||
|
||||
import confluent.client as client
|
||||
|
||||
def main():
|
||||
argparser = optparse.OptionParser(
|
||||
usage="Usage: %prog <group> <new group name>\n")
|
||||
(options, args) = argparser.parse_args()
|
||||
noderange=""
|
||||
nodelist=""
|
||||
nodelist = '/nodegroups/'
|
||||
session = client.Command()
|
||||
exitcode = 0
|
||||
requestargs=args[1:]
|
||||
nodetype='noderange'
|
||||
if len(args) != 2:
|
||||
argparser.print_help()
|
||||
sys.exit(1)
|
||||
else:
|
||||
for res in session.update(
|
||||
'/nodegroups/{0}/attributes/rename'.format(args[0]),
|
||||
{'rename': args[1]}):
|
||||
if 'error' in res:
|
||||
sys.stderr.write(res['error'] + '\n')
|
||||
exitcode = 1
|
||||
else:
|
||||
print('{0}: {1}'.format(res['oldname'], res['newname']))
|
||||
|
||||
sys.exit(exitcode)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python
|
||||
#!/usr/bin/python2
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2015-2017 Lenovo
|
||||
@@ -32,7 +32,8 @@ if path.startswith('/opt'):
|
||||
|
||||
import confluent.client as client
|
||||
|
||||
sys.stdout = codecs.getwriter('utf8')(sys.stdout)
|
||||
if sys.version_info[0] < 3:
|
||||
sys.stdout = codecs.getwriter('utf8')(sys.stdout)
|
||||
|
||||
argparser = optparse.OptionParser(usage="Usage: %prog <noderange>")
|
||||
(options, args) = argparser.parse_args()
|
||||
@@ -44,6 +45,13 @@ except IndexError:
|
||||
client.check_globbing(noderange)
|
||||
|
||||
|
||||
codemappings = {
|
||||
'ok': 0,
|
||||
'warning': 1,
|
||||
'critical': 2,
|
||||
'failed': 2,
|
||||
}
|
||||
|
||||
def main():
|
||||
global session, exitcode, healthbynode, healthexplanations, health, node, sensor, explanation
|
||||
session = client.Command()
|
||||
@@ -66,9 +74,11 @@ def main():
|
||||
if 'error' in health[node]:
|
||||
sys.stderr.write('{0}: Error: {1}\n'.format(
|
||||
node, health[node]['error']))
|
||||
exitcode |= 1
|
||||
exitcode = 3
|
||||
if 'health' in health[node]:
|
||||
healthbynode[node] = health[node]['health']['value']
|
||||
if codemappings[healthbynode[node]] > exitcode:
|
||||
exitcode = codemappings[healthbynode[node]]
|
||||
if 'sensors' in health[node]:
|
||||
healthexplanations[node] = []
|
||||
for sensor in health[node]['sensors']:
|
||||
@@ -94,5 +104,5 @@ try:
|
||||
main()
|
||||
except KeyboardInterrupt:
|
||||
print('')
|
||||
sys.exit(0)
|
||||
sys.exit(exitcode)
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python
|
||||
#!/usr/bin/python2
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2015-2017 Lenovo
|
||||
@@ -31,7 +31,7 @@ if path.startswith('/opt'):
|
||||
|
||||
import confluent.client as client
|
||||
|
||||
argparser = optparse.OptionParser(usage="Usage: %prog <noderange> [on|off]")
|
||||
argparser = optparse.OptionParser(usage="Usage: %prog <noderange> [on|off|blink]")
|
||||
(options, args) = argparser.parse_args()
|
||||
try:
|
||||
noderange = args[0]
|
||||
@@ -42,9 +42,6 @@ client.check_globbing(noderange)
|
||||
identifystate = None
|
||||
if len(sys.argv) > 2:
|
||||
identifystate = sys.argv[2]
|
||||
else:
|
||||
argparser.print_help()
|
||||
sys.exit(1)
|
||||
session = client.Command()
|
||||
exitcode = 0
|
||||
sys.exit(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/python
|
||||
#!/usr/bin/python2
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2016-2017 Lenovo
|
||||
@@ -16,6 +16,7 @@
|
||||
# limitations under the License.
|
||||
|
||||
import codecs
|
||||
import json
|
||||
import optparse
|
||||
import os
|
||||
import re
|
||||
@@ -33,7 +34,8 @@ if path.startswith('/opt'):
|
||||
|
||||
import confluent.client as client
|
||||
|
||||
sys.stdout = codecs.getwriter('utf8')(sys.stdout)
|
||||
if sys.version_info[0] < 3:
|
||||
sys.stdout = codecs.getwriter('utf8')(sys.stdout)
|
||||
|
||||
filters = []
|
||||
|
||||
@@ -49,15 +51,17 @@ def print_mem_info(node, prefix, meminfo):
|
||||
memdescfmt = '{0}GB PC'
|
||||
if meminfo['memory_type'] == 'DDR3 SDRAM':
|
||||
memdescfmt += '3-{1} '
|
||||
elif meminfo['memory_type'] == 'DDR4 SDRAM':
|
||||
elif 'DDR4' in meminfo['memory_type']:
|
||||
memdescfmt += '4-{1} '
|
||||
elif meminfo['memory_type'] == 'Unknown':
|
||||
print('{0}: Unrecognized Memory'.format(node))
|
||||
else:
|
||||
print('{0}: {1}: Unrecognized Memory'.format(node, prefix))
|
||||
return
|
||||
if meminfo['ecc']:
|
||||
if meminfo.get('ecc', False):
|
||||
memdescfmt += 'ECC '
|
||||
capacity = meminfo['capacity_mb'] / 1024
|
||||
memdescfmt += meminfo['module_type']
|
||||
modtype = meminfo.get('module_type', None)
|
||||
if modtype:
|
||||
memdescfmt += modtype
|
||||
memdesc = memdescfmt.format(capacity, meminfo['speed'])
|
||||
print('{0}: {1} description: {2}'.format(node, prefix, memdesc))
|
||||
print('{0}: {1} manufacturer: {2}'.format(
|
||||
@@ -65,10 +69,11 @@ def print_mem_info(node, prefix, meminfo):
|
||||
print('{0}: {1} model: {2}'.format(node, prefix, meminfo['model']))
|
||||
print('{0}: {1} serial number: {2}'.format(node, prefix,
|
||||
meminfo['serial']))
|
||||
print('{0}: {1} manufacture date: {2}'.format(node, prefix,
|
||||
meminfo['manufacture_date']))
|
||||
print('{0}: {1} manufacture location: {2}'.format(
|
||||
node, prefix, meminfo['manufacture_location']))
|
||||
if 'manufacture_date' in meminfo:
|
||||
print('{0}: {1} manufacture date: {2}'.format(node, prefix,
|
||||
meminfo['manufacture_date']))
|
||||
print('{0}: {1} manufacture location: {2}'.format(
|
||||
node, prefix, meminfo['manufacture_location']))
|
||||
|
||||
exitcode = 0
|
||||
|
||||
@@ -86,12 +91,14 @@ def printerror(res, node=None):
|
||||
|
||||
|
||||
url = '/noderange/{0}/inventory/hardware/all/all'
|
||||
usedprefixes = set([])
|
||||
|
||||
argparser = optparse.OptionParser(
|
||||
usage="Usage: %prog <noderange> [serial|model|uuid|mac]")
|
||||
argparser.add_option('-j', '--json', action='store_true', help='Output JSON')
|
||||
(options, args) = argparser.parse_args()
|
||||
try:
|
||||
noderange = sys.argv[1]
|
||||
noderange = args[0]
|
||||
except IndexError:
|
||||
argparser.print_help()
|
||||
sys.exit(1)
|
||||
@@ -114,6 +121,8 @@ if len(args) > 1:
|
||||
filters.append(re.compile('mac address'))
|
||||
url = '/noderange/{0}/inventory/hardware/all/all'
|
||||
try:
|
||||
if options.json:
|
||||
databynode = {}
|
||||
session = client.Command()
|
||||
for res in session.read(url.format(noderange)):
|
||||
printerror(res)
|
||||
@@ -125,9 +134,19 @@ try:
|
||||
continue
|
||||
for inv in res['databynode'][node]['inventory']:
|
||||
prefix = inv['name']
|
||||
idx = 2
|
||||
while (node, prefix) in usedprefixes:
|
||||
prefix = '{0} {1}'.format(inv['name'], idx)
|
||||
idx += 1
|
||||
usedprefixes.add((node, prefix))
|
||||
if not inv['present']:
|
||||
if not filters:
|
||||
print '{0}: {1}: Not Present'.format(node, prefix)
|
||||
if options.json:
|
||||
if node not in databynode:
|
||||
databynode[node] = {}
|
||||
databynode[node][prefix] = inv
|
||||
else:
|
||||
print('{0}: {1}: Not Present'.format(node, prefix))
|
||||
continue
|
||||
info = inv['information']
|
||||
info.pop('board_extra', None)
|
||||
@@ -136,6 +155,11 @@ try:
|
||||
info.pop('product_extra', None)
|
||||
if 'memory_type' in info:
|
||||
if not filters:
|
||||
if options.json:
|
||||
if node not in databynode:
|
||||
databynode[node] = {}
|
||||
databynode[node][prefix] = inv
|
||||
continue
|
||||
print_mem_info(node, prefix, info)
|
||||
continue
|
||||
for datum in info:
|
||||
@@ -147,9 +171,17 @@ try:
|
||||
continue
|
||||
if info[datum] is None:
|
||||
continue
|
||||
if options.json:
|
||||
if node not in databynode:
|
||||
databynode[node] = {}
|
||||
databynode[node][prefix] = inv
|
||||
break
|
||||
print(u'{0}: {1} {2}: {3}'.format(node, prefix,
|
||||
pretty(datum),
|
||||
info[datum]))
|
||||
if options.json:
|
||||
print(json.dumps(databynode, sort_keys=True, indent=4,
|
||||
separators=(',', ': ')))
|
||||
except KeyboardInterrupt:
|
||||
print('')
|
||||
sys.exit(exitcode)
|
||||
sys.exit(exitcode)
|
||||
|
||||
Executable
+151
@@ -0,0 +1,151 @@
|
||||
#!/usr/bin/python2
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2019 Lenovo
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import optparse
|
||||
import os
|
||||
import signal
|
||||
import sys
|
||||
import time
|
||||
|
||||
try:
|
||||
signal.signal(signal.SIGPIPE, signal.SIG_DFL)
|
||||
except AttributeError:
|
||||
pass
|
||||
path = os.path.dirname(os.path.realpath(__file__))
|
||||
path = os.path.realpath(os.path.join(path, '..', 'lib', 'python'))
|
||||
if path.startswith('/opt'):
|
||||
sys.path.append(path)
|
||||
|
||||
import confluent.client as client
|
||||
|
||||
exitcode = 0
|
||||
|
||||
argparser = optparse.OptionParser(
|
||||
usage="Usage: "
|
||||
"%prog <noderange> [list][install <file>|save <directory>|delete <name>]")
|
||||
argparser.add_option('-m', '--maxnodes', type='int',
|
||||
help='Specify a maximum number of '
|
||||
'nodes to delete licenses from, '
|
||||
'prompting if over the threshold')
|
||||
(options, args) = argparser.parse_args()
|
||||
upfile = None
|
||||
downdir = None
|
||||
delete = False
|
||||
try:
|
||||
noderange = args[0]
|
||||
if len(args) > 1:
|
||||
if args[1] == 'install':
|
||||
upfile = args[2]
|
||||
elif args[1] == 'save':
|
||||
downdir = args[2]
|
||||
elif args[1] == 'delete':
|
||||
delete = args[2]
|
||||
elif args[1] != 'list':
|
||||
argparser.print_help()
|
||||
sys.exit(1)
|
||||
except IndexError:
|
||||
argparser.print_help()
|
||||
sys.exit(1)
|
||||
client.check_globbing(noderange)
|
||||
|
||||
|
||||
def install_license(session, filename):
|
||||
global exitcode
|
||||
if not os.path.exists(filename):
|
||||
sys.stderr.write('Unable to locate requested file {0}\n'.format(
|
||||
filename))
|
||||
sys.exit(404)
|
||||
resource = '/noderange/{0}/configuration/' \
|
||||
'management_controller/licenses/'.format(noderange)
|
||||
filename = os.path.abspath(filename)
|
||||
instargs = {'filename': filename}
|
||||
for res in session.create(resource, instargs):
|
||||
for node in res.get('databynode', []):
|
||||
if 'error' in res['databynode'][node]:
|
||||
sys.stderr.write('{0}: {1}\n'.format(
|
||||
node, res['databynode'][node]['error']))
|
||||
sys.exit(res['databynode'][node].get('errorcode', 1))
|
||||
show_licenses(session)
|
||||
|
||||
|
||||
def save_licenses(session, dirname):
|
||||
global exitcode
|
||||
resource = '/noderange/{0}/configuration/' \
|
||||
'management_controller/save_licenses'.format(noderange)
|
||||
filename = os.path.abspath(dirname)
|
||||
if not os.path.exists(filename):
|
||||
sys.stderr.write('Unable to locate specified directory {0}\n'.format(
|
||||
filename))
|
||||
sys.exit(404)
|
||||
instargs = {'dirname': filename}
|
||||
for res in session.create(resource, instargs):
|
||||
for node in res.get('databynode', {}):
|
||||
fname = res['databynode'][node].get('filename', None)
|
||||
if fname:
|
||||
print('{0}: Saved license to {1}'.format(node, fname))
|
||||
else:
|
||||
sys.stderr.write('{0}: {1}', node, repr(res['databynode'][node]))
|
||||
|
||||
|
||||
def show_licenses(session):
|
||||
global exitcode
|
||||
for res in session.read(
|
||||
'/noderange/{0}/configuration/management_controller/licenses/'
|
||||
'all'.format(noderange)):
|
||||
for node in res.get('databynode', {}):
|
||||
for license in res['databynode'][node].get('License', []):
|
||||
msg = '{0}: {1}'.format(node, license.get('feature',
|
||||
'Unknown'))
|
||||
if license.get('state', 'Active') != 'Active':
|
||||
msg += ' ({0})'.format(license['state'])
|
||||
print(msg)
|
||||
|
||||
|
||||
def delete_license(session, licname):
|
||||
global exitcode
|
||||
licstodel = []
|
||||
for res in list(session.read(
|
||||
'/noderange/{0}/configuration/management_controller/licenses/'
|
||||
'all'.format(noderange))):
|
||||
for node in res.get('databynode', {}):
|
||||
for license in res['databynode'][node].get('License', []):
|
||||
if license.get('feature', None) == licname:
|
||||
prefix = '/nodes/{0}/configuration/management_controller/licenses/'.format(node)
|
||||
for currlic in list(session.read(prefix)):
|
||||
currlic = currlic.get('item', {}).get('href', 'all')
|
||||
if currlic == 'all':
|
||||
continue
|
||||
currname = list(session.read(prefix + currlic))[0]
|
||||
currname = currname.get('License', [{}])[0].get('feature', None)
|
||||
if currname == licname:
|
||||
list(session.delete(prefix + currlic))
|
||||
show_licenses(session)
|
||||
|
||||
try:
|
||||
session = client.Command()
|
||||
if upfile:
|
||||
install_license(session, upfile)
|
||||
elif downdir:
|
||||
save_licenses(session, downdir)
|
||||
elif delete:
|
||||
session.stop_if_noderange_over(noderange, options.maxnodes)
|
||||
delete_license(session, delete)
|
||||
else:
|
||||
show_licenses(session)
|
||||
except KeyboardInterrupt:
|
||||
print('')
|
||||
sys.exit(exitcode)
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python
|
||||
#!/usr/bin/python2
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2015-2017 Lenovo
|
||||
@@ -61,7 +61,7 @@ def main():
|
||||
sys.stderr.write(res['error'] + '\n')
|
||||
exitcode = 1
|
||||
else:
|
||||
print res['item']['href'].replace('/', '')
|
||||
print(res['item']['href'].replace('/', ''))
|
||||
|
||||
sys.exit(exitcode)
|
||||
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
#!/usr/bin/python2
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2018 Lenovo
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import optparse
|
||||
import os
|
||||
import signal
|
||||
import sys
|
||||
import time
|
||||
|
||||
try:
|
||||
signal.signal(signal.SIGPIPE, signal.SIG_DFL)
|
||||
except AttributeError:
|
||||
pass
|
||||
path = os.path.dirname(os.path.realpath(__file__))
|
||||
path = os.path.realpath(os.path.join(path, '..', 'lib', 'python'))
|
||||
if path.startswith('/opt'):
|
||||
sys.path.append(path)
|
||||
|
||||
import confluent.client as client
|
||||
import confluent.screensqueeze as sq
|
||||
|
||||
exitcode = 0
|
||||
|
||||
|
||||
def get_update_progress(session, url):
|
||||
for res in session.read(url):
|
||||
status = res['phase']
|
||||
percent = res['progress']
|
||||
detail = res['detail']
|
||||
if status == 'error':
|
||||
text = 'error!'
|
||||
else:
|
||||
text = '{0}: {1:3.0f}%'.format(status, percent)
|
||||
return text, status, detail
|
||||
|
||||
|
||||
def printerror(res, node=None):
|
||||
global exitcode
|
||||
if 'errorcode' in res:
|
||||
exitcode = res['errorcode']
|
||||
if 'error' in res:
|
||||
if node:
|
||||
sys.stderr.write('{0}: {1}\n'.format(node, res['error']))
|
||||
else:
|
||||
sys.stderr.write('{0}\n'.format(res['error']))
|
||||
if 'errorcode' not in res:
|
||||
exitcode = 1
|
||||
for node in res.get('databynode', ()):
|
||||
printerror(res['databynode'][node], node)
|
||||
|
||||
|
||||
def attach_media(noderange, media):
|
||||
global exitcode
|
||||
session = client.Command()
|
||||
if ':' not in media:
|
||||
sys.stderr.write('Full URL required for attach\n')
|
||||
sys.exit(1)
|
||||
resource = '/noderange/{0}/media/attach'.format(noderange)
|
||||
for res in session.update(resource, {'url': media}):
|
||||
printerror(res)
|
||||
list_media(noderange, media)
|
||||
|
||||
|
||||
def list_media(noderange, media):
|
||||
session = client.Command()
|
||||
resource = '/noderange/{0}/media/current'.format(noderange)
|
||||
for res in session.read(resource):
|
||||
printerror(res)
|
||||
for node in res.get('databynode', []):
|
||||
url = res['databynode'][node].get('url', None)
|
||||
name = res['databynode'][node].get('name', None)
|
||||
if (url and not url.startswith('file:') and
|
||||
not res['databynode'][node].get('secure', False)):
|
||||
name += ' (insecure)'
|
||||
if not name:
|
||||
continue
|
||||
print('{0}: {1}'.format(node, url + '/' + name if url else name))
|
||||
|
||||
|
||||
def detach_media(noderange, media):
|
||||
global exitcode
|
||||
session = client.Command()
|
||||
resource = '/noderange/{0}/media/detach'.format(noderange)
|
||||
for res in session.update(resource, {'detachall': 1}):
|
||||
printerror(res)
|
||||
|
||||
|
||||
def upload_media(noderange, media):
|
||||
global exitcode
|
||||
if not os.path.exists(media):
|
||||
sys.stderr.write('Unable to locate requested file {0}\n'.format(
|
||||
media))
|
||||
sys.exit(404)
|
||||
session = client.Command()
|
||||
output = sq.ScreenPrinter(noderange, session)
|
||||
filename = os.path.abspath(media)
|
||||
resource = '/noderange/{0}/media/uploads/'.format(noderange)
|
||||
upargs = {'filename': filename}
|
||||
noderrs = {}
|
||||
nodeurls = {}
|
||||
for res in session.create(resource, upargs):
|
||||
if 'created' not in res:
|
||||
for nodename in res.get('databynode', ()):
|
||||
output.set_output(nodename, 'error!')
|
||||
noderrs[nodename] = res['databynode'][nodename].get(
|
||||
'error', 'Unknown Error')
|
||||
continue
|
||||
watchurl = res['created']
|
||||
currnode = watchurl.split('/')[1]
|
||||
nodeurls[currnode] = '/' + watchurl
|
||||
while nodeurls:
|
||||
for node in list(nodeurls):
|
||||
progress, status, err = get_update_progress(
|
||||
session, nodeurls[node])
|
||||
if status == 'error':
|
||||
exitcode = 1
|
||||
noderrs[node] = err
|
||||
if status in ('error', 'complete', 'pending'):
|
||||
list(session.delete(nodeurls[node]))
|
||||
del nodeurls[node]
|
||||
output.set_output(node, progress)
|
||||
time.sleep(2)
|
||||
allerrnodes = ','.join(noderrs)
|
||||
if noderrs:
|
||||
sys.stderr.write(
|
||||
'Nodes had errors receiving media ({0})!\n'.format(allerrnodes))
|
||||
for node in noderrs:
|
||||
sys.stderr.write('{0}: {1}\n'.format(node, noderrs[node]))
|
||||
list_media(noderange, media)
|
||||
|
||||
funmap = {
|
||||
'upload': upload_media,
|
||||
'attach': attach_media,
|
||||
'detachall': detach_media,
|
||||
'list': list_media,
|
||||
}
|
||||
|
||||
|
||||
class OptParser(optparse.OptionParser):
|
||||
|
||||
def format_epilog(self, formatter):
|
||||
return self.expand_prog_name(self.epilog)
|
||||
|
||||
def main():
|
||||
argparser = OptParser(
|
||||
usage="Usage: %prog <noderange> [list|upload|attach|detachall] "
|
||||
"<filename>|all|<url>",
|
||||
epilog='\nupload will take the specified file and upload it to the '
|
||||
'BMC.\n\n'
|
||||
'attach will instruct the BMC to connect a remote media to the '
|
||||
'specified url.\n\ndetachall will remove *ALL* uploaded and '
|
||||
'attached urls from the BMC\n\nlist shows currently mounted '
|
||||
'media.\n\nSee `man %prog` for more info.\n')
|
||||
(options, args) = argparser.parse_args()
|
||||
media = None
|
||||
try:
|
||||
noderange = args[0]
|
||||
operation = args[1]
|
||||
arglength = 2
|
||||
if operation in ('attach', 'upload'):
|
||||
media = args[2]
|
||||
arglength = 3
|
||||
if len(args) > arglength:
|
||||
argparser.print_help()
|
||||
sys.exit(1)
|
||||
except IndexError:
|
||||
argparser.print_help()
|
||||
sys.exit(1)
|
||||
client.check_globbing(noderange)
|
||||
try:
|
||||
handler = funmap[operation]
|
||||
except KeyError:
|
||||
argparser.print_help()
|
||||
sys.exit(1)
|
||||
handler(noderange, media)
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python
|
||||
#!/usr/bin/python2
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2015-2017 Lenovo
|
||||
@@ -34,6 +34,14 @@ import confluent.client as client
|
||||
argparser = optparse.OptionParser(
|
||||
usage="Usage: %prog [options] noderange "
|
||||
"([status|on|off|shutdown|boot|reset])")
|
||||
argparser.add_option('-p', '--showprevious', dest='previous',
|
||||
action='store_true', default=False,
|
||||
help='Show previous power state')
|
||||
argparser.add_option('-m', '--maxnodes', type='int',
|
||||
help='Specify a maximum number of '
|
||||
'nodes to change power state, '
|
||||
'prompting if over the threshold')
|
||||
|
||||
(options, args) = argparser.parse_args()
|
||||
try:
|
||||
noderange = args[0]
|
||||
@@ -42,11 +50,11 @@ except IndexError:
|
||||
sys.exit(1)
|
||||
client.check_globbing(noderange)
|
||||
setstate = None
|
||||
if len(sys.argv) > 2:
|
||||
if len(args) > 1:
|
||||
if setstate == 'softoff':
|
||||
setstate = 'shutdown'
|
||||
elif not sys.argv[2] in ('stat', 'state', 'status'):
|
||||
setstate = sys.argv[2]
|
||||
elif not args[1] in ('stat', 'state', 'status'):
|
||||
setstate = args[1]
|
||||
|
||||
if setstate not in (None, 'on', 'off', 'shutdown', 'boot', 'reset'):
|
||||
argparser.print_help()
|
||||
@@ -54,5 +62,19 @@ if setstate not in (None, 'on', 'off', 'shutdown', 'boot', 'reset'):
|
||||
session = client.Command()
|
||||
exitcode = 0
|
||||
session.add_precede_key('oldstate')
|
||||
sys.exit(
|
||||
session.simple_noderange_command(noderange, '/power/state', setstate))
|
||||
|
||||
if options.previous:
|
||||
# get previous states
|
||||
prev = {}
|
||||
for rsp in session.read("/noderange/{0}/power/state".format(noderange)):
|
||||
# gets previous (current) states
|
||||
|
||||
databynode = rsp["databynode"]
|
||||
|
||||
for node in databynode:
|
||||
prev[node] = databynode[node]["state"]["value"]
|
||||
|
||||
# add dictionary to session
|
||||
session.add_precede_dict(prev)
|
||||
|
||||
sys.exit(session.simple_noderange_command(noderange, '/power/state', setstate, promptover=options.maxnodes))
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python
|
||||
#!/usr/bin/python2
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2017 Lenovo
|
||||
@@ -35,6 +35,10 @@ import confluent.client as client
|
||||
argparser = optparse.OptionParser(
|
||||
usage='''\n %prog noderange
|
||||
\n ''')
|
||||
argparser.add_option('-m', '--maxnodes', type='int',
|
||||
help='Specify a maximum number of '
|
||||
'nodes to delete, '
|
||||
'prompting if over the threshold')
|
||||
(options, args) = argparser.parse_args()
|
||||
if len(args) != 1:
|
||||
argparser.print_help()
|
||||
@@ -43,10 +47,11 @@ noderange = args[0]
|
||||
client.check_globbing(noderange)
|
||||
session = client.Command()
|
||||
exitcode = 0
|
||||
session.stop_if_noderange_over(noderange, options.maxnodes)
|
||||
for r in session.delete('/noderange/{0}'.format(noderange)):
|
||||
if 'error' in r:
|
||||
sys.stderr.write(r['error'] + '\n')
|
||||
exitcode |= 1
|
||||
if 'deleted' in r:
|
||||
print('{0}: deleted'.format(r['deleted']))
|
||||
sys.exit(exitcode)
|
||||
sys.exit(exitcode)
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
#!/usr/bin/python2
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2019 Lenovo
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import optparse
|
||||
import os
|
||||
import signal
|
||||
import sys
|
||||
try:
|
||||
signal.signal(signal.SIGPIPE, signal.SIG_DFL)
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
path = os.path.dirname(os.path.realpath(__file__))
|
||||
path = os.path.realpath(os.path.join(path, '..', 'lib', 'python'))
|
||||
if path.startswith('/opt'):
|
||||
sys.path.append(path)
|
||||
|
||||
import confluent.client as client
|
||||
|
||||
argparser = optparse.OptionParser(usage="Usage: %prog <noderange> <newname>")
|
||||
(options, args) = argparser.parse_args()
|
||||
try:
|
||||
noderange = args[0]
|
||||
except IndexError:
|
||||
argparser.print_help()
|
||||
sys.exit(1)
|
||||
client.check_globbing(noderange)
|
||||
identifystate = None
|
||||
if len(sys.argv) > 2:
|
||||
newname = sys.argv[2]
|
||||
else:
|
||||
argparser.print_help()
|
||||
sys.exit(1)
|
||||
session = client.Command()
|
||||
exitcode = 0
|
||||
sys.exit(
|
||||
session.simple_noderange_command(noderange, 'attributes/rename', newname))
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python
|
||||
#!/usr/bin/python2
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2015-2017 Lenovo
|
||||
@@ -32,6 +32,10 @@ if path.startswith('/opt'):
|
||||
import confluent.client as client
|
||||
|
||||
argparser = optparse.OptionParser(usage="Usage: %prog <noderange>")
|
||||
argparser.add_option('-m', '--maxnodes', type='int',
|
||||
help='Specify a maximum number of '
|
||||
'nodes to reseat, '
|
||||
'prompting if over the threshold')
|
||||
(options, args) = argparser.parse_args()
|
||||
try:
|
||||
noderange = args[0]
|
||||
@@ -43,7 +47,7 @@ session = client.Command()
|
||||
exitcode = 0
|
||||
|
||||
errorNodes = set([])
|
||||
|
||||
session.stop_if_noderange_over(noderange, options.maxnodes)
|
||||
success = session.simple_noderange_command(noderange, 'power/reseat', 'reseat', key='reseat', errnodes=errorNodes) # = 0 if successful
|
||||
|
||||
# Determine which nodes were successful and print them
|
||||
@@ -56,7 +60,7 @@ for node in session.read('/noderange/{0}/nodes/'.format(noderange)):
|
||||
goodNodes = allNodes - errorNodes
|
||||
|
||||
for node in goodNodes:
|
||||
print node + ": Reseat successful"
|
||||
print(node + ": Reseat successful")
|
||||
|
||||
|
||||
sys.exit(success)
|
||||
|
||||
Executable
+159
@@ -0,0 +1,159 @@
|
||||
#!/usr/bin/python2
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2016-2017 Lenovo
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from collections import deque
|
||||
import optparse
|
||||
import os
|
||||
import select
|
||||
import shlex
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
try:
|
||||
signal.signal(signal.SIGPIPE, signal.SIG_DFL)
|
||||
except AttributeError:
|
||||
pass
|
||||
path = os.path.dirname(os.path.realpath(__file__))
|
||||
path = os.path.realpath(os.path.join(path, '..', 'lib', 'python'))
|
||||
if path.startswith('/opt'):
|
||||
sys.path.append(path)
|
||||
|
||||
import confluent.client as client
|
||||
import confluent.screensqueeze as sq
|
||||
import confluent.sortutil as sortutil
|
||||
|
||||
|
||||
def run():
|
||||
argparser = optparse.OptionParser(
|
||||
usage="Usage: %prog location noderange:location",
|
||||
)
|
||||
argparser.add_option('-m', '--maxnodes', type='int',
|
||||
help='Specify a maximum number of '
|
||||
'nodes to run rsync to, '
|
||||
'prompting if over the threshold')
|
||||
argparser.add_option('-f', '-c', '--count', type='int', default=168,
|
||||
help='Number of nodes to concurrently rsync')
|
||||
# among other things, FD_SETSIZE limits. Besides, spawning too many
|
||||
# processes can be unkind for the unaware on memory pressure and such...
|
||||
argparser.disable_interspersed_args()
|
||||
(options, args) = argparser.parse_args()
|
||||
if len(args) < 2 or ':' not in args[-1]:
|
||||
argparser.print_help()
|
||||
sys.exit(1)
|
||||
concurrentprocs = options.count
|
||||
noderange, targpath = args[-1].split(':', 1)
|
||||
client.check_globbing(noderange)
|
||||
c = client.Command()
|
||||
cmdstr = ' '.join(args[:-1])
|
||||
cmdstr = 'rsync -av --info=progress2 ' + cmdstr
|
||||
cmdstr += ' {node}:' + targpath
|
||||
|
||||
currprocs = 0
|
||||
all = set([])
|
||||
pipedesc = {}
|
||||
pendingexecs = deque()
|
||||
exitcode = 0
|
||||
c.stop_if_noderange_over(noderange, options.maxnodes)
|
||||
for exp in c.create('/noderange/{0}/attributes/expression'.format(noderange),
|
||||
{'expression': cmdstr}):
|
||||
if 'error' in exp:
|
||||
sys.stderr.write(exp['error'] + '\n')
|
||||
exitcode |= exp.get('errorcode', 1)
|
||||
ex = exp.get('databynode', ())
|
||||
for node in ex:
|
||||
cmd = ex[node]['value']
|
||||
if not isinstance(cmd, bytes) and not isinstance(cmd, str):
|
||||
cmd = cmd.encode('utf-8')
|
||||
cmdv = shlex.split(cmd)
|
||||
if currprocs < concurrentprocs:
|
||||
currprocs += 1
|
||||
run_cmdv(node, cmdv, all, pipedesc)
|
||||
else:
|
||||
pendingexecs.append((node, cmdv))
|
||||
if not all or exitcode:
|
||||
sys.exit(exitcode)
|
||||
rdy, _, _ = select.select(all, [], [], 10)
|
||||
nodeerrs = {}
|
||||
pernodeout = {}
|
||||
pernodefile = {}
|
||||
output = sq.ScreenPrinter(noderange, c)
|
||||
|
||||
while all:
|
||||
for r in rdy:
|
||||
desc = pipedesc[r]
|
||||
node = desc['node']
|
||||
data = True
|
||||
while data and select.select([r], [], [], 0)[0]:
|
||||
data = r.read(1)
|
||||
if data:
|
||||
if desc['type'] == 'stdout':
|
||||
if node not in pernodeout:
|
||||
pernodeout[node] = ''
|
||||
pernodeout[node] += client.stringify(data)
|
||||
if '\n' in pernodeout[node]:
|
||||
currout, pernodeout[node] = pernodeout[node].split('\n', 1)
|
||||
if currout:
|
||||
pernodefile[node] = os.path.basename(currout)
|
||||
if '\r' in pernodeout[node]:
|
||||
currout, pernodeout[node] = pernodeout[node].split('\r', 1)
|
||||
if currout:
|
||||
currout = currout.split()
|
||||
try:
|
||||
currout = currout[1]
|
||||
output.set_output(node, '{0}:{1}'.format(pernodefile[node], currout))
|
||||
except IndexError:
|
||||
pernodefile = currout[0]
|
||||
pass
|
||||
else:
|
||||
output.set_output(node, 'error!')
|
||||
if node not in nodeerrs:
|
||||
nodeerrs[node] = ''
|
||||
nodeerrs[node] += client.stringify(data)
|
||||
else:
|
||||
pop = desc['popen']
|
||||
ret = pop.poll()
|
||||
if ret is not None:
|
||||
exitcode = exitcode | ret
|
||||
all.discard(r)
|
||||
r.close()
|
||||
if node not in nodeerrs:
|
||||
output.set_output(node, 'complete')
|
||||
if desc['type'] == 'stdout' and pendingexecs:
|
||||
node, cmdv = pendingexecs.popleft()
|
||||
run_cmdv(node, cmdv, all, pipedesc)
|
||||
if all:
|
||||
rdy, _, _ = select.select(all, [], [], 10)
|
||||
for node in nodeerrs:
|
||||
for line in nodeerrs[node].split('\n'):
|
||||
sys.stderr.write('{0}: {1}\n'.format(node, line))
|
||||
sys.exit(exitcode)
|
||||
|
||||
|
||||
def run_cmdv(node, cmdv, all, pipedesc):
|
||||
nopen = subprocess.Popen(
|
||||
cmdv, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
pipedesc[nopen.stdout] = {'node': node, 'popen': nopen,
|
||||
'type': 'stdout'}
|
||||
pipedesc[nopen.stderr] = {'node': node, 'popen': nopen,
|
||||
'type': 'stderr'}
|
||||
all.add(nopen.stdout)
|
||||
all.add(nopen.stderr)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
run()
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python
|
||||
#!/usr/bin/python2
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2016-2017 Lenovo
|
||||
@@ -39,11 +39,17 @@ import confluent.sortutil as sortutil
|
||||
|
||||
def run():
|
||||
argparser = optparse.OptionParser(
|
||||
usage="Usage: %prog noderange commandexpression",
|
||||
usage="Usage: %prog [options] noderange commandexpression",
|
||||
epilog="Expressions are the same as in attributes, e.g. "
|
||||
"'ipmitool -H {hardwaremanagement.manager}' will be expanded.")
|
||||
argparser.add_option('-c', '--count', type='int', default=168,
|
||||
argparser.add_option('-f', '-c', '--count', type='int', default=168,
|
||||
help='Number of commands to run at a time')
|
||||
argparser.add_option('-n', '--nonodeprefix', action='store_true',
|
||||
help='Do not prefix output with node names')
|
||||
argparser.add_option('-m', '--maxnodes', type='int',
|
||||
help='Specify a maximum number of '
|
||||
'nodes to run the command with, '
|
||||
'prompting if over the threshold')
|
||||
# among other things, FD_SETSIZE limits. Besides, spawning too many
|
||||
# processes can be unkind for the unaware on memory pressure and such...
|
||||
argparser.disable_interspersed_args()
|
||||
@@ -61,7 +67,7 @@ def run():
|
||||
pipedesc = {}
|
||||
pendingexecs = deque()
|
||||
exitcode = 0
|
||||
|
||||
c.stop_if_noderange_over(args[0], options.maxnodes)
|
||||
for exp in c.create('/noderange/{0}/attributes/expression'.format(args[0]),
|
||||
{'expression': cmdstr}):
|
||||
if 'error' in exp:
|
||||
@@ -69,7 +75,9 @@ def run():
|
||||
exitcode |= exp.get('errorcode', 1)
|
||||
ex = exp.get('databynode', ())
|
||||
for node in ex:
|
||||
cmd = ex[node]['value'].encode('utf-8')
|
||||
cmd = ex[node]['value']
|
||||
if not isinstance(cmd, bytes) and not isinstance(cmd, str):
|
||||
cmd = cmd.encode('utf-8')
|
||||
cmdv = shlex.split(cmd)
|
||||
if currprocs < concurrentprocs:
|
||||
currprocs += 1
|
||||
@@ -85,7 +93,7 @@ def run():
|
||||
desc = pipedesc[r]
|
||||
node = desc['node']
|
||||
data = True
|
||||
while data and select.select([r], [], [], 0):
|
||||
while data and select.select([r], [], [], 0)[0]:
|
||||
data = r.readline()
|
||||
if data:
|
||||
if desc['type'] == 'stdout':
|
||||
@@ -93,7 +101,11 @@ def run():
|
||||
pernodeout[node] = []
|
||||
pernodeout[node].append(data)
|
||||
else:
|
||||
sys.stderr.write('{0}: {1}'.format(node, data))
|
||||
data = client.stringify(data)
|
||||
if options.nonodeprefix:
|
||||
sys.stderr.write(data)
|
||||
else:
|
||||
sys.stderr.write('{0}: {1}'.format(node, data))
|
||||
sys.stderr.flush()
|
||||
else:
|
||||
pop = desc['popen']
|
||||
@@ -101,12 +113,17 @@ def run():
|
||||
if ret is not None:
|
||||
exitcode = exitcode | ret
|
||||
all.discard(r)
|
||||
r.close()
|
||||
if desc['type'] == 'stdout' and pendingexecs:
|
||||
node, cmdv = pendingexecs.popleft()
|
||||
run_cmdv(node, cmdv, all, pipedesc)
|
||||
for node in sortutil.natural_sort(pernodeout):
|
||||
for line in pernodeout[node]:
|
||||
sys.stdout.write('{0}: {1}'.format(node, line))
|
||||
line = client.stringify(line)
|
||||
if options.nonodeprefix:
|
||||
sys.stdout.write(line)
|
||||
else:
|
||||
sys.stdout.write('{0}: {1}'.format(node, line))
|
||||
sys.stdout.flush()
|
||||
if all:
|
||||
rdy, _, _ = select.select(all, [], [], 10)
|
||||
@@ -125,4 +142,4 @@ def run_cmdv(node, cmdv, all, pipedesc):
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
run()
|
||||
run()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python
|
||||
#!/usr/bin/python2
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2015-2017 Lenovo
|
||||
@@ -114,7 +114,8 @@ def sensorpass(showout=True, appendtime=False):
|
||||
continue
|
||||
for redundant_state in ('Non-Critical', 'Critical'):
|
||||
try:
|
||||
sensedata['states'].remove(redundant_state)
|
||||
if sensedata.get('states', False):
|
||||
sensedata['states'].remove(redundant_state)
|
||||
except ValueError:
|
||||
pass
|
||||
resultdata[node][sensedata['name']] = sensedata
|
||||
@@ -132,11 +133,12 @@ def sensorpass(showout=True, appendtime=False):
|
||||
showval = u' {0} '.format(sensedata['value'])
|
||||
if sensedata['units'] not in (None, u''):
|
||||
showval += sensedata['units']
|
||||
if sensedata['health'] != 'ok':
|
||||
if sensedata.get('health', 'ok') != 'ok':
|
||||
datadescription = [sensedata['health']]
|
||||
else:
|
||||
datadescription = []
|
||||
datadescription.extend(sensedata['states'])
|
||||
if sensedata.get('states', False):
|
||||
datadescription.extend(sensedata['states'])
|
||||
if datadescription:
|
||||
if showval == '':
|
||||
showval += u' {0}'.format(
|
||||
@@ -147,8 +149,11 @@ def sensorpass(showout=True, appendtime=False):
|
||||
if appendtime:
|
||||
showval += ' @' + time.strftime(
|
||||
'%Y-%m-%dT%H:%M:%S')
|
||||
print(u'{0}: {1}:{2}'.format(
|
||||
node, sensedata['name'], showval).encode('utf-8'))
|
||||
printval = u'{0}: {1}:{2}'.format(
|
||||
node, sensedata['name'], showval)
|
||||
if not isinstance(printval, str):
|
||||
printval = printval.encode('utf-8')
|
||||
print(printval)
|
||||
sys.stdout.flush()
|
||||
return resultdata
|
||||
|
||||
@@ -156,7 +161,7 @@ def sensorpass(showout=True, appendtime=False):
|
||||
def format_csv(csvwriter, orderedsensors, resdata, showtime=True):
|
||||
for nodekey in resdata:
|
||||
if showtime:
|
||||
if showtime.is_integer():
|
||||
if isinstance(showtime, int):
|
||||
rowdata = [time.strftime('%Y-%m-%dT%H:%M:%S'), nodekey]
|
||||
else:
|
||||
rowdata = [time.strftime('%Y-%m-%dT%H:%M:%S.') +
|
||||
@@ -183,6 +188,7 @@ def format_csv(csvwriter, orderedsensors, resdata, showtime=True):
|
||||
except KeyError:
|
||||
rowdata.append('N/A')
|
||||
csvwriter.writerow(rowdata)
|
||||
sys.stdout.flush()
|
||||
|
||||
|
||||
def main():
|
||||
@@ -196,7 +202,11 @@ def main():
|
||||
orderedsensors.append(name)
|
||||
orderedsensors.sort()
|
||||
for name in orderedsensors:
|
||||
headernames.append(sensorheaders[name].encode('utf-8'))
|
||||
headername = sensorheaders[name]
|
||||
if (not isinstance(headername, str) and
|
||||
not isinstance(headername, bytes)):
|
||||
headername = headername.encode('utf-8')
|
||||
headernames.append(headername)
|
||||
if options.csv:
|
||||
linebyline = False
|
||||
csvwriter = csv.writer(sys.stdout)
|
||||
@@ -220,7 +230,7 @@ def main():
|
||||
sys.exit(exitcode)
|
||||
sleeptime = nextstart - os.times()[4]
|
||||
if sleeptime > 0:
|
||||
time.sleep(nextstart - os.times()[4])
|
||||
time.sleep(sleeptime)
|
||||
else:
|
||||
if options.csv:
|
||||
format_csv(csvwriter, orderedsensors, resdata, showtime=False)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python
|
||||
#!/usr/bin/python2
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2015 Lenovo
|
||||
@@ -43,7 +43,10 @@ argparser.add_option('-p', '--persist', dest='persist', action='store_true',
|
||||
argparser.add_option('-u', '--uefi', dest='uefi', action='store_true',
|
||||
default=True,
|
||||
help='Request UEFI style boot (rather than BIOS)')
|
||||
|
||||
argparser.add_option('-m', '--maxnodes', type='int',
|
||||
help='Specify a maximum number of '
|
||||
'nodes to modify next boot device, '
|
||||
'prompting if over the threshold')
|
||||
(options, args) = argparser.parse_args()
|
||||
|
||||
try:
|
||||
@@ -53,8 +56,8 @@ except IndexError:
|
||||
argparser.print_help()
|
||||
sys.exit(1)
|
||||
bootdev = None
|
||||
if len(sys.argv) > 2:
|
||||
bootdev = sys.argv[2]
|
||||
if len(args) > 1:
|
||||
bootdev = args[1]
|
||||
if bootdev in ('net', 'pxe'):
|
||||
bootdev = 'network'
|
||||
session = client.Command()
|
||||
@@ -63,6 +66,7 @@ if options.biosmode:
|
||||
bootmode = 'bios'
|
||||
else:
|
||||
bootmode = 'uefi'
|
||||
session.stop_if_noderange_over(noderange, options.maxnodes)
|
||||
sys.exit(session.simple_noderange_command(noderange, '/boot/nextdevice', bootdev,
|
||||
bootmode=bootmode,
|
||||
persistent=options.persist))
|
||||
persistent=options.persist))
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python
|
||||
#!/usr/bin/python2
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2016-2017 Lenovo
|
||||
@@ -39,11 +39,17 @@ import confluent.sortutil as sortutil
|
||||
def run():
|
||||
|
||||
argparser = optparse.OptionParser(
|
||||
usage="Usage: %prog noderange commandexpression",
|
||||
usage="Usage: %prog [options] noderange commandexpression",
|
||||
epilog="Expressions are the same as in attributes, e.g. "
|
||||
"'ipmitool -H {hardwaremanagement.manager}' will be expanded.")
|
||||
argparser.add_option('-c', '--count', type='int', default=168,
|
||||
argparser.add_option('-f', '-c', '--count', type='int', default=168,
|
||||
help='Number of commands to run at a time')
|
||||
argparser.add_option('-n', '--nonodeprefix', action='store_true',
|
||||
help='Do not prefix output with node names')
|
||||
argparser.add_option('-m', '--maxnodes', type='int',
|
||||
help='Specify a maximum number of '
|
||||
'nodes to run remote ssh command to, '
|
||||
'prompting if over the threshold')
|
||||
# among other things, FD_SETSIZE limits. Besides, spawning too many
|
||||
# processes can be unkind for the unaware on memory pressure and such...
|
||||
argparser.disable_interspersed_args()
|
||||
@@ -53,7 +59,7 @@ def run():
|
||||
sys.exit(1)
|
||||
client.check_globbing(args[0])
|
||||
concurrentprocs = options.count
|
||||
c = client.Command()
|
||||
c = client.Command()
|
||||
cmdstr = " ".join(args[1:])
|
||||
|
||||
currprocs = 0
|
||||
@@ -62,7 +68,7 @@ def run():
|
||||
pendingexecs = deque()
|
||||
exitcode = 0
|
||||
|
||||
|
||||
c.stop_if_noderange_over(args[0], options.maxnodes)
|
||||
for exp in c.create('/noderange/{0}/attributes/expression'.format(args[0]),
|
||||
{'expression': cmdstr}):
|
||||
if 'error' in exp:
|
||||
@@ -70,7 +76,9 @@ def run():
|
||||
exitcode |= exp.get('errorcode', 1)
|
||||
ex = exp.get('databynode', ())
|
||||
for node in ex:
|
||||
cmd = ex[node]['value'].encode('utf-8')
|
||||
cmd = ex[node]['value']
|
||||
if not isinstance(cmd, str) and not isinstance(cmd, bytes):
|
||||
cmd = cmd.encode('utf-8')
|
||||
cmdv = ['ssh', node, cmd]
|
||||
if currprocs < concurrentprocs:
|
||||
currprocs += 1
|
||||
@@ -86,7 +94,7 @@ def run():
|
||||
desc = pipedesc[r]
|
||||
node = desc['node']
|
||||
data = True
|
||||
while data and select.select([r], [], [], 0):
|
||||
while data and select.select([r], [], [], 0)[0]:
|
||||
data = r.readline()
|
||||
if data:
|
||||
if desc['type'] == 'stdout':
|
||||
@@ -94,7 +102,11 @@ def run():
|
||||
pernodeout[node] = []
|
||||
pernodeout[node].append(data)
|
||||
else:
|
||||
sys.stderr.write('{0}: {1}'.format(node, data))
|
||||
data = client.stringify(data)
|
||||
if options.nonodeprefix:
|
||||
sys.stderr.write(data)
|
||||
else:
|
||||
sys.stderr.write('{0}: {1}'.format(node, data))
|
||||
sys.stderr.flush()
|
||||
else:
|
||||
pop = desc['popen']
|
||||
@@ -102,12 +114,17 @@ def run():
|
||||
if ret is not None:
|
||||
exitcode = exitcode | ret
|
||||
all.discard(r)
|
||||
r.close()
|
||||
if desc['type'] == 'stdout' and pendingexecs:
|
||||
node, cmdv = pendingexecs.popleft()
|
||||
run_cmdv(node, cmdv, all, pipedesc)
|
||||
for node in sortutil.natural_sort(pernodeout):
|
||||
for line in pernodeout[node]:
|
||||
sys.stdout.write('{0}: {1}'.format(node, line))
|
||||
line = client.stringify(line)
|
||||
if options.nonodeprefix:
|
||||
sys.stdout.write(line)
|
||||
else:
|
||||
sys.stdout.write('{0}: {1}'.format(node, line))
|
||||
sys.stdout.flush()
|
||||
if all:
|
||||
rdy, _, _ = select.select(all, [], [], 10)
|
||||
@@ -126,4 +143,4 @@ def run_cmdv(node, cmdv, all, pipedesc):
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
run()
|
||||
run()
|
||||
|
||||
@@ -0,0 +1,237 @@
|
||||
#!/usr/bin/python2
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2018 Lenovo
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import optparse
|
||||
import os
|
||||
import signal
|
||||
import sys
|
||||
|
||||
try:
|
||||
signal.signal(signal.SIGPIPE, signal.SIG_DFL)
|
||||
except AttributeError:
|
||||
pass
|
||||
path = os.path.dirname(os.path.realpath(__file__))
|
||||
path = os.path.realpath(os.path.join(path, '..', 'lib', 'python'))
|
||||
if path.startswith('/opt'):
|
||||
sys.path.append(path)
|
||||
|
||||
import confluent.client as client
|
||||
exitcode = 0
|
||||
|
||||
class OptParser(optparse.OptionParser):
|
||||
|
||||
def format_epilog(self, formatter):
|
||||
return self.expand_prog_name(self.epilog)
|
||||
|
||||
def mbtohuman(mb):
|
||||
if mb > 1000000:
|
||||
return '{0:.3f} TB'.format(mb/1000000.0)
|
||||
if mb > 1000:
|
||||
return '{0:.3f} GB'.format(mb/1000.0)
|
||||
return '{0:.3f} MB'.format(mb)
|
||||
|
||||
def showstorage(noderange, options, args):
|
||||
global exitcode
|
||||
session = client.Command()
|
||||
disks = {}
|
||||
arrays = {}
|
||||
volumes = {}
|
||||
scfg = session.read('/noderange/{0}/configuration/storage/all'.format(
|
||||
noderange))
|
||||
_print_cfg(scfg)
|
||||
|
||||
|
||||
def _print_cfg(scfg):
|
||||
global exitcode
|
||||
storagebynode = {}
|
||||
for e in scfg:
|
||||
if 'error' in e:
|
||||
sys.stderr.write(e['error'] + '\n')
|
||||
exitcode = e.get('errorcode', 1)
|
||||
for node in e.get('databynode', {}):
|
||||
curr = e['databynode'][node]
|
||||
if 'error' in curr:
|
||||
if 'no available drives' in curr['error']:
|
||||
curr['error'] += ' (drives must be in unconfigured state to be available, they must not be in jbod or online state)'
|
||||
sys.stderr.write('{0}: {1}\n'.format(node, curr['error']))
|
||||
exitcode = curr.get('errorcode', 1)
|
||||
continue
|
||||
if node not in storagebynode:
|
||||
storagebynode[node] = {'disks': [], 'arrays': [],
|
||||
'volumes': []}
|
||||
storagebynode[node][curr['type'] + 's'].append(curr)
|
||||
for node in storagebynode:
|
||||
for disk in sorted(storagebynode[node]['disks'],
|
||||
key=lambda x: x['name']):
|
||||
print('{0}: Disk {1} Description: {2}'.format(
|
||||
node, disk['name'], disk['description']))
|
||||
print('{0}: Disk {1} State: {2}'.format(node, disk['name'],
|
||||
disk['state']))
|
||||
print('{0}: Disk {1} FRU: {2}'.format(node, disk['name'],
|
||||
disk['fru']))
|
||||
print('{0}: Disk {1} Serial Number: {2}'.format(node, disk['name'],
|
||||
disk['serial']))
|
||||
if disk['array']:
|
||||
print('{0}: Disk {1} Array: {2}'.format(node, disk['name'],
|
||||
disk['array']))
|
||||
for arr in storagebynode[node]['arrays']:
|
||||
print('{0}: Array {1} Available Capacity: {2}'.format(
|
||||
node, arr['id'], mbtohuman(arr['available'])))
|
||||
print('{0}: Array {1} Total Capacity: {2}'.format(
|
||||
node, arr['id'], mbtohuman(arr['capacity'])))
|
||||
print('{0}: Array {1} RAID: {2}'.format(node, arr['id'],
|
||||
arr['raid']))
|
||||
print('{0}: Array {1} Disks: {2}'.format(node, arr['id'], ','.join(
|
||||
arr['disks'])))
|
||||
print(
|
||||
'{0}: Array {1} Volumes: {2}'.format(node, arr['id'], ','.join(
|
||||
arr['volumes'])))
|
||||
for vol in storagebynode[node]['volumes']:
|
||||
print('{0}: Volume {1}: Size: {2}'.format(node, vol['name'],
|
||||
mbtohuman(vol['size'])))
|
||||
print('{0}: Volume {1}: State: {2}'.format(node, vol['name'],
|
||||
vol['state']))
|
||||
print('{0}: Volume {1}: Array {2}'.format(node, vol['name'],
|
||||
vol['array']))
|
||||
|
||||
|
||||
def createstorage(noderange, options, args):
|
||||
if options.raidlevel is None or options.disks is None:
|
||||
sys.stderr.write('-r and -d are required arguments to create array\n')
|
||||
sys.exit(1)
|
||||
session = client.Command()
|
||||
session.stop_if_noderange_over(noderange, options.maxnodes)
|
||||
names = options.name
|
||||
if names is None:
|
||||
names = ''.join(args)
|
||||
parms = {'disks': options.disks, 'raidlevel': options.raidlevel,
|
||||
'name': names}
|
||||
if options.size:
|
||||
parms['size'] = options.size
|
||||
if options.stripsizes:
|
||||
parms['stripsizes'] = options.stripsizes
|
||||
_print_cfg(session.create(
|
||||
'/noderange/{0}/configuration/storage/volumes/{1}'.format(
|
||||
noderange, names), parms))
|
||||
|
||||
|
||||
def deletestorage(noderange, options, args):
|
||||
if options.name is None:
|
||||
if len(args) == 1:
|
||||
names = args[0]
|
||||
else:
|
||||
sys.stderr.write('-n is required to indicate volume(s) to delete\n')
|
||||
sys.exit(1)
|
||||
else:
|
||||
names = options.name
|
||||
session = client.Command()
|
||||
session.stop_if_noderange_over(noderange, options.maxnodes)
|
||||
for rsp in session.delete(
|
||||
'/noderange/{0}/configuration/storage/volumes/{1}'.format(
|
||||
noderange, names)):
|
||||
if 'deleted' in rsp:
|
||||
print('Deleted: {0}'.format(rsp['deleted']))
|
||||
elif 'databynode' in rsp:
|
||||
for node in rsp['databynode']:
|
||||
if 'error' in rsp['databynode'][node]:
|
||||
sys.stderr.write('{0}: {1}\n'.format(
|
||||
node, rsp['databynode'][node]['error']))
|
||||
else:
|
||||
sys.stderr.write('{0}: {1}\n'.format(
|
||||
node, repr(rsp['databynode'][node])))
|
||||
else:
|
||||
print(repr(rsp))
|
||||
|
||||
|
||||
def setdisk(noderange, options, args):
|
||||
if options.disks is None:
|
||||
if len(args):
|
||||
names = args.pop(0)
|
||||
else:
|
||||
sys.stderr.write('-d is required to indicate disk to modify\n')
|
||||
sys.exit(1)
|
||||
else:
|
||||
names = options.disks
|
||||
if not len(args) or args[0] not in ('hotspare', 'jbod', 'unconfigured'):
|
||||
sys.stderr.write('diskset requires valid state as argument (hotspare, jbod, unconfigured)\n')
|
||||
sys.exit(1)
|
||||
session = client.Command()
|
||||
session.stop_if_noderange_over(noderange, options.maxnodes)
|
||||
scfg = session.update('/noderange/{0}/configuration/storage/disks/{1}'.format(noderange, names), {'state': args[0]})
|
||||
_print_cfg(scfg)
|
||||
|
||||
funmap = {
|
||||
'create': createstorage,
|
||||
'show': showstorage,
|
||||
'diskset': setdisk,
|
||||
'delete': deletestorage,
|
||||
'rm': deletestorage,
|
||||
}
|
||||
|
||||
|
||||
|
||||
def main():
|
||||
argparser = OptParser(
|
||||
usage='Usage: %prog <noderange> [show|create|delete|diskset]',
|
||||
epilog='',
|
||||
)
|
||||
argparser.add_option('-r', '--raidlevel', type='int',
|
||||
help='RAID level to use when creating an array')
|
||||
argparser.add_option('-d', '--disks', type='str',
|
||||
help='Comma separated list of disks to use, or the '
|
||||
'word "rest" to indicate use of all available '
|
||||
'disks')
|
||||
argparser.add_option('-s', '--size', type='str',
|
||||
help='Comma separated list of sizes to use when '
|
||||
'creating volumes. The sizes may be absolute '
|
||||
'size (e.g. 16gb), percentage (10%) or the word '
|
||||
'"rest" to use remaining capacity, default '
|
||||
'behavior is to use all capacity to make a '
|
||||
'volume')
|
||||
argparser.add_option('-n', '--name', type='str',
|
||||
help='Comma separated list of names to use when '
|
||||
'naming volumes, or selecting a volume for '
|
||||
'delete. Default behavior is to use '
|
||||
'implementation provided default names.')
|
||||
argparser.add_option('-z', '--stripsizes', type='str',
|
||||
help='Comma separated list of stripsizes to use when creating volumes. '
|
||||
'This value is in kilobytes. The default behavior is to allow the '
|
||||
'storage controller to decide.')
|
||||
argparser.add_option('-m', '--maxnodes', type='int',
|
||||
help='Specify a maximum number of '
|
||||
'nodes to configure storage on, '
|
||||
'prompting if over the threshold')
|
||||
(options, args) = argparser.parse_args()
|
||||
if len(args) == 1:
|
||||
args.append('show')
|
||||
try:
|
||||
noderange = args[0]
|
||||
operation = args[1]
|
||||
except IndexError:
|
||||
argparser.print_help()
|
||||
sys.exit(1)
|
||||
client.check_globbing(noderange)
|
||||
try:
|
||||
handler = funmap[operation]
|
||||
except KeyError:
|
||||
argparser.print_help()
|
||||
sys.exit(1)
|
||||
handler(noderange, options, args[2:])
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,152 @@
|
||||
#!/usr/bin/python2
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2018 Lenovo
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import optparse
|
||||
import os
|
||||
import signal
|
||||
import sys
|
||||
import time
|
||||
|
||||
try:
|
||||
signal.signal(signal.SIGPIPE, signal.SIG_DFL)
|
||||
except AttributeError:
|
||||
pass
|
||||
path = os.path.dirname(os.path.realpath(__file__))
|
||||
path = os.path.realpath(os.path.join(path, '..', 'lib', 'python'))
|
||||
if path.startswith('/opt'):
|
||||
sys.path.append(path)
|
||||
|
||||
import confluent.client as client
|
||||
import confluent.screensqueeze as sq
|
||||
|
||||
exitcode = 0
|
||||
|
||||
|
||||
def get_update_progress(session, url):
|
||||
for res in session.read(url):
|
||||
status = res['phase']
|
||||
percent = res['progress']
|
||||
detail = res['detail']
|
||||
if status == 'error':
|
||||
text = 'error!'
|
||||
else:
|
||||
text = '{0}: {1:3.0f}%'.format(status, percent)
|
||||
return text, status, detail
|
||||
|
||||
|
||||
def printerror(res, node=None):
|
||||
global exitcode
|
||||
if 'errorcode' in res:
|
||||
exitcode = res['errorcode']
|
||||
if 'error' in res:
|
||||
if node:
|
||||
sys.stderr.write('{0}: {1}\n'.format(node, res['error']))
|
||||
else:
|
||||
sys.stderr.write('{0}\n'.format(res['error']))
|
||||
if 'errorcode' not in res:
|
||||
exitcode = 1
|
||||
for node in res.get('databynode', ()):
|
||||
printerror(res['databynode'][node], node)
|
||||
|
||||
|
||||
|
||||
def download_servicedata(noderange, media, options):
|
||||
global exitcode
|
||||
session = client.Command()
|
||||
output = sq.ScreenPrinter(noderange, session)
|
||||
filename = os.path.abspath(media)
|
||||
resource = '/noderange/{0}/support/servicedata/'.format(noderange)
|
||||
upargs = {'filename': filename}
|
||||
noderrs = {}
|
||||
nodeurls = {}
|
||||
session.stop_if_noderange_over(noderange, options.maxnodes)
|
||||
for res in session.create(resource, upargs):
|
||||
if 'created' not in res:
|
||||
for nodename in res.get('databynode', ()):
|
||||
output.set_output(nodename, 'error!')
|
||||
noderrs[nodename] = res['databynode'][nodename].get(
|
||||
'error', 'Unknown Error')
|
||||
continue
|
||||
watchurl = res['created']
|
||||
currnode = watchurl.split('/')[1]
|
||||
nodeurls[currnode] = '/' + watchurl
|
||||
while nodeurls:
|
||||
for node in list(nodeurls):
|
||||
progress, status, err = get_update_progress(
|
||||
session, nodeurls[node])
|
||||
if status == 'error':
|
||||
exitcode = 1
|
||||
noderrs[node] = err
|
||||
if status in ('error', 'complete', 'pending'):
|
||||
list(session.delete(nodeurls[node]))
|
||||
del nodeurls[node]
|
||||
output.set_output(node, progress)
|
||||
time.sleep(2)
|
||||
allerrnodes = ','.join(noderrs)
|
||||
if noderrs:
|
||||
sys.stderr.write(
|
||||
'Nodes had errors retrieving service data ({0})!\n'.format(allerrnodes))
|
||||
for node in noderrs:
|
||||
sys.stderr.write('{0}: {1}\n'.format(node, noderrs[node]))
|
||||
|
||||
funmap = {
|
||||
'servicedata': download_servicedata,
|
||||
}
|
||||
|
||||
|
||||
class OptParser(optparse.OptionParser):
|
||||
|
||||
def format_epilog(self, formatter):
|
||||
return self.expand_prog_name(self.epilog)
|
||||
|
||||
def main():
|
||||
argparser = OptParser(
|
||||
usage="Usage: %prog <noderange> servicedata "
|
||||
"<filename>",
|
||||
epilog='\nservicedata will save service data to the given '
|
||||
'directory. It is saved to the location on the relevant '
|
||||
'management server (the confluent server if running remote, '
|
||||
'and the collective.manager if in collective)\n'
|
||||
'\n\nSee `man %prog` for more info.\n')
|
||||
argparser.add_option('-m', '--maxnodes', type='int',
|
||||
help='Specify a maximum number of '
|
||||
'nodes to download diagnostic data from, '
|
||||
'prompting if over the threshold')
|
||||
(options, args) = argparser.parse_args()
|
||||
media = None
|
||||
try:
|
||||
noderange = args[0]
|
||||
operation = args[1]
|
||||
arglength = 2
|
||||
if operation == 'servicedata':
|
||||
media = args[2]
|
||||
arglength = 3
|
||||
if len(args) > arglength:
|
||||
argparser.print_help()
|
||||
sys.exit(1)
|
||||
except IndexError:
|
||||
argparser.print_help()
|
||||
sys.exit(1)
|
||||
client.check_globbing(noderange)
|
||||
try:
|
||||
handler = funmap[operation]
|
||||
except KeyError:
|
||||
argparser.print_help()
|
||||
sys.exit(1)
|
||||
handler(noderange, media, options)
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Executable
+167
@@ -0,0 +1,167 @@
|
||||
#!/usr/bin/python2
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2019 Lenovo
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import argparse
|
||||
import csv
|
||||
import fcntl
|
||||
import io
|
||||
import numpy as np
|
||||
|
||||
import os
|
||||
import sixel
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
class DumbWriter(sixel.SixelWriter):
|
||||
def restore_position(self, output):
|
||||
return
|
||||
|
||||
|
||||
def plot(gui, output, plotdata, bins):
|
||||
import matplotlib as mpl
|
||||
if not gui:
|
||||
mpl.use('Agg')
|
||||
import matplotlib.pyplot as plt
|
||||
n, bins, patches = plt.hist(plotdata, bins)
|
||||
plt.show()
|
||||
if not gui:
|
||||
if output:
|
||||
tdata = output
|
||||
else:
|
||||
tdata = io.BytesIO()
|
||||
plt.savefig(tdata)
|
||||
if not gui and not output:
|
||||
writer = DumbWriter()
|
||||
writer.draw(tdata)
|
||||
return n, bins
|
||||
|
||||
def textplot(plotdata, bins):
|
||||
n, bins = np.histogram(plotdata, bins)
|
||||
labels = []
|
||||
for bin in bins:
|
||||
labels.append('{0:0.1f}'.format(bin))
|
||||
width = 80
|
||||
# Since this will be primarily piped into, hard to get
|
||||
# terminal width
|
||||
labelwidth = 0
|
||||
for lab in labels:
|
||||
if len(lab) > labelwidth:
|
||||
labelwidth = len(lab)
|
||||
width -= (labelwidth) + 1
|
||||
labelfmt = '{{0:>{0}s}}|'.format(labelwidth)
|
||||
maxn = 0.0
|
||||
for lgth in n:
|
||||
if lgth > maxn:
|
||||
maxn = float(lgth)
|
||||
for i in range(len(n)):
|
||||
print(labelfmt.format(labels[i]) + '=' * int(np.round((n[i]/maxn) * width)))
|
||||
return n, bins
|
||||
|
||||
histogram = False
|
||||
aparser = argparse.ArgumentParser(description='Quick access to common statistics')
|
||||
aparser.add_argument('-c', type=int, default=0, help='Column number to analyze (default is last column)')
|
||||
aparser.add_argument('-d', default=None, help='Value used to separate columns')
|
||||
aparser.add_argument('-x', default=False, action='store_true', help='Output histogram in sixel format')
|
||||
aparser.add_argument('-s', default=0, help='Number of header lines to skip before processing')
|
||||
aparser.add_argument('-g', default=False, action='store_true', help='Open histogram in separate graphical window')
|
||||
aparser.add_argument('-o', default=None, help='Output histogram to the specified filename in PNG format')
|
||||
aparser.add_argument('-t', default=False, action='store_true', help='Output a histogram in text format')
|
||||
aparser.add_argument('-v', default=False, action='store_true', help='Attempt to list nodes relevant to each histogram bar (requires -s, -o, or -t)')
|
||||
aparser.add_argument('-b', type=int, default=10, help='Number of bins to use in histogram (default is 10)')
|
||||
args = aparser.parse_args(sys.argv[1:])
|
||||
plotdata = []
|
||||
headlines = int(args.s)
|
||||
while headlines >= 0:
|
||||
data = sys.stdin.readline()
|
||||
headlines -= 1
|
||||
if args.d:
|
||||
delimiter = args.d
|
||||
else:
|
||||
if '\t' in data:
|
||||
delimiter = '\t'
|
||||
elif ' ' in data:
|
||||
delimiter = ' '
|
||||
elif ',' in data:
|
||||
delimiter = ','
|
||||
else:
|
||||
delimiter = ' ' # handle single column
|
||||
data = list(csv.reader([data], delimiter=delimiter))[0]
|
||||
nodebydatum = {}
|
||||
idx = args.c - 1
|
||||
autoidx = False
|
||||
while data:
|
||||
node = None
|
||||
if ':' in data[0]:
|
||||
node, data[0] = data[0].split(':', 1)
|
||||
else:
|
||||
node = data[0]
|
||||
if idx == -1 and not autoidx:
|
||||
while not autoidx:
|
||||
try:
|
||||
datum = float(data[idx])
|
||||
except ValueError:
|
||||
idx -= 1
|
||||
continue
|
||||
except IndexError:
|
||||
sys.stderr.write('Unable to identify a numerical column\n')
|
||||
sys.exit(1)
|
||||
autoidx = True
|
||||
else:
|
||||
datum = float(data[idx])
|
||||
if node:
|
||||
if datum in nodebydatum:
|
||||
nodebydatum[datum].add(node)
|
||||
else:
|
||||
nodebydatum[datum] = set([node])
|
||||
plotdata.append(datum)
|
||||
data = sys.stdin.readline()
|
||||
data = list(csv.reader([data], delimiter=delimiter))[0]
|
||||
n = None
|
||||
if args.g or args.o or args.x:
|
||||
n, bins = plot(args.g, args.o, plotdata, bins=args.b)
|
||||
if args.t:
|
||||
n, bins = textplot(plotdata, bins=args.b)
|
||||
print('Samples: {5} Min: {3} Median: {0} Mean: {1} Max: {4} StandardDeviation: {2} Sum: {6}'.format(np.median(plotdata), np.mean(plotdata), np.std(plotdata), np.min(plotdata), np.max(plotdata), len(plotdata), np.sum(plotdata)))
|
||||
if args.v and n is not None and nodebydatum:
|
||||
print('')
|
||||
currbin = bins[0]
|
||||
bins = bins[1:]
|
||||
currbinmembers = []
|
||||
for datum in sorted(nodebydatum):
|
||||
if datum > bins[0]:
|
||||
nextbin = None
|
||||
endbin = bins[0]
|
||||
while len(bins) and bins[0] < datum:
|
||||
nextbin = bins[0]
|
||||
bins = bins[1:]
|
||||
if not nextbin:
|
||||
nextbin = np.max(plotdata)
|
||||
print('Entries between {0} and {1}'.format(currbin, endbin))
|
||||
currbin = nextbin
|
||||
print('-' * 80)
|
||||
print(','.join(sorted(currbinmembers)))
|
||||
print('')
|
||||
print('')
|
||||
currbinmembers = []
|
||||
for node in nodebydatum[datum]:
|
||||
currbinmembers.append(node)
|
||||
if currbinmembers:
|
||||
print('Entries between {0} and {1}'.format(currbin, np.max(plotdata)))
|
||||
print('-' * 80)
|
||||
print(','.join(sorted(currbinmembers)))
|
||||
print('')
|
||||
print('')
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../confluent_server/builddeb
|
||||
@@ -1,7 +1,7 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2014 IBM Corporation
|
||||
# Copyright 2015-2016 Lenovo
|
||||
# Copyright 2015-2019 Lenovo
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -15,8 +15,13 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import anydbm as dbm
|
||||
try:
|
||||
import anydbm as dbm
|
||||
except ImportError:
|
||||
import dbm
|
||||
import csv
|
||||
import errno
|
||||
import fnmatch
|
||||
import hashlib
|
||||
import os
|
||||
import shlex
|
||||
@@ -24,6 +29,7 @@ import socket
|
||||
import ssl
|
||||
import sys
|
||||
import confluent.tlvdata as tlvdata
|
||||
import confluent.sortutil as sortutil
|
||||
|
||||
SO_PASSCRED = 16
|
||||
|
||||
@@ -33,8 +39,91 @@ _attraliases = {
|
||||
'bmcpass': 'secret.hardwaremanagementpassword',
|
||||
}
|
||||
|
||||
try:
|
||||
input = raw_input
|
||||
except NameError:
|
||||
pass
|
||||
|
||||
def stringify(instr):
|
||||
# Normalize unicode and bytes to 'str', correcting for
|
||||
# current python version
|
||||
if isinstance(instr, bytes) and not isinstance(instr, str):
|
||||
return instr.decode('utf-8')
|
||||
elif not isinstance(instr, bytes) and not isinstance(instr, str):
|
||||
return instr.encode('utf-8')
|
||||
return instr
|
||||
|
||||
class Tabulator(object):
|
||||
def __init__(self, headers):
|
||||
self.headers = headers
|
||||
self.rows = []
|
||||
|
||||
def add_row(self, row):
|
||||
self.rows.append(row)
|
||||
|
||||
def get_table(self, order=None):
|
||||
i = 0
|
||||
fmtstr = ''
|
||||
separator = []
|
||||
for head in self.headers:
|
||||
if order and order == head:
|
||||
order = i
|
||||
neededlen = len(head)
|
||||
for row in self.rows:
|
||||
if len(row[i]) > neededlen:
|
||||
neededlen = len(row[i])
|
||||
separator.append('-' * (neededlen + 1))
|
||||
fmtstr += '{{{0}:>{1}}}|'.format(i, neededlen + 1)
|
||||
i = i + 1
|
||||
fmtstr = fmtstr[:-1]
|
||||
yield fmtstr.format(*self.headers)
|
||||
yield fmtstr.format(*separator)
|
||||
if order is not None:
|
||||
for row in sorted(
|
||||
self.rows,
|
||||
key=lambda x: sortutil.naturalize_string(x[order])):
|
||||
yield fmtstr.format(*row)
|
||||
else:
|
||||
for row in self.rows:
|
||||
yield fmtstr.format(*row)
|
||||
|
||||
def write_csv(self, output, order=None):
|
||||
output = csv.writer(output)
|
||||
output.writerow(self.headers)
|
||||
i = 0
|
||||
for head in self.headers:
|
||||
if order and order == head:
|
||||
order = i
|
||||
i = i + 1
|
||||
if order is not None:
|
||||
for row in sorted(
|
||||
self.rows,
|
||||
key=lambda x: sortutil.naturalize_string(x[order])):
|
||||
output.writerow(row)
|
||||
else:
|
||||
for row in self.rows:
|
||||
output.writerow(row)
|
||||
|
||||
|
||||
def printerror(res, node=None):
|
||||
exitcode = 0
|
||||
if 'errorcode' in res:
|
||||
exitcode = res['errorcode']
|
||||
if 'error' in res:
|
||||
if node:
|
||||
sys.stderr.write('{0}: {1}\n'.format(node, res['error']))
|
||||
else:
|
||||
sys.stderr.write('{0}\n'.format(res['error']))
|
||||
if 'errorcode' not in res:
|
||||
exitcode = 1
|
||||
return exitcode
|
||||
|
||||
|
||||
def cprint(txt):
|
||||
print(txt)
|
||||
try:
|
||||
print(txt)
|
||||
except UnicodeEncodeError:
|
||||
print(txt.encode('utf8'))
|
||||
sys.stdout.flush()
|
||||
|
||||
def _parseserver(string):
|
||||
@@ -53,6 +142,7 @@ def _parseserver(string):
|
||||
|
||||
class Command(object):
|
||||
def __init__(self, server=None):
|
||||
self._prevdict = None
|
||||
self._prevkeyname = None
|
||||
self.connection = None
|
||||
self._currnoderange = None
|
||||
@@ -65,6 +155,8 @@ class Command(object):
|
||||
self.serverloc = server
|
||||
if os.path.isabs(self.serverloc) and os.path.exists(self.serverloc):
|
||||
self._connect_unix()
|
||||
elif self.serverloc == '/var/run/confluent/api.sock':
|
||||
raise Exception('Confluent service is not available')
|
||||
else:
|
||||
self._connect_tls()
|
||||
tlvdata.recv(self.connection)
|
||||
@@ -88,6 +180,9 @@ class Command(object):
|
||||
def add_precede_key(self, keyname):
|
||||
self._prevkeyname = keyname
|
||||
|
||||
def add_precede_dict(self, dict):
|
||||
self._prevdict = dict
|
||||
|
||||
def handle_results(self, ikey, rc, res, errnodes=None):
|
||||
if 'error' in res:
|
||||
if errnodes is not None:
|
||||
@@ -120,12 +215,15 @@ class Command(object):
|
||||
if self._prevkeyname and self._prevkeyname in res[node]:
|
||||
cprint('{0}: {2}->{1}'.format(
|
||||
node, val, res[node][self._prevkeyname]['value']))
|
||||
elif self._prevdict and node in self._prevdict:
|
||||
cprint('{0}: {2}->{1}'.format(
|
||||
node, val, self._prevdict[node]))
|
||||
else:
|
||||
cprint('{0}: {1}'.format(node, val))
|
||||
return rc
|
||||
|
||||
def simple_noderange_command(self, noderange, resource, input=None,
|
||||
key=None, errnodes=None, **kwargs):
|
||||
key=None, errnodes=None, promptover=None, **kwargs):
|
||||
try:
|
||||
self._currnoderange = noderange
|
||||
rc = 0
|
||||
@@ -141,6 +239,7 @@ class Command(object):
|
||||
noderange, resource)):
|
||||
rc = self.handle_results(ikey, rc, res, errnodes)
|
||||
else:
|
||||
self.stop_if_noderange_over(noderange, promptover)
|
||||
kwargs[ikey] = input
|
||||
for res in self.update('/noderange/{0}/{1}'.format(
|
||||
noderange, resource), kwargs):
|
||||
@@ -150,6 +249,33 @@ class Command(object):
|
||||
except KeyboardInterrupt:
|
||||
cprint('')
|
||||
return 0
|
||||
|
||||
def stop_if_noderange_over(self, noderange, maxnodes):
|
||||
if maxnodes is None:
|
||||
return
|
||||
nsize = self.get_noderange_size(noderange)
|
||||
if nsize > maxnodes:
|
||||
if nsize == 1:
|
||||
nodename = list(self.read(
|
||||
'/noderange/{0}/nodes/'.format(noderange)))[0].get('item', {}).get('href', None)
|
||||
nodename = nodename[:-1]
|
||||
p = input('Command is about to affect node {0}, continue (y/n)? '.format(nodename))
|
||||
else:
|
||||
p = input('Command is about to affect {0} nodes, continue (y/n)? '.format(nsize))
|
||||
if p.lower() != 'y':
|
||||
sys.stderr.write('Aborting at user request\n')
|
||||
sys.exit(1)
|
||||
raise Exception("Aborting at user request")
|
||||
|
||||
|
||||
def get_noderange_size(self, noderange):
|
||||
numnodes = 0
|
||||
for node in self.read('/noderange/{0}/nodes/'.format(noderange)):
|
||||
if node.get('item', {}).get('href', None):
|
||||
numnodes += 1
|
||||
else:
|
||||
raise Exception("Error trying to size noderange {0}".format(noderange))
|
||||
return numnodes
|
||||
|
||||
def simple_nodegroups_command(self, noderange, resource, input=None, key=None, **kwargs):
|
||||
try:
|
||||
@@ -239,18 +365,18 @@ class Command(object):
|
||||
certreqs = ssl.CERT_NONE
|
||||
knownhosts = True
|
||||
self.connection = ssl.wrap_socket(self.connection, ca_certs=cacert,
|
||||
cert_reqs=certreqs,
|
||||
ssl_version=ssl.PROTOCOL_TLSv1)
|
||||
cert_reqs=certreqs)
|
||||
if knownhosts:
|
||||
certdata = self.connection.getpeercert(binary_form=True)
|
||||
fingerprint = 'sha512$' + hashlib.sha512(certdata).hexdigest()
|
||||
fingerprint = fingerprint.encode('utf-8')
|
||||
hostid = '@'.join((port, server))
|
||||
khf = dbm.open(os.path.join(clientcfgdir, "knownhosts"), 'c', 384)
|
||||
if hostid in khf:
|
||||
if fingerprint == khf[hostid]:
|
||||
return
|
||||
else:
|
||||
replace = raw_input(
|
||||
replace = input(
|
||||
"MISMATCHED CERTIFICATE DATA, ACCEPT NEW? (y/n):")
|
||||
if replace not in ('y', 'Y'):
|
||||
raise Exception("BAD CERTIFICATE")
|
||||
@@ -290,10 +416,10 @@ def attrrequested(attr, attrlist, seenattributes):
|
||||
candidate = candidate.replace('hm', 'hardwaremanagement', 1)
|
||||
if candidate in _attraliases:
|
||||
candidate = _attraliases[candidate]
|
||||
if candidate == attr:
|
||||
if fnmatch.fnmatch(attr.lower(), candidate.lower()):
|
||||
seenattributes.add(truename)
|
||||
return True
|
||||
elif attr.startswith(candidate + '.'):
|
||||
elif attr.lower().startswith(candidate.lower() + '.'):
|
||||
seenattributes.add(truename)
|
||||
return True
|
||||
return False
|
||||
@@ -304,7 +430,7 @@ def printattributes(session, requestargs, showtype, nodetype, noderange, options
|
||||
return print_attrib_path(path, session, requestargs, options)
|
||||
|
||||
|
||||
def print_attrib_path(path, session, requestargs, options, rename=None):
|
||||
def print_attrib_path(path, session, requestargs, options, rename=None, attrprefix=None):
|
||||
exitcode = 0
|
||||
seenattributes = set([])
|
||||
for res in session.read(path):
|
||||
@@ -312,20 +438,32 @@ def print_attrib_path(path, session, requestargs, options, rename=None):
|
||||
sys.stderr.write(res['error'] + '\n')
|
||||
exitcode = 1
|
||||
continue
|
||||
for node in res['databynode']:
|
||||
for attr in res['databynode'][node]:
|
||||
for node in sorted(res['databynode']):
|
||||
for attr, val in sorted(
|
||||
res['databynode'][node].items(),
|
||||
key=lambda k: k[1].get('sortid', k[0]) if isinstance(k[1], dict) else k[0]):
|
||||
if attr == 'error':
|
||||
sys.stderr.write('{0}: Error: {1}\n'.format(node, val))
|
||||
continue
|
||||
if attr == 'errorcode':
|
||||
exitcode |= val
|
||||
continue
|
||||
seenattributes.add(attr)
|
||||
if rename and attr in rename:
|
||||
printattr = rename[attr]
|
||||
if rename:
|
||||
printattr = rename.get(attr, attr)
|
||||
else:
|
||||
printattr = attr
|
||||
if attrprefix:
|
||||
printattr = attrprefix + printattr
|
||||
currattr = res['databynode'][node][attr]
|
||||
if (requestargs is None or requestargs == [] or attrrequested(
|
||||
attr, requestargs, seenattributes)):
|
||||
if show_attr(attr, requestargs, seenattributes, options):
|
||||
if 'value' in currattr:
|
||||
if currattr['value'] is not None:
|
||||
val = currattr['value']
|
||||
if isinstance(val, list):
|
||||
val = ','.join(val)
|
||||
attrout = '{0}: {1}: {2}'.format(
|
||||
node, printattr, currattr['value'])
|
||||
node, printattr, val)
|
||||
else:
|
||||
attrout = '{0}: {1}:'.format(node, printattr)
|
||||
elif 'isset' in currattr:
|
||||
@@ -347,8 +485,11 @@ def print_attrib_path(path, session, requestargs, options, rename=None):
|
||||
attrout = '{0}: {1}: {2}'.format(node, printattr, ','.join(map(str, dictout)))
|
||||
else:
|
||||
cprint("CODE ERROR" + repr(attr))
|
||||
|
||||
if options.blame or 'broken' in currattr:
|
||||
try:
|
||||
blame = options.blame
|
||||
except AttributeError:
|
||||
blame = False
|
||||
if blame or 'broken' in currattr:
|
||||
blamedata = []
|
||||
if 'inheritedfrom' in currattr:
|
||||
blamedata.append('inherited from group {0}'.format(
|
||||
@@ -360,7 +501,42 @@ def print_attrib_path(path, session, requestargs, options, rename=None):
|
||||
currattr['expression']))
|
||||
if blamedata:
|
||||
attrout += ' (' + ', '.join(blamedata) + ')'
|
||||
cprint(attrout)
|
||||
try:
|
||||
comparedefault = options.comparedefault
|
||||
except AttributeError:
|
||||
comparedefault = False
|
||||
if comparedefault:
|
||||
try:
|
||||
exclude = options.exclude
|
||||
except AttributeError:
|
||||
exclude = False
|
||||
if ((requestargs and not exclude) or
|
||||
(currattr.get('default', None) is not None and
|
||||
currattr.get('value', None) is not None and
|
||||
currattr['value'] != currattr['default'])):
|
||||
cval = ','.join(currattr['value']) if isinstance(
|
||||
currattr['value'], list) else currattr['value']
|
||||
dval = ','.join(currattr['default']) if isinstance(
|
||||
currattr['default'], list) else currattr['default']
|
||||
cprint('{0}: {1}: {2} (Default: {3})'.format(
|
||||
node, printattr, cval, dval))
|
||||
else:
|
||||
|
||||
try:
|
||||
details = options.detail
|
||||
except AttributeError:
|
||||
details = False
|
||||
if details:
|
||||
if currattr.get('help', None):
|
||||
attrout += u' (Help: {0})'.format(
|
||||
currattr['help'])
|
||||
if currattr.get('possible', None):
|
||||
try:
|
||||
attrout += u' (Choices: {0})'.format(
|
||||
','.join(currattr['possible']))
|
||||
except TypeError:
|
||||
pass
|
||||
cprint(attrout)
|
||||
if not exitcode:
|
||||
if requestargs:
|
||||
for attr in requestargs:
|
||||
@@ -370,6 +546,19 @@ def print_attrib_path(path, session, requestargs, options, rename=None):
|
||||
return exitcode
|
||||
|
||||
|
||||
def show_attr(attr, requestargs, seenattributes, options):
|
||||
try:
|
||||
reverse = options.exclude
|
||||
except AttributeError:
|
||||
reverse = False
|
||||
if requestargs is None or requestargs == []:
|
||||
return True
|
||||
processattr = attrrequested(attr, requestargs, seenattributes)
|
||||
if reverse:
|
||||
processattr = not processattr
|
||||
return processattr
|
||||
|
||||
|
||||
def printgroupattributes(session, requestargs, showtype, nodetype, noderange, options):
|
||||
exitcode = 0
|
||||
seenattributes = set([])
|
||||
@@ -417,7 +606,7 @@ def printgroupattributes(session, requestargs, showtype, nodetype, noderange, op
|
||||
exitcode = 1
|
||||
return exitcode
|
||||
|
||||
def updateattrib(session, updateargs, nodetype, noderange, options):
|
||||
def updateattrib(session, updateargs, nodetype, noderange, options, dictassign=None):
|
||||
# update attribute
|
||||
exitcode = 0
|
||||
if options.clear:
|
||||
@@ -431,26 +620,53 @@ def updateattrib(session, updateargs, nodetype, noderange, options):
|
||||
exitcode = res['errorcode']
|
||||
sys.stderr.write('Error: ' + res['error'] + '\n')
|
||||
sys.exit(exitcode)
|
||||
elif hasattr(options, 'environment') and options.environment:
|
||||
for key in updateargs[1:]:
|
||||
key = key.replace('.', '_')
|
||||
value = os.environ.get(
|
||||
key, os.environ[key.upper()])
|
||||
# Let's do one pass to make sure that there's not a usage problem
|
||||
for key in updateargs[1:]:
|
||||
key = key.replace('.', '_')
|
||||
value = os.environ.get(
|
||||
key, os.environ[key.upper()])
|
||||
if (nodetype == "nodegroups"):
|
||||
exitcode = session.simple_nodegroups_command(noderange,
|
||||
'attributes/all',
|
||||
value, key)
|
||||
else:
|
||||
exitcode = session.simple_noderange_command(noderange,
|
||||
'attributes/all',
|
||||
value, key)
|
||||
sys.exit(exitcode)
|
||||
elif dictassign:
|
||||
for key in dictassign:
|
||||
if nodetype == 'nodegroups':
|
||||
exitcode = session.simple_nodegroups_command(
|
||||
noderange, 'attributes/all', dictassign[key], key)
|
||||
else:
|
||||
exitcode = session.simple_noderange_command(
|
||||
noderange, 'attributes/all', dictassign[key], key)
|
||||
else:
|
||||
if "=" in updateargs[1]:
|
||||
try:
|
||||
if len(updateargs[1:]) > 1:
|
||||
for val in updateargs[1:]:
|
||||
val = val.split('=')
|
||||
if (nodetype == "nodegroups"):
|
||||
exitcode = session.simple_nodegroups_command(noderange, 'attributes/all',
|
||||
val[1],val[0])
|
||||
else:
|
||||
exitcode = session.simple_noderange_command(noderange, 'attributes/all',
|
||||
val[1], val[0])
|
||||
else:
|
||||
val = updateargs[1].split('=')
|
||||
if nodetype == "nodegroups" :
|
||||
exitcode = session.simple_nodegroups_command(noderange, 'attributes/all',
|
||||
val[1], val[0])
|
||||
for val in updateargs[1:]:
|
||||
val = val.split('=', 1)
|
||||
if val[0][-1] in (',', '-', '^'):
|
||||
key = val[0][:-1]
|
||||
if val[0][-1] == ',':
|
||||
value = {'prepend': val[1]}
|
||||
elif val[0][-1] in ('-', '^'):
|
||||
value = {'remove': val[1]}
|
||||
else:
|
||||
key = val[0]
|
||||
value = val[1]
|
||||
if (nodetype == "nodegroups"):
|
||||
exitcode = session.simple_nodegroups_command(noderange, 'attributes/all',
|
||||
value, key)
|
||||
else:
|
||||
exitcode = session.simple_noderange_command(noderange, 'attributes/all',
|
||||
val[1], val[0])
|
||||
value, key)
|
||||
except:
|
||||
sys.stderr.write('Error: {0} not a valid expression\n'.format(str(updateargs[1:])))
|
||||
exitcode = 1
|
||||
@@ -489,4 +705,4 @@ def check_globbing(noderange):
|
||||
'bash or change directories such that there is no filename '
|
||||
'that would conflict.'
|
||||
'\n'.format(noderange))
|
||||
sys.exit(1)
|
||||
sys.exit(1)
|
||||
|
||||
@@ -44,6 +44,8 @@ class ScreenPrinter(object):
|
||||
self.fieldwidth = maxlen + textlen + 1 # 1 for column
|
||||
|
||||
def set_output(self, node, text):
|
||||
if self.nodeoutput[node] == text:
|
||||
return
|
||||
self.nodeoutput[node] = text
|
||||
if len(text) >= self.textlen:
|
||||
self.textlen = len(text) + 1
|
||||
|
||||
@@ -169,13 +169,18 @@ class GroupedData(object):
|
||||
output.flush()
|
||||
|
||||
def print_deviants(self, output=sys.stdout, skipmodal=False, reverse=False,
|
||||
count=False):
|
||||
count=False, basenode=None):
|
||||
self.generate_byoutput()
|
||||
modaloutput = None
|
||||
ismodal = True
|
||||
revoutput = []
|
||||
if basenode:
|
||||
for checkout in self.byoutput:
|
||||
if basenode in self.byoutput[checkout]:
|
||||
modaloutput = checkout
|
||||
for outdata in sorted(
|
||||
self.byoutput, key=lambda x: [0 - len(self.byoutput[x]),
|
||||
self.byoutput, key=lambda x: [0 if modaloutput == x else 1,
|
||||
0 - len(self.byoutput[x]),
|
||||
humanify_nodename(
|
||||
self.get_group_text(
|
||||
self.byoutput[x]
|
||||
|
||||
@@ -20,6 +20,16 @@ from datetime import datetime
|
||||
import json
|
||||
import struct
|
||||
|
||||
try:
|
||||
unicode
|
||||
except NameError:
|
||||
unicode = str
|
||||
|
||||
try:
|
||||
range = xrange
|
||||
except NameError:
|
||||
pass
|
||||
|
||||
def decodestr(value):
|
||||
ret = None
|
||||
try:
|
||||
@@ -29,26 +39,39 @@ def decodestr(value):
|
||||
ret = value.decode('cp437')
|
||||
except UnicodeDecodeError:
|
||||
ret = value
|
||||
except AttributeError:
|
||||
return value
|
||||
return ret
|
||||
|
||||
def unicode_dictvalues(dictdata):
|
||||
for key in dictdata:
|
||||
if isinstance(dictdata[key], str):
|
||||
if isinstance(dictdata[key], bytes):
|
||||
dictdata[key] = decodestr(dictdata[key])
|
||||
elif isinstance(dictdata[key], datetime):
|
||||
dictdata[key] = dictdata[key].strftime('%Y-%m-%dT%H:%M:%S')
|
||||
elif isinstance(dictdata[key], list):
|
||||
for i in xrange(len(dictdata[key])):
|
||||
if isinstance(dictdata[key][i], str):
|
||||
dictdata[key][i] = decodestr(dictdata[key][i])
|
||||
elif isinstance(dictdata[key][i], dict):
|
||||
unicode_dictvalues(dictdata[key][i])
|
||||
_unicode_list(dictdata[key])
|
||||
elif isinstance(dictdata[key], dict):
|
||||
unicode_dictvalues(dictdata[key])
|
||||
|
||||
|
||||
def _unicode_list(currlist):
|
||||
for i in range(len(currlist)):
|
||||
if isinstance(currlist[i], str):
|
||||
currlist[i] = decodestr(currlist[i])
|
||||
elif isinstance(currlist[i], dict):
|
||||
unicode_dictvalues(currlist[i])
|
||||
elif isinstance(currlist[i], list):
|
||||
_unicode_list(currlist[i])
|
||||
|
||||
|
||||
def send(handle, data):
|
||||
if isinstance(data, str):
|
||||
if isinstance(data, unicode):
|
||||
try:
|
||||
data = data.encode('utf-8')
|
||||
except AttributeError:
|
||||
pass
|
||||
if isinstance(data, bytes) or isinstance(data, unicode):
|
||||
# plain text, e.g. console data
|
||||
tl = len(data)
|
||||
if tl == 0:
|
||||
@@ -74,6 +97,14 @@ def send(handle, data):
|
||||
handle.sendall(struct.pack("!I", tl))
|
||||
handle.sendall(sdata)
|
||||
|
||||
def recvall(handle, size):
|
||||
rd = handle.recv(size)
|
||||
while len(rd) < size:
|
||||
nd = handle.recv(size - len(rd))
|
||||
if not nd:
|
||||
raise Exception("Error reading data")
|
||||
rd += nd
|
||||
return rd
|
||||
|
||||
def recv(handle):
|
||||
tl = handle.recv(4)
|
||||
|
||||
@@ -24,10 +24,18 @@ a confluent server.
|
||||
%setup -n %{name}-%{version} -n %{name}-%{version}
|
||||
|
||||
%build
|
||||
python setup.py build
|
||||
%if "%{dist}" == ".el8"
|
||||
python3 setup.py build
|
||||
%else
|
||||
python2 setup.py build
|
||||
%endif
|
||||
|
||||
%install
|
||||
python setup.py install --single-version-externally-managed -O1 --root=$RPM_BUILD_ROOT --record=INSTALLED_FILES --install-scripts=/opt/confluent/bin --install-purelib=/opt/confluent/lib/python
|
||||
%if "%{dist}" == ".el8"
|
||||
python3 setup.py install --single-version-externally-managed -O1 --root=$RPM_BUILD_ROOT --record=INSTALLED_FILES --install-scripts=/opt/confluent/bin --install-purelib=/opt/confluent/lib/python
|
||||
%else
|
||||
python2 setup.py install --single-version-externally-managed -O1 --root=$RPM_BUILD_ROOT --record=INSTALLED_FILES --install-scripts=/opt/confluent/bin --install-purelib=/opt/confluent/lib/python
|
||||
%endif
|
||||
|
||||
|
||||
%clean
|
||||
|
||||
@@ -29,10 +29,257 @@ alias nodehealth='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export C
|
||||
alias nodeidentify='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export CURRENT_CMDLINE; nodeidentify'
|
||||
alias nodeinventory='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export CURRENT_CMDLINE; nodeinventory'
|
||||
alias nodelist='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export CURRENT_CMDLINE; nodelist'
|
||||
alias nodemedia='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export CURRENT_CMDLINE; nodemedia'
|
||||
alias nodepower='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export CURRENT_CMDLINE; nodepower'
|
||||
alias noderemove='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export CURRENT_CMDLINE; noderemove'
|
||||
alias nodereseat='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export CURRENT_CMDLINE; nodereseat'
|
||||
alias noderun='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export CURRENT_CMDLINE; noderun'
|
||||
alias nodesensors='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export CURRENT_CMDLINE; nodesensors'
|
||||
alias nodesetboot='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export CURRENT_CMDLINE; nodesetboot'
|
||||
alias nodestorage='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export CURRENT_CMDLINE; nodestorage'
|
||||
alias nodeshell='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export CURRENT_CMDLINE; nodeshell'
|
||||
alias nodelicense='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export CURRENT_CMDLINE; nodelicense'
|
||||
# Do not continue for non-bash shells, the rest of this sets up bash completion functions
|
||||
[ -z "$BASH_VERSION" -o -z "$PS1" ] && return
|
||||
|
||||
|
||||
_confluent_get_args()
|
||||
{
|
||||
CMPARGS=($COMP_LINE)
|
||||
NUMARGS=$((COMP_CWORD+1))
|
||||
if [ "${COMP_WORDS[COMP_CWORD]}" == '' ]; then
|
||||
CMPARGS+=("")
|
||||
fi
|
||||
GENNED=""
|
||||
for CAND in ${COMP_CANDIDATES[@]}; do
|
||||
candarray=(${CAND//,/ })
|
||||
matched=0
|
||||
for c in "${candarray[@]}"; do
|
||||
for arg in "${CMPARGS[@]}"; do
|
||||
if [ "$arg" = "$c" ]; then
|
||||
matched=1
|
||||
break
|
||||
fi
|
||||
done
|
||||
done
|
||||
if [ 0 = $matched ]; then
|
||||
for c in "${candarray[@]}"; do
|
||||
GENNED+=" $c"
|
||||
done
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
function _confluent_generic_completion()
|
||||
{
|
||||
_confluent_get_args
|
||||
if [ $NUMARGS -ge 3 ] && [ ! -z "$GENNED" ]; then
|
||||
COMPREPLY=($(compgen -W "$GENNED" -- ${COMP_WORDS[COMP_CWORD]}))
|
||||
fi
|
||||
if [ $NUMARGS -lt 3 ]; then
|
||||
_confluent_nr_completion
|
||||
return;
|
||||
fi
|
||||
}
|
||||
_confluent_nodeidentify_completion()
|
||||
{
|
||||
COMP_CANDIDATES=("on,off,blink -h")
|
||||
_confluent_generic_completion
|
||||
}
|
||||
|
||||
|
||||
_confluent_nodesetboot_completion()
|
||||
{
|
||||
COMP_CANDIDATES=("default,cd,network,setup,hd -h -b -p")
|
||||
_confluent_generic_completion
|
||||
}
|
||||
|
||||
_confluent_nodepower_completion()
|
||||
{
|
||||
COMP_CANDIDATES=("boot,off,on,status -h -p")
|
||||
_confluent_generic_completion
|
||||
}
|
||||
|
||||
_confluent_nodemedia_completion()
|
||||
{
|
||||
COMP_CANDIDATES=("list,upload,attach,detachall -h")
|
||||
_confluent_get_args
|
||||
if [ $NUMARGS -gt 3 ] && [ ${CMPARGS[-2]} == 'upload' ]; then
|
||||
compopt -o default
|
||||
COMPREPLY=()
|
||||
return
|
||||
fi
|
||||
if [ $NUMARGS -ge 3 ] && [ ! -z "$GENNED" ]; then
|
||||
COMPREPLY=($(compgen -W "$GENNED" -- ${COMP_WORDS[COMP_CWORD]}))
|
||||
return;
|
||||
fi
|
||||
if [ $NUMARGS -lt 3 ]; then
|
||||
_confluent_nr_completion
|
||||
return;
|
||||
fi
|
||||
}
|
||||
|
||||
_confluent_nodefirmware_completion()
|
||||
{
|
||||
_confluent_get_args
|
||||
if [ $NUMARGS == 3 ]; then
|
||||
COMPREPLY=($(compgen -W "list update" -- ${COMP_WORDS[COMP_CWORD]}))
|
||||
return;
|
||||
fi
|
||||
if [ $NUMARGS -gt 3 ] && [ ${CMPARGS[2]} == 'update' ]; then
|
||||
compopt -o default
|
||||
COMPREPLY=()
|
||||
return
|
||||
fi
|
||||
if [ $NUMARGS -lt 3 ]; then
|
||||
_confluent_nr_completion
|
||||
return;
|
||||
fi
|
||||
}
|
||||
|
||||
_confluent_nodeshell_completion()
|
||||
{
|
||||
_confluent_get_args
|
||||
if [ $NUMARGS == 3 ]; then
|
||||
COMPREPLY=($(compgen -c -- ${COMP_WORDS[COMP_CWORD]}))
|
||||
return
|
||||
fi
|
||||
if [ $NUMARGS -gt 3 ]; then
|
||||
compopt -o default
|
||||
COMPREPLY=()
|
||||
return
|
||||
fi
|
||||
if [ $NUMARGS -lt 3 ]; then
|
||||
_confluent_nr_completion
|
||||
return;
|
||||
fi
|
||||
}
|
||||
|
||||
_confluent_nodelicense_completion()
|
||||
{
|
||||
_confluent_get_args
|
||||
if [ $NUMARGS == 3 ]; then
|
||||
COMPREPLY=($(compgen -W "install list save delete" -- ${COMP_WORDS[COMP_CWORD]}))
|
||||
return;
|
||||
fi
|
||||
if [ $NUMARGS == 4 ] && [ ${CMPARGS[2]} == 'install' ]; then
|
||||
compopt -o default
|
||||
COMPREPLY=()
|
||||
return
|
||||
fi
|
||||
if [ $NUMARGS == 4 ] && [ ${CMPARGS[2]} == 'save' ]; then
|
||||
compopt -o dirnames
|
||||
COMPREPLY=()
|
||||
return
|
||||
fi
|
||||
if [ $NUMARGS -lt 3 ]; then
|
||||
_confluent_nr_completion
|
||||
return
|
||||
fi
|
||||
}
|
||||
|
||||
_confluent_nodesupport_completion()
|
||||
{
|
||||
_confluent_get_args
|
||||
if [ $NUMARGS == 3 ]; then
|
||||
COMPREPLY=($(compgen -W "servicedata" -- ${COMP_WORDS[COMP_CWORD]}))
|
||||
return;
|
||||
fi
|
||||
if [ $NUMARGS == 4 ] && [ ${CMPARGS[2]} == 'servicedata' ]; then
|
||||
compopt -o dirnames
|
||||
COMPREPLY=()
|
||||
return
|
||||
fi
|
||||
if [ $NUMARGS -lt 3 ]; then
|
||||
_confluent_nr_completion
|
||||
return
|
||||
fi
|
||||
}
|
||||
|
||||
_confluent_nodeattrib_completion()
|
||||
{
|
||||
COMP_CANDIDATES=$(nodeattrib '~.>1' all | awk '{print $2}'|sed -e 's/://')
|
||||
_confluent_generic_completion
|
||||
}
|
||||
|
||||
|
||||
_confluent_nn_completion()
|
||||
{
|
||||
_confluent_get_args
|
||||
if [ $NUMARGS -gt 2 ]; then
|
||||
return;
|
||||
fi
|
||||
INPUT=${COMP_WORDS[COMP_CWORD]}
|
||||
INPUT=${INPUT##*,-}
|
||||
INPUT=${INPUT##*,}
|
||||
INPUT=${INPUT##*@}
|
||||
PREFIX=""
|
||||
if [ "$INPUT" != "${COMP_WORDS[COMP_CWORD]}" ]; then
|
||||
PREFIX=${COMP_WORDS[COMP_CWORD]}
|
||||
PREFIX=$(echo $PREFIX | sed -e 's/,[^,@-]*$/,/' -e 's/,-[^,@]*$/,-/' -e 's/@[^,@]*/@/')
|
||||
fi
|
||||
|
||||
COMPREPLY=($(compgen -W "$(nodelist | sed -e s/^/$PREFIX/)" -- "${COMP_WORDS[COMP_CWORD]}"))
|
||||
}
|
||||
_confluent_nr_completion()
|
||||
{
|
||||
CMPARGS=($COMP_LINE)
|
||||
_confluent_get_args
|
||||
if [ $NUMARGS -gt 2 ]; then
|
||||
return;
|
||||
fi
|
||||
INPUT=${COMP_WORDS[COMP_CWORD]}
|
||||
INPUT=${INPUT##*,-}
|
||||
INPUT=${INPUT##*,}
|
||||
INPUT=${INPUT##*@}
|
||||
PREFIX=""
|
||||
if [ "$INPUT" != "${COMP_WORDS[COMP_CWORD]}" ]; then
|
||||
PREFIX=${COMP_WORDS[COMP_CWORD]}
|
||||
PREFIX=$(echo $PREFIX | sed -e 's/,[^,@-]*$/,/' -e 's/,-[^,@]*$/,-/' -e 's/@[^,@]*/@/')
|
||||
fi
|
||||
|
||||
COMPREPLY=($(compgen -W "$(confetty show /nodegroups|sed -e 's/\///' -e s/^/$PREFIX/;nodelist | sed -e s/^/$PREFIX/)" -- "${COMP_WORDS[COMP_CWORD]}"))
|
||||
}
|
||||
_confluent_ng_completion()
|
||||
{
|
||||
_confluent_get_args
|
||||
if [ $NUMARGS -gt 2 ]; then
|
||||
return;
|
||||
fi
|
||||
INPUT=${COMP_WORDS[COMP_CWORD]}
|
||||
INPUT=${INPUT##*,-}
|
||||
INPUT=${INPUT##*,}
|
||||
INPUT=${INPUT##*@}
|
||||
PREFIX=""
|
||||
if [ "$INPUT" != "${COMP_WORDS[COMP_CWORD]}" ]; then
|
||||
PREFIX=${COMP_WORDS[COMP_CWORD]}
|
||||
PREFIX=$(echo $PREFIX | sed -e 's/,[^,@-]*$/,/' -e 's/,-[^,@]*$/,-/' -e 's/@[^,@]*/@/')
|
||||
fi
|
||||
|
||||
COMPREPLY=($(compgen -W "$(confetty show /nodegroups|sed -e 's/\///' -e s/^/$PREFIX/)" -- "${COMP_WORDS[COMP_CWORD]}"))
|
||||
}
|
||||
complete -F _confluent_nodeattrib_completion nodeattrib
|
||||
complete -F _confluent_nodeattrib_completion nodegroupattrib
|
||||
complete -F _confluent_nr_completion nodebmcreset
|
||||
complete -F _confluent_nodesetboot_completion nodeboot
|
||||
complete -F _confluent_nr_completion nodeconfig
|
||||
complete -F _confluent_nn_completion nodeconsole
|
||||
complete -F _confluent_nr_completion nodeeventlog
|
||||
complete -F _confluent_nodefirmware_completion nodefirmware
|
||||
complete -F _confluent_ng_completion nodegroupattrib
|
||||
complete -F _confluent_ng_completion nodegroupremove
|
||||
complete -F _confluent_nr_completion nodehealth
|
||||
complete -F _confluent_nodeidentify_completion nodeidentify
|
||||
complete -F _confluent_nr_completion nodeinventory
|
||||
complete -F _confluent_nodeattrib_completion nodelist
|
||||
complete -F _confluent_nodemedia_completion nodemedia
|
||||
complete -F _confluent_nodepower_completion nodepower
|
||||
complete -F _confluent_nr_completion noderemove
|
||||
complete -F _confluent_nr_completion nodereseat
|
||||
complete -F _confluent_nodeshell_completion noderun
|
||||
complete -F _confluent_nr_completion nodesensors
|
||||
complete -F _confluent_nodesetboot_completion nodesetboot
|
||||
complete -F _confluent_nodeshell_completion nodeshell
|
||||
complete -F _confluent_nodesupport_completion nodesupport
|
||||
complete -F _confluent_nodelicense_completion nodelicense
|
||||
|
||||
|
||||
@@ -3,13 +3,16 @@ collate(1) -- Organize text input by node
|
||||
|
||||
## SYNOPSIS
|
||||
|
||||
`<other command> | collate [-a] [-d] [-w] [-s] [-c] [-r]`
|
||||
`<other command> | collate [-a] [-d] [-w] [-s] [-c] [-r] [-l lognametemplate]`
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
**collate** takes input in the form of <nodename>: <data> and groups
|
||||
the output for each <nodename> together, and checks for identical data to further group nodes together. It also will sort the output groups so that
|
||||
the most frequently seen output is printed first, and then other groups in descending order of frequency.
|
||||
the output for each <nodename> together, and checks for identical data to further group nodes together.
|
||||
Output groups are sorted in descending size such that the topmost group is the largest, and the
|
||||
last output group is the smallest. In the event of equally sized output groups,
|
||||
the groups are sorted alphanumerically by their header, and each header has
|
||||
node and group names sorted alphanumerically.
|
||||
|
||||
## OPTIONS
|
||||
|
||||
@@ -23,6 +26,10 @@ the most frequently seen output is printed first, and then other groups in desce
|
||||
Express all but the most common result group in terms of diff from
|
||||
the most common result group
|
||||
|
||||
* `-l`, `--log`:
|
||||
Save output per node to individual log files, replacing {node} in the name
|
||||
with the nodename of each
|
||||
|
||||
* `-w`, `--watch`:
|
||||
Update results dynamically as data becomes available, rather than
|
||||
waiting for the command to fully complete.
|
||||
@@ -82,4 +89,4 @@ the most frequently seen output is printed first, and then other groups in desce
|
||||
` Processors.ExecuteDisableBit=Enable`
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
collective(1) -- Check and manage a confluent collective
|
||||
==============================
|
||||
|
||||
## SYNOPSIS
|
||||
|
||||
`collective invite <server>`
|
||||
`collective join <server> [-i TOKEN]`
|
||||
`collective show`
|
||||
`collective gencert`
|
||||
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
**collective** helps manage the collective mode of confluent, where multiple
|
||||
confluent servers are linked together to act as one. For example, the procedure to set up
|
||||
a collective to run on three servers called mgt1, mgt2, and mgt3, first install and start
|
||||
confluent as usual on the three servers. On mgt1, run `collective invite mgt2` and an
|
||||
invitation token will be output. On mgt2, either run `collective join mgt1` to paste
|
||||
the token interactively, or `collective join mgt1 -i <token>`. At this point, either
|
||||
mgt1 or mgt2 can bring in mgt3. For example on mgt2 run `collective invite mgt3` and
|
||||
on mgt3 run `collective join mgt2 -i <token>`
|
||||
|
||||
This can be linked together in the following manner with ssh:
|
||||
on mgt1:
|
||||
`# ssh mgt2 collective join mgt1 -i $(collective invite mgt2)`
|
||||
|
||||
Note that a collective is only redundant with 3 or more members. The collective
|
||||
will function so long as more than half of the members are online. A collective
|
||||
of two members is supported, but without redundancy.
|
||||
|
||||
Also note that the collective leader role is dynamic, but has no impact on interacting
|
||||
with confluent. It is merely an internal role that can dynamically change depending
|
||||
on circumstances.
|
||||
|
||||
## OPTIONS
|
||||
|
||||
* `-i`:
|
||||
Provide the token as an argument rather than interactively.
|
||||
|
||||
## EXAMPLES
|
||||
* Inviting a server called mgt2:
|
||||
`# collective invite mgt2`
|
||||
`bWd0MkA+BNQ6XAxMXlqJJa+EQRlihL/k9xCXnasgSQXZr989Pa1/ln7G3e1Ncxx6BMzMqqreHJVkPr2FrzjNit/UgHlg`
|
||||
|
||||
* On mgt2, joining mgt1:
|
||||
`# collective join mgt1 -i bWd0MkA+BNQ6XAxMXlqJJa+EQRlihL/k9xCXnasgSQXZr989Pa1/ln7G3e1Ncxx6BMzMqqreHJVkPr2FrzjNit/UgHlg`
|
||||
`Success`
|
||||
|
||||
* Showing the collective state:
|
||||
`# collective show`
|
||||
`Quorum: True`
|
||||
`Leader: mgt1`
|
||||
`Active collective members:`
|
||||
` mgt2`
|
||||
|
||||
@@ -3,9 +3,11 @@ nodeattrib(8) -- List or change confluent nodes attributes
|
||||
|
||||
## SYNOPSIS
|
||||
|
||||
`nodeattrib [-b] <noderange> [<nodeattribute>...]`
|
||||
`nodeattrib [-b] <noderange> [all|<nodeattribute>...]`
|
||||
`nodeattrib <noderange> [<nodeattribute1=value1> <nodeattribute2=value2> ...]`
|
||||
`nodeattrib -c <noderange> <nodeattribute1> <nodeattribute2> ...`
|
||||
`nodeattrib -e <noderange> <nodeattribute1> <nodeattribute2> ...`
|
||||
`nodeattrib -p <noderange> <nodeattribute1> <nodeattribute2> ...`
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
@@ -18,9 +20,19 @@ displayed. If `-b` is specified, it will also display information on
|
||||
how inherited and expression based attributes are defined. Attributes can be
|
||||
straightforward values, or an expression as documented in nodeattribexpressions(5).
|
||||
For a full list of attributes, run `nodeattrib <node> all` against a node.
|
||||
If `-c` is specified, this will set the nodeattribute to a null valid.
|
||||
If `-c` is specified, this will set the nodeattribute to a null value.
|
||||
This is different from setting the value to an empty string.
|
||||
|
||||
Attributes may be specified by wildcard, for example `net.*switch` will report
|
||||
all attributes that begin with `net.` and end with `switch`.
|
||||
|
||||
If the word all is specified, then all available attributes are given.
|
||||
Omitting any attribute name or the word 'all' will display only attributes
|
||||
that are currently set.
|
||||
|
||||
For the `groups` attribute, it is possible to add a group by doing
|
||||
`groups,=<newgroup>` and to remove by doing `groups^=<oldgroup>`
|
||||
|
||||
Note that `nodeattrib <group>` will likely not provide the expected behavior.
|
||||
See nodegroupattrib(8) command on how to manage attributes on a group level.
|
||||
|
||||
@@ -30,6 +42,10 @@ See nodegroupattrib(8) command on how to manage attributes on a group level.
|
||||
Annotate inherited and expression based attributes to show their base value.
|
||||
* `-c`, `--clear`:
|
||||
Clear specified nodeattributes
|
||||
* `-e`, `--environment`:
|
||||
Set specified attributes based on exported environment variable of matching name. Environment variable names may be lower case or all upper case. Replace . with _ as needed (e.g. info.note may be specified as either $info_note or $INFO_NOTE
|
||||
* `-p`, `--prompt`:
|
||||
Request interactive prompting to provide values rather than the command line or environment variables.
|
||||
|
||||
## EXAMPLES
|
||||
* Listing matching nodes of a simple noderange:
|
||||
@@ -75,6 +91,12 @@ See nodegroupattrib(8) command on how to manage attributes on a group level.
|
||||
`n1: console.method: `
|
||||
`n2: console.method: `
|
||||
|
||||
* List all switches that a node is described as connected to:
|
||||
`# nodeattrib d1 net.*switch`
|
||||
`d1: net.mgt.switch: mgtswitch1`
|
||||
`d1: net.pxe.switch: pxeswitch1`
|
||||
`d1: net.switch:`
|
||||
|
||||
## SEE ALSO
|
||||
|
||||
nodegroupattrib(8), nodeattribexpressions(5)
|
||||
|
||||
@@ -9,6 +9,18 @@ expression to generate the value.
|
||||
An expression will contain some directives wrapped in `{}` characters. Within
|
||||
`{}` are a number of potential substitute values and operations.
|
||||
|
||||
Note that syntax of expressions can have overlap with the shell syntax.
|
||||
For example:
|
||||
|
||||
`$ echo (n2)`
|
||||
`-bash: syntax error near unexpected token `n2'`
|
||||
|
||||
In such a case, it helps to quote the expression to allow it to be passed:
|
||||
|
||||
`$ echo '(n2)'`
|
||||
`(n2)`
|
||||
|
||||
|
||||
The most common operation is to extract a number from the nodename. These
|
||||
values are available as n1, n2, etc. So for example attributes for a node named
|
||||
b1o2r3u4 would have {n1} as 1, {n2} as 2, {n3} as 3, and {n4} as 4.
|
||||
@@ -39,7 +51,7 @@ number of operators at the end to indicate formatting changes.
|
||||
|
||||
Another common element to pull into an expression is the node name in whole:
|
||||
|
||||
`hardwaremanagement.manager={nodename}-imm`
|
||||
`hardwaremanagement.manager={node}-imm`
|
||||
|
||||
Additionally other attributes may be pulled in:
|
||||
|
||||
@@ -47,7 +59,7 @@ Additionally other attributes may be pulled in:
|
||||
|
||||
Multiple expressions are permissible within a single attribute:
|
||||
|
||||
`hardwaremanagement.manager={nodename}-{hardwaremanagement.method}`
|
||||
`hardwaremanagement.manager={node}-{hardwaremanagement.method}`
|
||||
|
||||
A note to developers: in general the API layer will automatically recognize a
|
||||
generic set attribute to string with expression syntax and import it as an
|
||||
|
||||
@@ -4,12 +4,44 @@ nodeboot(8) -- Reboot a confluent node to a specific device
|
||||
## SYNOPSIS
|
||||
|
||||
`nodeboot <noderange>`
|
||||
`nodeboot <noderange>` [net|setup]
|
||||
`nodeboot [options] <noderange>` [default|cd|network|setup|hd]
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
**nodeboot** reboots nodes in a noderange. If an additional argument is given,
|
||||
it sets the node to specifically boot to that as the next boot.
|
||||
it sets the node to specifically boot to that as the next boot. This
|
||||
performs an immediate reboot without waiting for the OS. To set the boot
|
||||
device without inducing a reboot, see the `nodesetboot` command.
|
||||
|
||||
## OPTIONS
|
||||
|
||||
* `-b`, `--bios`:
|
||||
For a system that supports both BIOS and UEFI style boot, request BIOS style
|
||||
boot if supported (some platforms will UEFI boot with this flag anyway).
|
||||
|
||||
* `-u`, `--uefi`:
|
||||
This flag does nothing, it is for command compatibility with xCAT's rsetboot
|
||||
|
||||
* `-p`, `--persist`:
|
||||
For a system that supports it, mark the boot override to persist rather than
|
||||
be a one time change. Many systems do not support this functionality.
|
||||
|
||||
* `default`:
|
||||
Request a normal default boot with no particular device override
|
||||
|
||||
* `cd`:
|
||||
Request boot from media. Note that this can include physical CD,
|
||||
remote media mounted as CD/DVD, and detachable hard disks drives such as usb
|
||||
key devices.
|
||||
|
||||
* `network`:
|
||||
Request boot to network
|
||||
|
||||
* `setup`:
|
||||
Request to enter the firmware configuration menu (e.g. F1 setup) on next boot.
|
||||
|
||||
* `hd`:
|
||||
Boot straight to hard disk drive
|
||||
|
||||
## EXAMPLES
|
||||
* Booting n3 and n4 to the default boot behavior:
|
||||
|
||||
@@ -3,8 +3,8 @@ nodeconfig(8) -- Show or change node configuration
|
||||
|
||||
## SYNOPSIS
|
||||
|
||||
`nodecanfig <noderange> [<configuration>..]`
|
||||
`nodecanfig <noderange> [<configuration=value>..]`
|
||||
`nodeconfig <noderange> [options] [<configuration>..]`
|
||||
`nodeconfig <noderange> [options] [<configuration=value>..]`
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
@@ -14,6 +14,31 @@ running configuration on the node firmware. Calling without '=' will show the
|
||||
current value, and '=' will change the value. Network information can be
|
||||
given as a node expression, as documented in the man page for nodeattribexpressions(5).
|
||||
|
||||
## OPTIONS
|
||||
|
||||
* `-c`, `--comparedefault`:
|
||||
Take the given settings and compare against default value, if available. If
|
||||
no configuration values are specified, it will show only those that differ.
|
||||
If combined with `-x`, will show all differing values except those indicated
|
||||
by `-x`
|
||||
|
||||
* `-x`, `--exclude`:
|
||||
Rather than listing only the specified configuration parameters, list all
|
||||
attributes except for the specified ones
|
||||
|
||||
* `-d`, `--detail`:
|
||||
Provide detailed data as available. This can include help text and valid
|
||||
values for a setting.
|
||||
|
||||
* `-r`, `--restoredefault`:
|
||||
Request that the specified component of the targeted nodes will have its
|
||||
configuration reset to default. Currently the only component implemented
|
||||
is uefi.
|
||||
|
||||
* `-b`, '--batch':
|
||||
Provide arguments as lines of a file, rather than the command line.
|
||||
|
||||
|
||||
## EXAMPLES
|
||||
* Showing the current IP configuration of noderange BMC/IMM/XCC:
|
||||
`# nodeconfig s3,s4 bmc`
|
||||
|
||||
@@ -2,7 +2,7 @@ nodeconsole(8) -- Open a console to a confluent node
|
||||
=====================================================
|
||||
|
||||
## SYNOPSIS
|
||||
`nodeconsole <node>`
|
||||
`nodeconsole [options] <noderange>`
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
@@ -16,6 +16,12 @@ will initiate an automatic retry interval that is randomized between 2 and 4 min
|
||||
The reopen escape sequence below requests an immediate retry, as does connecting
|
||||
a new session.
|
||||
|
||||
## OPTIONS
|
||||
|
||||
* `-t`, `--tile`:
|
||||
Use tmux to arrange consoles of the given noderange into a tiled layout on
|
||||
the terminal screen
|
||||
|
||||
## ESCAPE SEQUENCE COMMANDS
|
||||
|
||||
While connected to a console, a number of commands may be performed through escape
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
nodedefine(8) -- Define new confluent nodes
|
||||
===================================================================
|
||||
|
||||
## SYNOPSIS
|
||||
|
||||
|
||||
@@ -5,7 +5,9 @@ nodediscover(8) -- List or manage confluent node discovery
|
||||
|
||||
`nodediscover rescan`
|
||||
`nodediscover [options] list`
|
||||
`nodeattrib [options] assign`
|
||||
`nodediscover [options] assign`
|
||||
`nodediscover [options] rescan`
|
||||
`nodediscover [options] clear`
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
@@ -22,6 +24,14 @@ data may be filtered by various parameters, as denoted in the options below.
|
||||
identity or, using `-i`, using a csv file to assign nodes all at once. For
|
||||
example, a spreadsheet of serial numbers to desired node names could be used.
|
||||
|
||||
**nodediscover rescan** requests the server to do an active sweep for new
|
||||
devices. Generally every effort is made to passively detect devices as they
|
||||
become available (as they boot or are plugged in), however sometimes an active
|
||||
scan is the best approach to catch something that appears to be missing.
|
||||
|
||||
**nodedsicover clear** requests the server forget about a selection of
|
||||
detected device. It takes the same arguments as **nodediscover list**.
|
||||
|
||||
## CSV FORMAT
|
||||
|
||||
The CSV format used by nodediscover consists of one header describing the
|
||||
@@ -65,12 +75,40 @@ the nodes.
|
||||
* `-e MAC`, `--ethaddr=MAC`:
|
||||
Operate against the system with the specified MAC
|
||||
address
|
||||
* `-t TYPE, --type=TYPE`:
|
||||
* `-f FIELDS`, `--fields=FIELDS`:
|
||||
Request a custom set of fields. The available fields are:
|
||||
Node: The node name if a correlation has been identified
|
||||
Model: The model number
|
||||
Serial: The serial number
|
||||
UUID: The UUID as it should appear in DMI
|
||||
Type: Device type (e.g. lenovo-xcc, pxe-client, etc)
|
||||
IP: The confirmed working IP addresses associated with the record
|
||||
Mac: Mac address of the relevant network interface
|
||||
Switch: The nearest detected switch to the entry
|
||||
Port: Port of the switch that most closely connects to the network interface
|
||||
Advertised IP: IP addresses that may not have been confirmed, but are advertised
|
||||
* `-o ORDER`, `--order=ORDER`:
|
||||
Order output by given field. Field names are the same as documented in the -f argument.
|
||||
* `-t TYPE`, `--type=TYPE`:
|
||||
Operate against the system of the specified type
|
||||
* `-c, --csv`:
|
||||
* `-c`, `--csv`:
|
||||
Use CSV formatted output
|
||||
* `-i IMPORT.CSV`, `--import=IMPORT.CSV`:
|
||||
Import bulk assignment data from given CSV file
|
||||
* `-d STATE`, `--discoverystate=STATE`:
|
||||
Indicate devices with a particular state. The states are listed below
|
||||
* discovered: The device has been identified and has also had discovery
|
||||
activities performed, including any relevant certificate
|
||||
exchanges and deploying user and network configuration.
|
||||
* identified: The device has been identified as to what node it is
|
||||
supposed to be, however no active changes to the attributes
|
||||
or device configuration has been performed. This is
|
||||
generally discovery awaiting approval due to
|
||||
discovery.policy specifying a strict security model.
|
||||
* unidentified: A device has been sensed, but no node identity has been
|
||||
established at all. It provides data that can be used
|
||||
for nodediscover assign, as well as current IP addresses
|
||||
that can be used for manual efforts as possible.
|
||||
|
||||
## EXAMPLES
|
||||
|
||||
|
||||
@@ -4,12 +4,23 @@ nodefirmware(8) -- Report firmware information on confluent nodes
|
||||
## SYNOPSIS
|
||||
|
||||
`nodefirmware <noderange>`
|
||||
`nodefirmware <noderange> update [--backup] <filename>`
|
||||
`nodefirmware <noderange> list|<components>|core`
|
||||
`nodefirmware <noderange> update [--backup] <filename>`
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
`nodefirmware` reports and updates various firmware on nodes. In the update form, it accepts a single
|
||||
file and attempts to update it using the out of band facilities. Firmware updates can end in one of three states:
|
||||
`nodefirmware` reports and updates various firmware on nodes. By default it
|
||||
will retrieve all firmware, but can be directed to fetch specific firmware by
|
||||
calling out the name of the firmware (e.g. uefi or xcc) or request reading only
|
||||
core firmware firmware by using the word 'core', which is generally a quicker
|
||||
operation. Different hardwaremanagement.method indicated plugins may have
|
||||
different capabilities available. For example, the 'core' distinction may
|
||||
not be relevant to redfish. Additionally, the Lenovo XCC makes certain
|
||||
information available over IPMI that is not otherwise available (for example
|
||||
the FPGA version where applicable).
|
||||
|
||||
In the update form, it accepts a single file and attempts to update it using
|
||||
the out of band facilities. Firmware updates can end in one of three states:
|
||||
|
||||
* `error`: The attempted update encountered an error that prevented successful install. Nodes experiencing this state will be reported at the end and more detail on the error will be given
|
||||
* `pending`: The firmware update process has completed, but the firmware will not be active until the relevant component next resets. Generally speaking, for UEFI update the system will need a reboot, and for BMC updates, the `nodebmcreset` command will begin the process to activate the firmware.
|
||||
|
||||
@@ -7,6 +7,7 @@ nodegroupattrib(8) -- List or change confluent nodegroup attributes
|
||||
`nodegroupattrib <group> [<nodeattribute>...]`
|
||||
`nodegroupattrib <group> [<nodeattribute1=value1> <nodeattribute2=value2> ...]`
|
||||
`nodegroupattrib <group> [-c] [<nodeattribute1> <nodeattribute2=value2> ...]`
|
||||
`nodeattrib -p <noderange> <nodeattribute1> <nodeattribute2> ...`
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
nodegroupdefine(8) -- Define new confluent node group
|
||||
===================================================================
|
||||
|
||||
## SYNOPSIS
|
||||
|
||||
@@ -6,7 +7,7 @@ nodegroupdefine(8) -- Define new confluent node group
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
`nodegroupdefine` allows the definition of a new node for the confluent management
|
||||
`nodegroupdefine` allows the definition of a new nodegroup for the confluent management
|
||||
service. It may only define a single group name at a time.
|
||||
It has the same syntax as `nodegroupattrib(8)`, and the commands differ in
|
||||
that `nodegroupattrib(8)` will error if a node group does not exist.
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
nodegrouplist(8) -- List the defined confluent nodegroups
|
||||
===================================================================
|
||||
|
||||
## SYNOPSIS
|
||||
|
||||
`nodegrouplist`
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
`nodegrouplist` lists the currently defined groups in confluent.
|
||||
|
||||
## SEE ALSO
|
||||
|
||||
nodeattrib(8), nodeattribexpressions(5), nodegroupattrib(8)
|
||||
@@ -3,7 +3,7 @@ nodeidentify(8) -- Control the identify LED of confluent nodes
|
||||
|
||||
## SYNOPSIS
|
||||
|
||||
`nodidentify <noderange> [on|off]`
|
||||
`nodidentify <noderange> [on|off|blink]`
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
@@ -13,6 +13,7 @@ options are supported:
|
||||
|
||||
* `on`: Turn on the identify LED
|
||||
* `off`: Turn off the identify LED
|
||||
* `blink`: Set the identify LED to blink (when supported by the system)
|
||||
|
||||
## EXAMPLES:
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
nodelicense(8) -- Manage license keys on BMC
|
||||
=================================================================
|
||||
|
||||
## SYNOPSIS
|
||||
|
||||
`nodelicense <noderange> [list|install <filename>|delete <license>|save <directory>]`
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
`nodelicense` manages license keys on supported BMCs. Without an argument, the command
|
||||
lists currently installed license. Using `delete` will remove the specified license name
|
||||
from th eBMC. The `save` subcommand will take the passed directory (which may be in the form
|
||||
of /path/to/{node}/ to have the node name substituted for each node) and back up installed licenses
|
||||
to that directory. The `install` command will take the specified filename and install. The filename
|
||||
argument may be of the form xcc_fod_0034_7X21{id.serial}.key to have the serial number substituted
|
||||
to allow unique licenses to be specified in a single command.
|
||||
|
||||
@@ -17,6 +17,9 @@ displayed. If `-b` is specified, it will also display information on
|
||||
how inherited and expression based attributes are defined. There is more
|
||||
information on node attributes in nodeattributes(5) man page.
|
||||
|
||||
Attributes may be specified by wildcard, for example `net.*switch` will report
|
||||
all attributes that begin with `net.` and end with `switch`.
|
||||
|
||||
## OPTIONS
|
||||
|
||||
* `-b`, `--blame`:
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
nodemedia(8) -- Manage server remote media
|
||||
=========================================================
|
||||
|
||||
## SYNOPSIS
|
||||
|
||||
`nodemedia <noderange> [attach|detachall|list|upload] [options] <media>`
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
**nodemedia** manages the remote media functionality of supported BMCs.
|
||||
|
||||
`list` shows all the current remote media the BMCs of the noderange are
|
||||
providing to the host platform. The string (insecure) is appended to URLs that
|
||||
are mounted in an insecure fashion. http is insecure, and https is also
|
||||
insecure when no meaningful certificate validation is performed. Currently
|
||||
there is no action that can change this, and this is purely informational. A
|
||||
future version of software may provide a means to increase security of attached
|
||||
remote media. If no media is mounted, this will provide no output, error
|
||||
conditions will result in output to standard error.
|
||||
|
||||
`detachall` removes all the currently provided media to the host. This unlinks
|
||||
remote media from urls and deletes uploaded media from the BMC.
|
||||
|
||||
`upload` takes the given media image and uploads it to the BMC. This causes
|
||||
the remote media to reside internally to the system without having to go
|
||||
to the network after the upload. This is more constrained, for example the
|
||||
Lenovo xClarity Controller has a limit of 50 megabytes, but it has zero ongoing
|
||||
load on the media source.
|
||||
|
||||
`attach` takes a URL to a remote media as an argument, and has the given
|
||||
BMCs map a virtual USB device to that url. Content is loaded on demand, and
|
||||
as such that URL is referenced potentially once for every IO operation that
|
||||
the host platform attempts.
|
||||
|
||||
## OPTIONS
|
||||
|
||||
* `-c`:
|
||||
Specify the maximum number of instances to run concurrently.
|
||||
|
||||
## EXAMPLES
|
||||
* Listing currently mounted media:
|
||||
`# nodemedia s1-s4 list`
|
||||
`s1: boot.img`
|
||||
`s2: boot.img`
|
||||
`s4: boot.img`
|
||||
|
||||
* Uploading a small boot image to the BMC:
|
||||
`# nodemedia s1-s4 upload boot.img`
|
||||
`s1:complete: 100% s2:complete: 100% s3:complete: 100% s4:complete: 100%`
|
||||
`s1: boot.img`
|
||||
`s4: boot.img`
|
||||
`s2: boot.img`
|
||||
|
||||
* Attaching a larger ISO for on-demand access:
|
||||
`# nodemedia s1,s4 attach http://172.30.0.6/install/rhel74.iso`
|
||||
`s4: http://172.30.0.6/install/rhel74.iso (insecure)`
|
||||
`s1: http://172.30.0.6/install/rhel74.iso (insecure)`
|
||||
@@ -4,7 +4,7 @@ nodepower(8) -- Check or change power state of confluent nodes
|
||||
## SYNOPSIS
|
||||
|
||||
`nodepower <noderange>`
|
||||
`nodepower <noderange> [on|off|boot|shutdown|reset|status]`
|
||||
`nodepower <noderange> [-p] [on|off|boot|shutdown|reset|status]`
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
@@ -26,6 +26,11 @@ respond.
|
||||
off will not react to this request.
|
||||
* `status`: Behave identically to having no argument passed at all.
|
||||
|
||||
## OPTIONS
|
||||
|
||||
* `-p`, '--showprevious':
|
||||
Show previous power state for all directives that may change power state.
|
||||
|
||||
## EXAMPLES
|
||||
* Get power state of nodes n1 through n4:
|
||||
`# nodepower n1-n4`
|
||||
|
||||
@@ -36,10 +36,13 @@ Also, regular expressions may be used to indicate nodes with names matching cert
|
||||
The other major noderange primitive is indicating nodes by some attribute value:
|
||||
`location.rack=7`
|
||||
|
||||
The attribute name may use a wildcard:
|
||||
`net.*switch=switch1`
|
||||
|
||||
Commas can be used to indicate multiple nodes, and can mix and match any of the above primitives. The following can be a valid single noderange, combining any and all members of each comma separated component
|
||||
`n1,n2,rack1,storage,location.rack=9,~s1..,n20-n30`
|
||||
|
||||
Exclusions can be done by prepending a ‘-‘ before a portion of a noderange:
|
||||
Exclusions can be done by prepending a '-' before a portion of a noderange:
|
||||
`rack1,-n2`
|
||||
`compute,-rack1`
|
||||
`compute,-location.row=12`
|
||||
@@ -51,7 +54,7 @@ To indicate nodes that match multiple selections at once (set intersection), the
|
||||
For complex expressions, () may be used to indicate order of expanding the noderange to be explicit
|
||||
`rack1,-(console.logging=full@compute)`
|
||||
|
||||
Noderange syntax can also indicate ‘pagination’, or separating the nodes into well defined chunks. > is used to indicate how many nodes to display at a time, and < is used to indicate how many nodes to skip into a noderange:
|
||||
Noderange syntax can also indicate 'pagination', or separating the nodes into well defined chunks. > is used to indicate how many nodes to display at a time, and < is used to indicate how many nodes to skip into a noderange:
|
||||
`rack1>3<6`
|
||||
|
||||
The above would show the seventh through ninth nodes of the rack1 group. Like all other noderange operations, this may be combined with any of the above, but must appear as the very last operation. Ordering is done with a natural sort.
|
||||
|
||||
@@ -48,6 +48,10 @@ themselves, see nodeshell(8).
|
||||
`n4: 01 10 00`
|
||||
`n2: 01 10 00`
|
||||
|
||||
|
||||
* If wanting to use literal {} in the command, they must be escaped by doubling:
|
||||
`# noderun n1-n4 "echo {node} | awk '{{print $1}}'"`
|
||||
|
||||
## SEE ALSO
|
||||
|
||||
nodeshell(8)
|
||||
|
||||
@@ -4,7 +4,7 @@ nodesetboot(8) -- Check or set next boot device for noderange
|
||||
## SYNOPSIS
|
||||
|
||||
`nodesetboot <noderange>`
|
||||
`nodesetboot [options] <noderang> [default|cd|network|setup|hd]`
|
||||
`nodesetboot [options] <noderange> [default|cd|network|setup|hd]`
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
@@ -30,7 +30,10 @@ control.
|
||||
* `-p`, `--persist`:
|
||||
For a system that supports it, mark the boot override to persist rather than
|
||||
be a one time change. Many systems do not support this functionality.
|
||||
|
||||
|
||||
* `-u`, `--uefi`:
|
||||
This flag does nothing, it is for command compatibility with xCAT's rsetboot
|
||||
|
||||
* `default`:
|
||||
Request a normal default boot with no particular device override
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ nodeshell(8) -- Execute command on many nodes in a noderange through ssh
|
||||
|
||||
## SYNOPSIS
|
||||
|
||||
`nodeshell <noderange> <command to execute on each node>`
|
||||
`nodeshell <noderange> [options] <command to execute on each node>`
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
@@ -12,6 +12,11 @@ accepts and interpolates confluent attribute expressions as documented in
|
||||
nodeattribexpressions(5). `nodeshell` provides stdout as stdout and stderr
|
||||
as stderr, unlike psh which combines all stdout and stderr into stdout.
|
||||
|
||||
## OPTIONS
|
||||
|
||||
* `-c`:
|
||||
Specify the maximum number of instances to run concurrently.
|
||||
|
||||
## EXAMPLES
|
||||
|
||||
* Running `echo hi` on for nodes:
|
||||
@@ -22,7 +27,10 @@ as stderr, unlike psh which combines all stdout and stderr into stdout.
|
||||
`n4: hi`
|
||||
|
||||
* Setting a new static ip address temporarily on secondary interface of four nodes:
|
||||
`# nodeshell n1-n4 ifconfig eth1 172.30.93.{n1}`
|
||||
`# nodeshell n1-n4 ifconfig eth1 172.30.93.{n1}`
|
||||
|
||||
* If wanting to use literal {} in the command, they must be escaped by doubling:
|
||||
`# nodeshell n1-n4 "ps | awk '{{print $1}}'"`
|
||||
|
||||
## SEE ALSO
|
||||
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
nodestorage(8) -- Examine/Modify storage configuration of a node
|
||||
============================================
|
||||
|
||||
## SYNOPSIS
|
||||
|
||||
`nodestorage <noderange> [show|create|delete] [options]`
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
`nodestorage` provides access to the remote storage configuration of
|
||||
the noderange.
|
||||
|
||||
## OPTIONS
|
||||
* `-r` **RAIDLEVEL**, `--raidlevel`=**RAIDLEVEL**:
|
||||
RAID level to use when creating an array
|
||||
|
||||
* `-d` **DISKS**, `--disks`=**DISKS**:
|
||||
Comma separated list of disks to use, or the word "rest" to
|
||||
indicate use of all available disks
|
||||
|
||||
* `-s` **SIZE**, `--size`=**SIZE**:
|
||||
Comma separated list of sizes to use when creating
|
||||
volumes. The sizes may be absolute size (e.g. 16gb),
|
||||
percentage (10%) or the word "rest" to use remaining
|
||||
capacity, default behavior is to use all capacity to
|
||||
make a volume
|
||||
* `-n` **NAME**, `--name`=**NAME**:
|
||||
Comma separated list of names to use when naming
|
||||
volumes, or selecting a volume for delete. Default
|
||||
behavior is to use implementation provided default
|
||||
|
||||
## EXAMPLES
|
||||
|
||||
* Deleting the volume `somedata`:
|
||||
`$ nodestorage d5 delete somedata`
|
||||
`Deleted: somedata`
|
||||
|
||||
* Creating a raid5 of 4 disks and a volume named `somedata`:
|
||||
`$ nodestorage d5 create -r 5 -d drive0,drive_1,drive_2,drive_3 -n somedata`
|
||||
`d5: Volume somedata: Size: 1.905 TB`
|
||||
`d5: Volume somedata: State: Optimal`
|
||||
`d5: Volume somedata: Array 1-2`
|
||||
|
||||
* Showing current storage configuration of `d3`:
|
||||
`$ nodestorage d3`
|
||||
`d3: Disk m.2-0 Description: 128GB M.2 SATA SSD`
|
||||
`d3: Disk m.2-0 State: online`
|
||||
`d3: Disk m.2-0 FRU: 00LF428`
|
||||
`d3: Disk m.2-0 Serial Number: H6B80054`
|
||||
`d3: Disk m.2-0 Array: 0-0`
|
||||
`d3: Disk m.2-1 Description: 128GB M.2 SATA SSD`
|
||||
`d3: Disk m.2-1 State: online`
|
||||
`d3: Disk m.2-1 FRU: 00LF428`
|
||||
`d3: Disk m.2-1 Serial Number: H6B80059`
|
||||
`d3: Disk m.2-1 Array: 0-0`
|
||||
`d3: Array 0-0 Available Capacity: 0.000 MB`
|
||||
`d3: Array 0-0 Total Capacity: 131.072 GB`
|
||||
`d3: Array 0-0 RAID: RAID 1`
|
||||
`d3: Array 0-0 Disks: m.2-0,m.2-1`
|
||||
`d3: Array 0-0 Volumes: new_vd`
|
||||
`d3: Volume new_vd: Size: 122.040 GB`
|
||||
`d3: Volume new_vd: State: Optimal`
|
||||
`d3: Volume new_vd: Array 0-0`
|
||||
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
nodesupport(8) -- Utilities for interacting with vendor support
|
||||
=================================================================
|
||||
|
||||
## SYNOPSIS
|
||||
|
||||
`nodesupport <noderange> servicedata <directory or filename>`
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
`nodesupport` provides capabilities associated with interacting with support.
|
||||
Currently it only has the `servicedata` subcommand. `servicedata` takes
|
||||
an argument that is either a directory name (that can be used for a single node
|
||||
or multiple nodes) or a file name (only to be used with single node noderange).
|
||||
Note that the file will be downloaded to the confluent server that actually
|
||||
connects to the managed system, so it will download to the remote system if running
|
||||
remotely and will download to the collective.manager indicated system if
|
||||
running in collective mode.
|
||||
|
||||
Note that due to vendor filename requirements, any filename may have vendor
|
||||
specific suffixes added to any file produced.
|
||||
|
||||
## EXAMPLES
|
||||
|
||||
* Download support data from a single node to a specific filename
|
||||
`# nodesupport d1 servicedata svcdata.out`
|
||||
`d1:initializing: 15%`
|
||||
|
||||
* Download support data from multiple nodes to a directory
|
||||
`# nodesupport d1-d4 servicedata service/`
|
||||
`d1:initializing: 0% d2:initializing: 0% d3:initializing: 0% d4:initializing: 0%`
|
||||
`# ls service/`
|
||||
`d1.svcdata d2.svcdata d3.svcdata d4.svcdata`
|
||||
@@ -0,0 +1,35 @@
|
||||
#!/usr/bin/python2
|
||||
|
||||
# This is a sample python script for going through all observed mac addresses
|
||||
# and assuming they are BMC related and printing nodeattrib commands
|
||||
# for each node to access the bmc using the interface specified on the command
|
||||
# line
|
||||
|
||||
# Not necessarily as useful if there may be mistakes in the
|
||||
# net.switch/net.switchport attributes, but a handy utility in a pinch when
|
||||
# you really know
|
||||
|
||||
|
||||
import confluent.client as cl
|
||||
import socket
|
||||
import struct
|
||||
c = cl.Command()
|
||||
macs = []
|
||||
interface = sys.argv[1]
|
||||
for mac in c.read('/networking/macs/by-mac/'):
|
||||
macs.append(mac['item']['href'])
|
||||
for mac in macs:
|
||||
macinfo = list(c.read('/networking/macs/by-mac/{0}'.format(mac)))[0]
|
||||
if 'possiblenode' in macinfo and macinfo['possiblenode']:
|
||||
if macinfo['macsonport'] > 1:
|
||||
print('#Ambiguous set of macs on port for ' + macinfo[
|
||||
'possiblenode'])
|
||||
prefix = int(mac.replace('-', '')[:6], 16) ^ 0b100000000000000000
|
||||
prefix = prefix << 8
|
||||
prefix |= 0xff
|
||||
suffix = int(mac.replace('-', '')[6:], 16)
|
||||
suffix |= 0xfe000000
|
||||
rawn = struct.pack('!QLL', 0xfe80000000000000, prefix, suffix)
|
||||
bmc = socket.inet_ntop(socket.AF_INET6, rawn)
|
||||
print('nodeattrib {0} bmc={1}%{2}'.format(macinfo['possiblenode'],
|
||||
bmc, interface))
|
||||
@@ -22,10 +22,10 @@ This provides the modules common for both client and server
|
||||
%setup -n %{name}-%{version} -n %{name}-%{version}
|
||||
|
||||
%build
|
||||
python setup.py build
|
||||
python2 setup.py build
|
||||
|
||||
%install
|
||||
python setup.py install --single-version-externally-managed -O1 --root=$RPM_BUILD_ROOT --record=INSTALLED_FILES --install-purelib=/opt/confluent/lib/python --install-scripts=/opt/confluent/bin
|
||||
python2 setup.py install --single-version-externally-managed -O1 --root=$RPM_BUILD_ROOT --record=INSTALLED_FILES --install-purelib=/opt/confluent/lib/python --install-scripts=/opt/confluent/bin
|
||||
|
||||
%clean
|
||||
rm -rf $RPM_BUILD_ROOT
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
[metadata]
|
||||
name = confluent_common
|
||||
summary = Confluent Common Libraries
|
||||
description-file =
|
||||
README.txt
|
||||
author = Jarrod Johnson
|
||||
author-email = jjohnson2@lenovo.com
|
||||
home-page = http://xcat.sf.net/
|
||||
classifier =
|
||||
Intended Audience :: Information Technology
|
||||
Intended Audience :: System Administrators
|
||||
License :: OSI Approved :: Apache Software License
|
||||
Operating System :: POSIX :: Linux
|
||||
Programming Language :: Python :: 2.6
|
||||
Programming Language :: Python :: 2.7
|
||||
|
||||
[files]
|
||||
@@ -1,3 +1,4 @@
|
||||
include pam/*
|
||||
include sysvinit/*
|
||||
include systemd/*
|
||||
include sysctl/*
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
#!/usr/bin/python2
|
||||
|
||||
import argparse
|
||||
import errno
|
||||
import os
|
||||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
path = os.path.dirname(os.path.realpath(__file__))
|
||||
path = os.path.realpath(os.path.join(path, '..', 'lib', 'python'))
|
||||
if path.startswith('/opt'):
|
||||
# if installed into system path, do not muck with things
|
||||
sys.path.append(path)
|
||||
|
||||
import confluent.client as client
|
||||
import confluent.tlvdata as tlvdata
|
||||
|
||||
try:
|
||||
input = raw_input
|
||||
except NameError:
|
||||
pass
|
||||
|
||||
def make_certificate():
|
||||
umask = os.umask(0o77)
|
||||
try:
|
||||
os.makedirs('/etc/confluent/cfg')
|
||||
except OSError as e:
|
||||
if e.errno == errno.EEXIST and os.path.isdir('/etc/confluent/cfg'):
|
||||
pass
|
||||
else:
|
||||
raise
|
||||
if subprocess.check_call(
|
||||
'openssl ecparam -name secp384r1 -genkey -out '
|
||||
'/etc/confluent/privkey.pem'.split(' ')):
|
||||
raise Exception('Error generating private key')
|
||||
|
||||
if subprocess.check_call('openssl req -new -x509 -key '
|
||||
'/etc/confluent/privkey.pem -days 7300 -out '
|
||||
'/etc/confluent/srvcert.pem -subj /CN='
|
||||
'{0}'.format(socket.gethostname()).split(' ')):
|
||||
raise Exception('Error generating certificate')
|
||||
print('Certificate generated successfully')
|
||||
os.umask(umask)
|
||||
|
||||
|
||||
def show_invitation(name):
|
||||
if not os.path.exists('/etc/confluent/srvcert.pem'):
|
||||
make_certificate()
|
||||
s = client.Command().connection
|
||||
tlvdata.send(s, {'collective': {'operation': 'invite', 'name': name}})
|
||||
invite = tlvdata.recv(s)['collective']
|
||||
if 'error' in invite:
|
||||
sys.stderr.write(invite['error'] + '\n')
|
||||
return
|
||||
print('{0}'.format(invite['invitation']))
|
||||
|
||||
|
||||
def join_collective(server, invitation):
|
||||
if not os.path.exists('/etc/confluent/srvcert.pem'):
|
||||
make_certificate()
|
||||
s = client.Command().connection
|
||||
while not invitation:
|
||||
invitation = input('Paste the invitation here: ')
|
||||
tlvdata.send(s, {'collective': {'operation': 'join',
|
||||
'invitation': invitation,
|
||||
'server': server}})
|
||||
res = tlvdata.recv(s)
|
||||
res = res.get('collective',
|
||||
{'status': 'Unknown response: ' + repr(res)})
|
||||
print(res.get('status', res.get('error', repr(res))))
|
||||
if 'error' in res:
|
||||
sys.exit(1)
|
||||
|
||||
def delete_member(name):
|
||||
s = client.Command().connection
|
||||
tlvdata.send(s, {'collective': {'operation': 'delete',
|
||||
'member': name}})
|
||||
res = tlvdata.recv(s)
|
||||
res = res.get('collective',
|
||||
{'status': 'Unknown response: ' + repr(res)})
|
||||
print(res.get('status', res.get('error', repr(res))))
|
||||
if 'error' in res:
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def show_collective():
|
||||
s = client.Command().connection
|
||||
tlvdata.send(s, {'collective': {'operation': 'show'}})
|
||||
res = tlvdata.recv(s)
|
||||
if 'error' in res['collective']:
|
||||
print(res['collective']['error'])
|
||||
return
|
||||
if 'quorum' in res['collective']:
|
||||
print('Quorum: {0}'.format(res['collective']['quorum']))
|
||||
print('Leader: {0}'.format(res['collective']['leader']))
|
||||
if 'active' in res['collective']:
|
||||
if res['collective']['active']:
|
||||
print('Active collective members:')
|
||||
for member in res['collective']['active']:
|
||||
print(' {0}'.format(member))
|
||||
if res['collective']['offline']:
|
||||
print('Offline collective members:')
|
||||
for member in res['collective']['offline']:
|
||||
print(' {0}'.format(member))
|
||||
else:
|
||||
print('Run collective show on leader for more data')
|
||||
|
||||
def main():
|
||||
a = argparse.ArgumentParser(description='Confluent server utility')
|
||||
sp = a.add_subparsers(dest='command')
|
||||
gc = sp.add_parser('gencert', help='Generate Confluent Certificates for '
|
||||
'collective mode and remote CLI access')
|
||||
sl = sp.add_parser('show', help='Show information about the collective')
|
||||
ic = sp.add_parser('invite', help='Generate a invitation to allow a new '
|
||||
'confluent instance to join as a '
|
||||
'collective member. Run collective invite -h for more information')
|
||||
ic.add_argument('name', help='Name of server to invite to join the '
|
||||
'collective')
|
||||
dc = sp.add_parser('delete', help='Delete a member of a collective')
|
||||
dc.add_argument('name', help='Name of server to delete from collective')
|
||||
jc = sp.add_parser('join', help='Join a collective. Run collective join -h for more information')
|
||||
jc.add_argument('server', help='Existing collective member that ran invite and generated a token')
|
||||
jc.add_argument('-i', help='Invitation provided by runniing invite on an '
|
||||
'existing collective member')
|
||||
cmdset = a.parse_args()
|
||||
if cmdset.command == 'gencert':
|
||||
make_certificate()
|
||||
elif cmdset.command == 'invite':
|
||||
show_invitation(cmdset.name)
|
||||
elif cmdset.command == 'join':
|
||||
join_collective(cmdset.server, cmdset.i)
|
||||
elif cmdset.command == 'show':
|
||||
show_collective()
|
||||
elif cmdset.command == 'delete':
|
||||
delete_member(cmdset.name)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python
|
||||
#!/usr/bin/python2
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2014 IBM Corporation
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
from os.path import exists
|
||||
import shutil
|
||||
import socket
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
def get_openssl_conf_location():
|
||||
if exists('/etc/pki/tls/openssl.cnf'):
|
||||
return '/etc/pki/tls/openssl.cnf'
|
||||
elif exists('/etc/ssl/openssl.cnf');
|
||||
return '/etc/ssl/openssl.cnf'
|
||||
else:
|
||||
raise Exception("Cannot find openssl config file")
|
||||
|
||||
def get_ip_addresses():
|
||||
lines = subprocess.check_output('ip addr'.split(' '))
|
||||
for line in lines.split('\n'):
|
||||
if line.startswith(' inet6 '):
|
||||
line = line.replace(' inet6 ', '').split('/')[0]
|
||||
if line.startswith('fe80::'):
|
||||
continue
|
||||
if line == '::1':
|
||||
continue
|
||||
elif line.startswith(' inet '):
|
||||
line = line.replace(' inet ', '').split('/')[0]
|
||||
if line == '127.0.0.1':
|
||||
continue
|
||||
if line.startswith('169.254.'):
|
||||
continue
|
||||
else:
|
||||
continue
|
||||
yield line
|
||||
|
||||
def create_certificate():
|
||||
shortname = socket.gethostname().split('.')[0]
|
||||
longname = socket.getfqdn()
|
||||
subprocess.check_call(
|
||||
'openssl ecparam -name secp384r1 -genkey -out privkey.pem'.split(' '))
|
||||
san = ['IP:{0}'.format(x) for x in get_ip_addresses()]
|
||||
san.append('DNS:{0}'.format(shortname))
|
||||
san.append('DNS:{0}'.format(longname))
|
||||
san = ','.join(san)
|
||||
sslcfg = get_openssl_conf_location()
|
||||
tmpconfig = tempfile.mktemp()
|
||||
shutil.copy2(sslcfg, tmpconfig)
|
||||
with open(tmpconfig, 'a') as cfgfile:
|
||||
cfgfile.write('\n[SAN]\nsubjectAltName={0}'.format(san))
|
||||
subprocess.check_call(
|
||||
'openssl req -new -x509 -key privkey.pem -days 7300 -out cert.pem '
|
||||
'-subj /CN={0} -extensions SAN '
|
||||
'-config {1}'.format(longname, tmpconfig).split(' ')
|
||||
)
|
||||
|
||||
if __name__ == '__main__':
|
||||
create_certificate()
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python
|
||||
#!/usr/bin/python2
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2017 Lenovo
|
||||
@@ -16,6 +16,7 @@
|
||||
# limitations under the License.
|
||||
|
||||
|
||||
import getpass
|
||||
import optparse
|
||||
import sys
|
||||
import os
|
||||
@@ -32,6 +33,8 @@ argparser = optparse.OptionParser(
|
||||
usage="Usage: %prog [options] [dump|restore] [path]")
|
||||
argparser.add_option('-p', '--password',
|
||||
help='Password to use to protect/unlock a protected dump')
|
||||
argparser.add_option('-i', '--interactivepassword', help='Prompt for password',
|
||||
action='store_true')
|
||||
argparser.add_option('-r', '--redact', action='store_true',
|
||||
help='Redact potentially sensitive data rather than store')
|
||||
argparser.add_option('-u', '--unprotected', action='store_true',
|
||||
@@ -39,6 +42,14 @@ argparser.add_option('-u', '--unprotected', action='store_true',
|
||||
' the key information. Fields will be encrypted, '
|
||||
'but keys.json will contain unencrypted decryption'
|
||||
' keys that may be used to read the dump')
|
||||
argparser.add_option('-s', '--skipkeys', action='store_true',
|
||||
help='This specifies to dump the encrypted data without '
|
||||
'dumping the keys needed to decrypt it. This is '
|
||||
'suitable for an automated incremental backup, '
|
||||
'where an earlier password protected dump has a '
|
||||
'protected keys.json file, and only the protected '
|
||||
'data is needed. keys do not change and as such '
|
||||
'they do not require incremental backup')
|
||||
(options, args) = argparser.parse_args()
|
||||
if len(args) != 2 or args[0] not in ('dump', 'restore'):
|
||||
argparser.print_help()
|
||||
@@ -51,17 +62,32 @@ if args[0] == 'restore':
|
||||
if pid is not None:
|
||||
print("Confluent is running, must shut down to restore db")
|
||||
sys.exit(1)
|
||||
cfm.restore_db_from_directory(dumpdir, options.password)
|
||||
elif args[0] == 'dump':
|
||||
if options.password is None and not (options.unprotected or options.redact):
|
||||
print("Must indicate a password to protect or -u to opt opt of "
|
||||
"secure value protection or -r to skip all protected data")
|
||||
try:
|
||||
cfm.restore_db_from_directory(dumpdir, options.password)
|
||||
except Exception as e:
|
||||
print(str(e))
|
||||
sys.exit(1)
|
||||
os.umask(077)
|
||||
elif args[0] == 'dump':
|
||||
password = options.password
|
||||
if not password and options.interactivepassword:
|
||||
passcfm = None
|
||||
while passcfm is None or password != passcfm:
|
||||
password = getpass.getpass(
|
||||
'Enter password to protect the backup: ')
|
||||
passcfm = getpass.getpass('Confirm password to protect the backup: ')
|
||||
if password is None and not (options.unprotected or options.redact
|
||||
or options.skipkeys):
|
||||
print("Must indicate a password to protect or -u to opt opt of "
|
||||
"secure value protection or -r to redact sensitive information, "
|
||||
"or -s to do encrypted backup that requires keys.json from "
|
||||
"another backup to restore.")
|
||||
sys.exit(1)
|
||||
os.umask(0o77)
|
||||
main._initsecurity(conf.get_config())
|
||||
if not os.path.exists(dumpdir):
|
||||
os.makedirs(dumpdir)
|
||||
cfm.dump_db_to_directory(dumpdir, options.password, options.redact)
|
||||
cfm.dump_db_to_directory(dumpdir, options.password, options.redact,
|
||||
options.skipkeys)
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python
|
||||
#!/usr/bin/python2
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2014 IBM Corporation
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
#!/bin/bash
|
||||
umask 0077
|
||||
openssl ecparam -name secp384r1 -genkey -out /etc/confluent/privkey.pem
|
||||
openssl req -new -x509 -key /etc/confluent/privkey.pem -days 760 -out /etc/confluent/srvcert.pem -subj /CN=$(hostname)
|
||||
Executable
+64
@@ -0,0 +1,64 @@
|
||||
#!/bin/bash
|
||||
cd `dirname $0`
|
||||
PKGNAME=$(basename $(pwd))
|
||||
DPKGNAME=$(basename $(pwd) | sed -e s/_/-/)
|
||||
OPKGNAME=$(basename $(pwd) | sed -e s/_/-/)
|
||||
if grep wheezy /etc/os-release; then
|
||||
DPKGNAME=python-$DPKGNAME
|
||||
fi
|
||||
cd ..
|
||||
mkdir -p /tmp/confluent # $DPKGNAME
|
||||
cp -a * .git /tmp/confluent # $DPKGNAME
|
||||
cd /tmp/confluent/$PKGNAME
|
||||
if [ -x ./makeman ]; then
|
||||
./makeman
|
||||
fi
|
||||
./makesetup
|
||||
VERSION=`cat VERSION`
|
||||
cat > setup.cfg << EOF
|
||||
[install]
|
||||
install-purelib=/opt/confluent/lib/python
|
||||
install-scripts=/opt/confluent/bin
|
||||
|
||||
[sdist_dsc]
|
||||
package=$DPKGNAME
|
||||
EOF
|
||||
|
||||
python setup.py sdist > /dev/null 2>&1
|
||||
py2dsc dist/*.tar.gz
|
||||
shopt -s extglob
|
||||
cd deb_dist/!(*.orig)/
|
||||
if [ "$OPKGNAME" = "confluent-server" ]; then
|
||||
if grep wheezy /etc/os-release; then
|
||||
sed -i 's/^\(Depends:.*\)/\1, python-confluent-client, python-lxml, python-eficompressor, python-pycryptodomex, python-dateutil, python-pyopenssl/' debian/control
|
||||
else
|
||||
sed -i 's/^\(Depends:.*\)/\1, confluent-client, python-lxml, python-eficompressor, python-pycryptodome, python-dateutil, python-websocket/' debian/control
|
||||
fi
|
||||
if grep wheezy /etc/os-release; then
|
||||
echo 'confluent_client python-confluent-client' >> debian/pydist-overrides
|
||||
else
|
||||
echo 'confluent_client confluent-client' >> debian/pydist-overrides
|
||||
fi
|
||||
fi
|
||||
head -n -1 debian/control > debian/control1
|
||||
mv debian/control1 debian/control
|
||||
echo 'export PYBUILD_INSTALL_ARGS=--install-lib=/opt/confluent/lib/python' >> debian/rules
|
||||
#echo 'Provides: python-'$DPKGNAME >> debian/control
|
||||
#echo 'Conflicts: python-'$DPKGNAME >> debian/control
|
||||
#echo 'Replaces: python-'$DPKGNAME' (<<2)' >> debian/control
|
||||
#echo 'Breaks: python-'$DPKGNAME' (<<2)' >> debian/control
|
||||
|
||||
dpkg-buildpackage -rfakeroot -uc -us -i
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "[ERROR] rpmbuild returned non-zero, run: rpmbuild -ba ~/rpmbuild/SPECS/$PKGNAME.spec"
|
||||
exit 1
|
||||
else
|
||||
cd -
|
||||
# Clean up the generated files in this directory
|
||||
rm -rf $PKGNAME.egg-info dist setup.py
|
||||
rm -rf $(find deb_dist -mindepth 1 -maxdepth 1 -type d)
|
||||
if [ ! -z "$1" ]; then
|
||||
mv deb_dist/* $1/
|
||||
fi
|
||||
fi
|
||||
exit 0
|
||||
@@ -6,7 +6,7 @@ fi
|
||||
./makesetup
|
||||
VERSION=`cat VERSION`
|
||||
PKGNAME=$(basename $(pwd))
|
||||
python setup.py sdist > /dev/null 2>&1
|
||||
python3 setup.py sdist > /dev/null 2>&1
|
||||
cp dist/*.tar.gz ~/rpmbuild/SOURCES
|
||||
sed -e 's/#VERSION#/'$VERSION/ $PKGNAME.spec.tmpl > ~/rpmbuild/SPECS/$PKGNAME.spec
|
||||
rpmbuild -ba ~/rpmbuild/SPECS/$PKGNAME.spec 2> /dev/null |grep ^Wrote:
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
__version__ = "1.5.0.dev395.ggacb3a20"
|
||||
@@ -1,6 +1,6 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2016 Lenovo
|
||||
# Copyright 2016-2018 Lenovo
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -83,6 +83,8 @@ class AsyncSession(object):
|
||||
self.reaper = eventlet.spawn_after(15, self.destroy)
|
||||
|
||||
def add(self, requestid, rsp):
|
||||
if self.responses is None:
|
||||
return
|
||||
self.responses.append((requestid, rsp))
|
||||
if self._evt:
|
||||
self._evt.send()
|
||||
@@ -107,6 +109,7 @@ class AsyncSession(object):
|
||||
for console in self.consoles:
|
||||
_consolesessions[console]['session'].destroy()
|
||||
self.consoles = None
|
||||
self.responses = None
|
||||
del _asyncsessions[self.asyncid]
|
||||
|
||||
def run_handler(self, handler, requestid):
|
||||
@@ -166,6 +169,9 @@ def handle_async(env, querydict, threadset):
|
||||
currsess = AsyncSession()
|
||||
yield messages.AsyncSession(currsess.asyncid)
|
||||
return
|
||||
if querydict['asyncid'] not in _asyncsessions:
|
||||
raise exc.InvalidArgumentException(
|
||||
'Invalid or expired async id')
|
||||
mythreadid = greenlet.getcurrent()
|
||||
threadset.add(mythreadid)
|
||||
loggedout = None
|
||||
|
||||
@@ -22,12 +22,16 @@
|
||||
import confluent.config.configmanager as configmanager
|
||||
import eventlet
|
||||
import eventlet.tpool
|
||||
import Crypto.Protocol.KDF as KDF
|
||||
import Cryptodome.Protocol.KDF as KDF
|
||||
from fnmatch import fnmatch
|
||||
import hashlib
|
||||
import hmac
|
||||
import multiprocessing
|
||||
import confluent.userutil as userutil
|
||||
import confluent.util as util
|
||||
pam = None
|
||||
try:
|
||||
import PAM
|
||||
import confluent.pam as pam
|
||||
except ImportError:
|
||||
pass
|
||||
import time
|
||||
@@ -39,24 +43,59 @@ _passchecking = {}
|
||||
authworkers = None
|
||||
authcleaner = None
|
||||
|
||||
_allowedbyrole = {
|
||||
'Operator': {
|
||||
'retrieve': ['*'],
|
||||
'create': [
|
||||
'/noderange/',
|
||||
'/nodes/',
|
||||
'/node*/media/uploads/',
|
||||
'/node*/inventory/firmware/updates/*',
|
||||
'/node*/suppport/servicedata*',
|
||||
'/node*/attributes/expression',
|
||||
'/nodes/*/console/session*',
|
||||
'/nodes/*/shell/sessions*',
|
||||
'/node*/configuration/*',
|
||||
],
|
||||
'update': [
|
||||
'/discovery/*',
|
||||
'/networking/macs/rescan',
|
||||
'/node*/power/state',
|
||||
'/node*/power/reseat',
|
||||
'/node*/attributes/*',
|
||||
'/node*/media/*tach',
|
||||
'/node*/boot/nextdevice',
|
||||
'/node*/identify',
|
||||
'/node*/configuration/*',
|
||||
],
|
||||
'start': [
|
||||
'/nodes/*/console/session*',
|
||||
'/nodes/*/shell/sessions*',
|
||||
],
|
||||
'delete': [
|
||||
'/discovery/*',
|
||||
'/node*',
|
||||
],
|
||||
},
|
||||
'Monitor': {
|
||||
'retrieve': [
|
||||
'/node*/health/hardware',
|
||||
'/node*/power/state',
|
||||
'/node*/sensors/*',
|
||||
'/nodes/',
|
||||
'/',
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
class Credentials(object):
|
||||
def __init__(self, username, passphrase):
|
||||
self.username = username
|
||||
self.passphrase = passphrase
|
||||
self.haspam = False
|
||||
|
||||
def pam_conv(self, auth, query_list):
|
||||
# use stored credentials in a pam conversation
|
||||
self.haspam = True
|
||||
resp = []
|
||||
for query_entry in query_list:
|
||||
query, pamtype = query_entry
|
||||
if query.startswith('Password'):
|
||||
resp.append((self.passphrase, 0))
|
||||
else:
|
||||
return None
|
||||
return resp
|
||||
_deniedbyrole = {
|
||||
# This supersedes the above and is only consulted after the allowed has happened
|
||||
'Operator': {
|
||||
'update': [
|
||||
'/node*/configuration/management_controller/users/*',
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def _prune_passcache():
|
||||
@@ -65,9 +104,9 @@ def _prune_passcache():
|
||||
while True:
|
||||
curtime = time.time()
|
||||
for passent in _passcache.iterkeys():
|
||||
if passent[2] < curtime - 10:
|
||||
if passent[2] < curtime - 90:
|
||||
del _passcache[passent]
|
||||
eventlet.sleep(10)
|
||||
eventlet.sleep(90)
|
||||
|
||||
|
||||
def _get_usertenant(name, tenant=False):
|
||||
@@ -81,11 +120,13 @@ def _get_usertenant(name, tenant=False):
|
||||
administrator account a tenant gets.
|
||||
Otherwise, just assume a user in the default tenant
|
||||
"""
|
||||
if not isinstance(name, bytes):
|
||||
name = name.encode('utf-8')
|
||||
if not isinstance(tenant, bool):
|
||||
# if not boolean, it must be explicit tenant
|
||||
user = name
|
||||
elif '/' in name: # tenant scoped name
|
||||
tenant, user = name.split('/', 1)
|
||||
elif b'/' in name: # tenant scoped name
|
||||
tenant, user = name.split(b'/', 1)
|
||||
elif configmanager.is_tenant(name):
|
||||
# the account is the implicit tenant owner account
|
||||
user = name
|
||||
@@ -93,6 +134,9 @@ def _get_usertenant(name, tenant=False):
|
||||
else: # assume it is a non-tenant user account
|
||||
user = name
|
||||
tenant = None
|
||||
user = util.stringify(user)
|
||||
if tenant:
|
||||
tenant = util.stringify(tenant)
|
||||
yield user
|
||||
yield tenant
|
||||
|
||||
@@ -112,21 +156,37 @@ def authorize(name, element, tenant=False, operation='create',
|
||||
and the relevant ConfigManager object for the context of the
|
||||
request.
|
||||
"""
|
||||
if operation not in ('create', 'start', 'update', 'retrieve', 'delete'):
|
||||
return None
|
||||
# skipuserobj is a leftover from the now abandoned plan to use pam session
|
||||
# to do authorization and authentication. Now confluent always does authorization
|
||||
# even if pam does authentication.
|
||||
if operation not in ('create', 'start', 'update', 'retrieve', 'delete', None):
|
||||
return False
|
||||
user, tenant = _get_usertenant(name, tenant)
|
||||
if tenant is not None and not configmanager.is_tenant(tenant):
|
||||
return None
|
||||
return False
|
||||
manager = configmanager.ConfigManager(tenant, username=user)
|
||||
if skipuserobj:
|
||||
return None, manager, user, tenant, skipuserobj
|
||||
userobj = manager.get_user(user)
|
||||
if not userobj:
|
||||
for group in userutil.grouplist(user):
|
||||
userobj = manager.get_usergroup(group)
|
||||
if userobj:
|
||||
break
|
||||
if userobj: # returning
|
||||
role = userobj.get('role', 'Administrator')
|
||||
if element and role != 'Administrator':
|
||||
for rule in _allowedbyrole.get(role, {}).get(operation, []):
|
||||
if fnmatch(element, rule):
|
||||
break
|
||||
else:
|
||||
return False
|
||||
for rule in _deniedbyrole.get(role, {}).get(operation, []):
|
||||
if fnmatch(element, rule):
|
||||
return False
|
||||
return userobj, manager, user, tenant, skipuserobj
|
||||
return None
|
||||
return False
|
||||
|
||||
|
||||
def check_user_passphrase(name, passphrase, element=None, tenant=False):
|
||||
def check_user_passphrase(name, passphrase, operation=None, element=None, tenant=False):
|
||||
"""Check a a login name and passphrase for authenticity and authorization
|
||||
|
||||
The function combines authentication and authorization into one function.
|
||||
@@ -157,63 +217,66 @@ def check_user_passphrase(name, passphrase, element=None, tenant=False):
|
||||
# would normally make an event and wait
|
||||
# but here there's no need for that
|
||||
eventlet.sleep(0.5)
|
||||
credobj = Credentials(user, passphrase)
|
||||
try:
|
||||
pammy = PAM.pam()
|
||||
pammy.start(_pamservice, user, credobj.pam_conv)
|
||||
pammy.authenticate()
|
||||
pammy.acct_mgmt()
|
||||
del pammy
|
||||
return authorize(user, element, tenant, skipuserobj=False)
|
||||
except NameError:
|
||||
pass
|
||||
except PAM.error:
|
||||
if credobj.haspam:
|
||||
return None
|
||||
cfm = configmanager.ConfigManager(tenant, username=user)
|
||||
ucfg = cfm.get_user(user)
|
||||
if ucfg is None:
|
||||
try:
|
||||
for group in userutil.grouplist(user):
|
||||
ucfg = cfm.get_usergroup(group)
|
||||
if ucfg:
|
||||
break
|
||||
except KeyError:
|
||||
pass
|
||||
if ucfg is None:
|
||||
eventlet.sleep(0.05)
|
||||
return None
|
||||
if (user, tenant) in _passcache:
|
||||
if passphrase == _passcache[(user, tenant)]:
|
||||
return authorize(user, element, tenant)
|
||||
if hashlib.sha256(passphrase).digest() == _passcache[(user, tenant)]:
|
||||
return authorize(user, element, tenant, operation=operation)
|
||||
else:
|
||||
# In case of someone trying to guess,
|
||||
# while someone is legitimately logged in
|
||||
# invalidate cache and force the slower check
|
||||
del _passcache[(user, tenant)]
|
||||
return None
|
||||
cfm = configmanager.ConfigManager(tenant, username=user)
|
||||
ucfg = cfm.get_user(user)
|
||||
if ucfg is None or 'cryptpass' not in ucfg:
|
||||
eventlet.sleep(0.05) # stall even on test for existence of a username
|
||||
return None
|
||||
_passchecking[(user, tenant)] = True
|
||||
# TODO(jbjohnso): WORKERPOOL
|
||||
# PBKDF2 is, by design, cpu intensive
|
||||
# throw it at the worker pool when implemented
|
||||
# maybe a distinct worker pool, wondering about starving out non-auth stuff
|
||||
salt, crypt = ucfg['cryptpass']
|
||||
# execute inside tpool to get greenthreads to give it a special thread
|
||||
# world
|
||||
#TODO(jbjohnso): util function to generically offload a call
|
||||
#such a beast could be passed into pyghmi as a way for pyghmi to
|
||||
#magically get offload of the crypto functions without having
|
||||
#to explicitly get into the eventlet tpool game
|
||||
global authworkers
|
||||
global authcleaner
|
||||
if authworkers is None:
|
||||
authworkers = multiprocessing.Pool(processes=1)
|
||||
else:
|
||||
authcleaner.cancel()
|
||||
authcleaner = eventlet.spawn_after(30, _clean_authworkers)
|
||||
crypted = eventlet.tpool.execute(_do_pbkdf, passphrase, salt)
|
||||
del _passchecking[(user, tenant)]
|
||||
eventlet.sleep(0.05) # either way, we want to stall so that client can't
|
||||
if 'cryptpass' in ucfg:
|
||||
_passchecking[(user, tenant)] = True
|
||||
# TODO(jbjohnso): WORKERPOOL
|
||||
# PBKDF2 is, by design, cpu intensive
|
||||
# throw it at the worker pool when implemented
|
||||
# maybe a distinct worker pool, wondering about starving out non-auth stuff
|
||||
salt, crypt = ucfg['cryptpass']
|
||||
# execute inside tpool to get greenthreads to give it a special thread
|
||||
# world
|
||||
# TODO(jbjohnso): util function to generically offload a call
|
||||
# such a beast could be passed into pyghmi as a way for pyghmi to
|
||||
# magically get offload of the crypto functions without having
|
||||
# to explicitly get into the eventlet tpool game
|
||||
global authworkers
|
||||
global authcleaner
|
||||
if authworkers is None:
|
||||
authworkers = multiprocessing.Pool(processes=1)
|
||||
else:
|
||||
authcleaner.cancel()
|
||||
authcleaner = eventlet.spawn_after(30, _clean_authworkers)
|
||||
crypted = eventlet.tpool.execute(_do_pbkdf, passphrase, salt)
|
||||
del _passchecking[(user, tenant)]
|
||||
eventlet.sleep(
|
||||
0.05) # either way, we want to stall so that client can't
|
||||
# determine failure because there is a delay, valid response will
|
||||
# delay as well
|
||||
if crypt == crypted:
|
||||
_passcache[(user, tenant)] = passphrase
|
||||
return authorize(user, element, tenant)
|
||||
if crypt == crypted:
|
||||
_passcache[(user, tenant)] = hashlib.sha256(passphrase).digest()
|
||||
return authorize(user, element, tenant, operation)
|
||||
if pam:
|
||||
pammy = pam.pam()
|
||||
usergood = pammy.authenticate(user, passphrase)
|
||||
del pammy
|
||||
if usergood:
|
||||
_passcache[(user, tenant)] = hashlib.sha256(passphrase).digest()
|
||||
return authorize(user, element, tenant, operation, skipuserobj=False)
|
||||
eventlet.sleep(0.05) # stall even on test for existence of a username
|
||||
return None
|
||||
|
||||
|
||||
def _apply_pbkdf(passphrase, salt):
|
||||
return KDF.PBKDF2(passphrase, salt, 32, 10000,
|
||||
lambda p, s: hmac.new(p, s, hashlib.sha256).digest())
|
||||
@@ -231,4 +294,4 @@ def _do_pbkdf(passphrase, salt):
|
||||
# compute. However, we do want to wait for result, so we have
|
||||
# one of the exceedingly rare sort of circumstances where 'apply'
|
||||
# actually makes sense
|
||||
return authworkers.apply(_apply_pbkdf, [passphrase, salt])
|
||||
return authworkers.apply(_apply_pbkdf, [passphrase, salt])
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2018 Lenovo
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# This handles the process of generating and tracking/validating invites
|
||||
|
||||
import base64
|
||||
import hashlib
|
||||
import hmac
|
||||
import os
|
||||
pending_invites = {}
|
||||
|
||||
def create_server_invitation(servername):
|
||||
servername = servername.encode('utf-8')
|
||||
randbytes = (3 - ((len(servername) + 2) % 3)) % 3 + 64
|
||||
invitation = os.urandom(randbytes)
|
||||
pending_invites[servername] = invitation
|
||||
return base64.b64encode(servername + b'@' + invitation)
|
||||
|
||||
def create_client_proof(invitation, mycert, peercert):
|
||||
return hmac.new(invitation, peercert + mycert, hashlib.sha256).digest()
|
||||
|
||||
def check_server_proof(invitation, mycert, peercert, proof):
|
||||
validproof = hmac.new(invitation, mycert + peercert, hashlib.sha256
|
||||
).digest()
|
||||
return proof == validproof
|
||||
|
||||
def check_client_proof(servername, mycert, peercert, proof):
|
||||
servername = servername.encode('utf-8')
|
||||
if servername not in pending_invites:
|
||||
return False
|
||||
invitation = pending_invites[servername]
|
||||
validproof = hmac.new(invitation, mycert + peercert, hashlib.sha256
|
||||
).digest()
|
||||
if proof == validproof:
|
||||
# We know that the client knew the secret, and that it measured our
|
||||
# certificate, and thus calling code can bless the certificate, and
|
||||
# we can forget the invitation
|
||||
del pending_invites[servername]
|
||||
# We now want to prove to the client that we also know the secret,
|
||||
# and that we measured their certificate well
|
||||
# Now to generate an answer...., reverse the cert order so our answer
|
||||
# is different, but still proving things
|
||||
return hmac.new(invitation, peercert + mycert, hashlib.sha256
|
||||
).digest()
|
||||
# The given proof did not verify the invitation
|
||||
return False
|
||||
|
||||
@@ -0,0 +1,619 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2018 Lenovo
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import base64
|
||||
import confluent.collective.invites as invites
|
||||
import confluent.config.configmanager as cfm
|
||||
import confluent.exceptions as exc
|
||||
import confluent.log as log
|
||||
import confluent.tlvdata as tlvdata
|
||||
import confluent.util as util
|
||||
import eventlet
|
||||
import eventlet.green.socket as socket
|
||||
import eventlet.green.ssl as ssl
|
||||
import eventlet.green.threading as threading
|
||||
import greenlet
|
||||
import random
|
||||
import sys
|
||||
try:
|
||||
import OpenSSL.crypto as crypto
|
||||
except ImportError:
|
||||
# while not always required, we use pyopenssl required for at least
|
||||
# collective
|
||||
crypto = None
|
||||
|
||||
currentleader = None
|
||||
follower = None
|
||||
retrythread = None
|
||||
|
||||
class ContextBool(object):
|
||||
def __init__(self):
|
||||
self.active = False
|
||||
self.mylock = threading.RLock()
|
||||
|
||||
def __enter__(self):
|
||||
self.active = True
|
||||
self.mylock.__enter__()
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
self.active = False
|
||||
self.mylock.__exit__(exc_type, exc_val, exc_tb)
|
||||
|
||||
connecting = ContextBool()
|
||||
leader_init = ContextBool()
|
||||
|
||||
def connect_to_leader(cert=None, name=None, leader=None):
|
||||
global currentleader
|
||||
global follower
|
||||
if leader is None:
|
||||
leader = currentleader
|
||||
log.log({'info': 'Attempting connection to leader {0}'.format(leader),
|
||||
'subsystem': 'collective'})
|
||||
try:
|
||||
remote = connect_to_collective(cert, leader)
|
||||
except socket.error as e:
|
||||
log.log({'error': 'Collective connection attempt to {0} failed: {1}'
|
||||
''.format(leader, str(e)),
|
||||
'subsystem': 'collective'})
|
||||
return False
|
||||
with connecting:
|
||||
with cfm._initlock:
|
||||
banner = tlvdata.recv(remote) # the banner
|
||||
vers = banner.split()[2]
|
||||
pvers = 0
|
||||
reqver = 4
|
||||
if vers == b'v0':
|
||||
pvers = 2
|
||||
elif vers == b'v1':
|
||||
pvers = 4
|
||||
if sys.version_info[0] < 3:
|
||||
pvers = 2
|
||||
reqver = 2
|
||||
tlvdata.recv(remote) # authpassed... 0..
|
||||
if name is None:
|
||||
name = get_myname()
|
||||
tlvdata.send(remote, {'collective': {'operation': 'connect',
|
||||
'protover': reqver,
|
||||
'name': name,
|
||||
'txcount': cfm._txcount}})
|
||||
keydata = tlvdata.recv(remote)
|
||||
if not keydata:
|
||||
return False
|
||||
if 'error' in keydata:
|
||||
if 'backoff' in keydata:
|
||||
log.log({
|
||||
'info': 'Collective initialization in progress on '
|
||||
'{0}'.format(leader),
|
||||
'subsystem': 'collective'})
|
||||
return False
|
||||
if 'leader' in keydata:
|
||||
log.log(
|
||||
{'info': 'Prospective leader {0} has redirected this '
|
||||
'member to {1}'.format(leader, keydata['leader']),
|
||||
'subsystem': 'collective'})
|
||||
ldrc = cfm.get_collective_member_by_address(
|
||||
keydata['leader'])
|
||||
if ldrc and ldrc['name'] == name:
|
||||
raise Exception("Redirected to self")
|
||||
return connect_to_leader(name=name,
|
||||
leader=keydata['leader'])
|
||||
if 'txcount' in keydata:
|
||||
log.log({'info':
|
||||
'Prospective leader {0} has inferior '
|
||||
'transaction count, becoming leader'
|
||||
''.format(leader), 'subsystem': 'collective',
|
||||
'subsystem': 'collective'})
|
||||
return become_leader(remote)
|
||||
return False
|
||||
follower.kill()
|
||||
cfm.stop_following()
|
||||
follower = None
|
||||
if follower:
|
||||
follower.kill()
|
||||
cfm.stop_following()
|
||||
follower = None
|
||||
log.log({'info': 'Following leader {0}'.format(leader),
|
||||
'subsystem': 'collective'})
|
||||
colldata = tlvdata.recv(remote)
|
||||
globaldata = tlvdata.recv(remote)
|
||||
dbi = tlvdata.recv(remote)
|
||||
dbsize = dbi['dbsize']
|
||||
dbjson = b''
|
||||
while (len(dbjson) < dbsize):
|
||||
ndata = remote.recv(dbsize - len(dbjson))
|
||||
if not ndata:
|
||||
try:
|
||||
remote.close()
|
||||
except Exception:
|
||||
pass
|
||||
raise Exception("Error doing initial DB transfer")
|
||||
dbjson += ndata
|
||||
cfm.clear_configuration()
|
||||
try:
|
||||
cfm._restore_keys(keydata, None, sync=False)
|
||||
for c in colldata:
|
||||
cfm._true_add_collective_member(c, colldata[c]['address'],
|
||||
colldata[c]['fingerprint'],
|
||||
sync=False)
|
||||
for globvar in globaldata:
|
||||
cfm.set_global(globvar, globaldata[globvar], False)
|
||||
cfm._txcount = dbi.get('txcount', 0)
|
||||
cfm.ConfigManager(tenant=None)._load_from_json(dbjson,
|
||||
sync=False)
|
||||
cfm.commit_clear()
|
||||
except Exception:
|
||||
cfm.stop_following()
|
||||
cfm.rollback_clear()
|
||||
raise
|
||||
currentleader = leader
|
||||
#spawn this as a thread...
|
||||
follower = eventlet.spawn(follow_leader, remote, pvers, leader)
|
||||
return True
|
||||
|
||||
|
||||
def follow_leader(remote, proto, leader):
|
||||
global currentleader
|
||||
cleanexit = False
|
||||
try:
|
||||
cfm.follow_channel(remote, proto)
|
||||
except greenlet.GreenletExit:
|
||||
cleanexit = True
|
||||
finally:
|
||||
if cleanexit:
|
||||
log.log({'info': 'Previous following cleanly closed',
|
||||
'subsystem': 'collective'})
|
||||
return
|
||||
log.log({'info': 'Current leader ({0}) has disappeared, restarting '
|
||||
'collective membership'.format(leader), 'subsystem': 'collective'})
|
||||
# The leader has folded, time to startup again...
|
||||
cfm.stop_following()
|
||||
currentleader = None
|
||||
eventlet.spawn_n(start_collective)
|
||||
|
||||
|
||||
def connect_to_collective(cert, member):
|
||||
remote = socket.create_connection((member, 13001))
|
||||
# TLS cert validation is custom and will not pass normal CA vetting
|
||||
# to override completely in the right place requires enormous effort, so just defer until after connect
|
||||
remote = ssl.wrap_socket(remote, cert_reqs=ssl.CERT_NONE, keyfile='/etc/confluent/privkey.pem',
|
||||
certfile='/etc/confluent/srvcert.pem')
|
||||
if cert:
|
||||
fprint = cert
|
||||
else:
|
||||
collent = cfm.get_collective_member_by_address(member)
|
||||
fprint = collent['fingerprint']
|
||||
if not util.cert_matches(fprint, remote.getpeercert(binary_form=True)):
|
||||
# probably Janeway up to something
|
||||
raise Exception("Certificate mismatch in the collective")
|
||||
return remote
|
||||
|
||||
|
||||
def get_myname():
|
||||
try:
|
||||
with open('/etc/confluent/cfg/myname', 'r') as f:
|
||||
return f.read().strip()
|
||||
except IOError:
|
||||
myname = socket.gethostname()
|
||||
with open('/etc/confluent/cfg/myname', 'w') as f:
|
||||
f.write(myname)
|
||||
return myname
|
||||
|
||||
def handle_connection(connection, cert, request, local=False):
|
||||
global currentleader
|
||||
global retrythread
|
||||
operation = request['operation']
|
||||
if cert:
|
||||
cert = crypto.dump_certificate(crypto.FILETYPE_ASN1, cert)
|
||||
else:
|
||||
if not local:
|
||||
return
|
||||
if operation in ('show', 'delete'):
|
||||
if not list(cfm.list_collective()):
|
||||
tlvdata.send(connection,
|
||||
{'collective': {'error': 'Collective mode not '
|
||||
'enabled on this '
|
||||
'system'}})
|
||||
return
|
||||
if follower:
|
||||
linfo = cfm.get_collective_member_by_address(currentleader)
|
||||
remote = socket.create_connection((currentleader, 13001))
|
||||
remote = ssl.wrap_socket(remote, cert_reqs=ssl.CERT_NONE,
|
||||
keyfile='/etc/confluent/privkey.pem',
|
||||
certfile='/etc/confluent/srvcert.pem')
|
||||
cert = remote.getpeercert(binary_form=True)
|
||||
if not (linfo and util.cert_matches(
|
||||
linfo['fingerprint'],
|
||||
cert)):
|
||||
remote.close()
|
||||
tlvdata.send(connection,
|
||||
{'error': 'Invalid certificate, '
|
||||
'redo invitation process'})
|
||||
connection.close()
|
||||
return
|
||||
tlvdata.recv(remote) # ignore banner
|
||||
tlvdata.recv(remote) # ignore authpassed: 0
|
||||
tlvdata.send(remote,
|
||||
{'collective': {'operation': 'getinfo',
|
||||
'name': get_myname()}})
|
||||
collinfo = tlvdata.recv(remote)
|
||||
else:
|
||||
collinfo = {}
|
||||
populate_collinfo(collinfo)
|
||||
try:
|
||||
cfm.check_quorum()
|
||||
collinfo['quorum'] = True
|
||||
except exc.DegradedCollective:
|
||||
collinfo['quorum'] = False
|
||||
if operation == 'show':
|
||||
tlvdata.send(connection, {'collective': collinfo})
|
||||
elif operation == 'delete':
|
||||
todelete = request['member']
|
||||
if (todelete == collinfo['leader'] or
|
||||
todelete in collinfo['active']):
|
||||
tlvdata.send(connection, {'collective':
|
||||
{'error': '{0} is still active, stop the confluent service to remove it'.format(todelete)}})
|
||||
return
|
||||
if todelete not in collinfo['offline']:
|
||||
tlvdata.send(connection, {'collective':
|
||||
{'error': '{0} is not a recognized collective member'.format(todelete)}})
|
||||
return
|
||||
cfm.del_collective_member(todelete)
|
||||
tlvdata.send(connection,
|
||||
{'collective': {'status': 'Successfully deleted {0}'.format(todelete)}})
|
||||
connection.close()
|
||||
return
|
||||
if 'invite' == operation:
|
||||
try:
|
||||
cfm.check_quorum()
|
||||
except exc.DegradedCollective:
|
||||
tlvdata.send(connection,
|
||||
{'collective':
|
||||
{'error': 'Collective does not have quorum'}})
|
||||
return
|
||||
#TODO(jjohnson2): Cannot do the invitation if not the head node, the certificate hand-carrying
|
||||
#can't work in such a case.
|
||||
name = request['name']
|
||||
invitation = invites.create_server_invitation(name)
|
||||
tlvdata.send(connection,
|
||||
{'collective': {'invitation': invitation}})
|
||||
connection.close()
|
||||
if 'join' == operation:
|
||||
invitation = request['invitation']
|
||||
try:
|
||||
invitation = base64.b64decode(invitation)
|
||||
name, invitation = invitation.split(b'@', 1)
|
||||
name = util.stringify(name)
|
||||
except Exception:
|
||||
tlvdata.send(
|
||||
connection,
|
||||
{'collective':
|
||||
{'status': 'Invalid token format'}})
|
||||
connection.close()
|
||||
return
|
||||
host = request['server']
|
||||
try:
|
||||
remote = socket.create_connection((host, 13001))
|
||||
# This isn't what it looks like. We do CERT_NONE to disable
|
||||
# openssl verification, but then use the invitation as a
|
||||
# shared secret to validate the certs as part of the join
|
||||
# operation
|
||||
remote = ssl.wrap_socket(remote, cert_reqs=ssl.CERT_NONE,
|
||||
keyfile='/etc/confluent/privkey.pem',
|
||||
certfile='/etc/confluent/srvcert.pem')
|
||||
except Exception:
|
||||
tlvdata.send(
|
||||
connection,
|
||||
{'collective':
|
||||
{'status': 'Failed to connect to {0}'.format(host)}})
|
||||
connection.close()
|
||||
return
|
||||
mycert = util.get_certificate_from_file(
|
||||
'/etc/confluent/srvcert.pem')
|
||||
cert = remote.getpeercert(binary_form=True)
|
||||
proof = base64.b64encode(invites.create_client_proof(
|
||||
invitation, mycert, cert))
|
||||
tlvdata.recv(remote) # ignore banner
|
||||
tlvdata.recv(remote) # ignore authpassed: 0
|
||||
tlvdata.send(remote, {'collective': {'operation': 'enroll',
|
||||
'name': name, 'hmac': proof}})
|
||||
rsp = tlvdata.recv(remote)
|
||||
if 'error' in rsp:
|
||||
tlvdata.send(connection, {'collective':
|
||||
{'status': rsp['error']}})
|
||||
connection.close()
|
||||
return
|
||||
proof = rsp['collective']['approval']
|
||||
proof = base64.b64decode(proof)
|
||||
j = invites.check_server_proof(invitation, mycert, cert, proof)
|
||||
if not j:
|
||||
remote.close()
|
||||
tlvdata.send(connection, {'collective':
|
||||
{'status': 'Bad server token'}})
|
||||
connection.close()
|
||||
return
|
||||
tlvdata.send(connection, {'collective': {'status': 'Success'}})
|
||||
connection.close()
|
||||
currentleader = rsp['collective']['leader']
|
||||
f = open('/etc/confluent/cfg/myname', 'w')
|
||||
f.write(name)
|
||||
f.close()
|
||||
log.log({'info': 'Connecting to collective due to join',
|
||||
'subsystem': 'collective'})
|
||||
eventlet.spawn_n(connect_to_leader, rsp['collective'][
|
||||
'fingerprint'], name)
|
||||
if 'enroll' == operation:
|
||||
#TODO(jjohnson2): error appropriately when asked to enroll, but the master is elsewhere
|
||||
mycert = util.get_certificate_from_file('/etc/confluent/srvcert.pem')
|
||||
proof = base64.b64decode(request['hmac'])
|
||||
myrsp = invites.check_client_proof(request['name'], mycert,
|
||||
cert, proof)
|
||||
if not myrsp:
|
||||
tlvdata.send(connection, {'error': 'Invalid token'})
|
||||
connection.close()
|
||||
return
|
||||
myrsp = base64.b64encode(myrsp)
|
||||
fprint = util.get_fingerprint(cert)
|
||||
myfprint = util.get_fingerprint(mycert)
|
||||
cfm.add_collective_member(get_myname(),
|
||||
connection.getsockname()[0], myfprint)
|
||||
cfm.add_collective_member(request['name'],
|
||||
connection.getpeername()[0], fprint)
|
||||
myleader = get_leader(connection)
|
||||
ldrfprint = cfm.get_collective_member_by_address(
|
||||
myleader)['fingerprint']
|
||||
tlvdata.send(connection,
|
||||
{'collective': {'approval': myrsp,
|
||||
'fingerprint': ldrfprint,
|
||||
'leader': get_leader(connection)}})
|
||||
if 'assimilate' == operation:
|
||||
drone = request['name']
|
||||
droneinfo = cfm.get_collective_member(drone)
|
||||
if not droneinfo:
|
||||
tlvdata.send(connection,
|
||||
{'error': 'Unrecognized leader, '
|
||||
'redo invitation process'})
|
||||
return
|
||||
if not util.cert_matches(droneinfo['fingerprint'], cert):
|
||||
tlvdata.send(connection,
|
||||
{'error': 'Invalid certificate, '
|
||||
'redo invitation process'})
|
||||
return
|
||||
if request['txcount'] < cfm._txcount:
|
||||
tlvdata.send(connection,
|
||||
{'error': 'Refusing to be assimilated by inferior'
|
||||
'transaction count',
|
||||
'txcount': cfm._txcount,})
|
||||
return
|
||||
if connecting.active:
|
||||
# don't try to connect while actively already trying to connect
|
||||
tlvdata.send(connection, {'status': 0})
|
||||
connection.close()
|
||||
return
|
||||
if (currentleader == connection.getpeername()[0] and
|
||||
follower and not follower.dead):
|
||||
# if we are happily following this leader already, don't stir
|
||||
# the pot
|
||||
tlvdata.send(connection, {'status': 0})
|
||||
connection.close()
|
||||
return
|
||||
log.log({'info': 'Connecting in response to assimilation',
|
||||
'subsystem': 'collective'})
|
||||
eventlet.spawn_n(connect_to_leader, None, None,
|
||||
leader=connection.getpeername()[0])
|
||||
tlvdata.send(connection, {'status': 0})
|
||||
connection.close()
|
||||
if 'getinfo' == operation:
|
||||
drone = request['name']
|
||||
droneinfo = cfm.get_collective_member(drone)
|
||||
if not (droneinfo and util.cert_matches(droneinfo['fingerprint'],
|
||||
cert)):
|
||||
tlvdata.send(connection,
|
||||
{'error': 'Invalid certificate, '
|
||||
'redo invitation process'})
|
||||
connection.close()
|
||||
return
|
||||
collinfo = {}
|
||||
populate_collinfo(collinfo)
|
||||
tlvdata.send(connection, collinfo)
|
||||
if 'connect' == operation:
|
||||
drone = request['name']
|
||||
folver = request.get('protover', 2)
|
||||
droneinfo = cfm.get_collective_member(drone)
|
||||
if not (droneinfo and util.cert_matches(droneinfo['fingerprint'],
|
||||
cert)):
|
||||
tlvdata.send(connection,
|
||||
{'error': 'Invalid certificate, '
|
||||
'redo invitation process'})
|
||||
connection.close()
|
||||
return
|
||||
myself = connection.getsockname()[0]
|
||||
if connecting.active:
|
||||
tlvdata.send(connection, {'error': 'Connecting right now',
|
||||
'backoff': True})
|
||||
connection.close()
|
||||
return
|
||||
if myself != get_leader(connection):
|
||||
tlvdata.send(
|
||||
connection,
|
||||
{'error': 'Cannot assimilate, our leader is '
|
||||
'in another castle', 'leader': currentleader})
|
||||
connection.close()
|
||||
return
|
||||
if request['txcount'] > cfm._txcount:
|
||||
retire_as_leader()
|
||||
tlvdata.send(connection,
|
||||
{'error': 'Client has higher tranasaction count, '
|
||||
'should assimilate me, connecting..',
|
||||
'txcount': cfm._txcount})
|
||||
log.log({'info': 'Connecting to leader due to superior '
|
||||
'transaction count', 'subsystem': collective})
|
||||
eventlet.spawn_n(connect_to_leader, None, None,
|
||||
connection.getpeername()[0])
|
||||
connection.close()
|
||||
return
|
||||
if retrythread:
|
||||
retrythread.cancel()
|
||||
retrythread = None
|
||||
with leader_init:
|
||||
cfm.update_collective_address(request['name'],
|
||||
connection.getpeername()[0])
|
||||
tlvdata.send(connection, cfm._dump_keys(None, False))
|
||||
tlvdata.send(connection, cfm._cfgstore['collective'])
|
||||
tlvdata.send(connection, cfm.get_globals())
|
||||
cfgdata = cfm.ConfigManager(None)._dump_to_json()
|
||||
tlvdata.send(connection, {'txcount': cfm._txcount,
|
||||
'dbsize': len(cfgdata)})
|
||||
connection.sendall(cfgdata)
|
||||
#tlvdata.send(connection, {'tenants': 0}) # skip the tenants for now,
|
||||
# so far unused anyway
|
||||
if not cfm.relay_slaved_requests(drone, connection, folver):
|
||||
if not retrythread: # start a recovery if everyone else seems
|
||||
# to have disappeared
|
||||
retrythread = eventlet.spawn_after(30 + random.random(),
|
||||
start_collective)
|
||||
# ok, we have a connecting member whose certificate checks out
|
||||
# He needs to bootstrap his configuration and subscribe it to updates
|
||||
|
||||
|
||||
def populate_collinfo(collinfo):
|
||||
iam = get_myname()
|
||||
collinfo['leader'] = iam
|
||||
collinfo['active'] = list(cfm.cfgstreams)
|
||||
activemembers = set(cfm.cfgstreams)
|
||||
activemembers.add(iam)
|
||||
collinfo['offline'] = []
|
||||
for member in cfm.list_collective():
|
||||
if member not in activemembers:
|
||||
collinfo['offline'].append(member)
|
||||
|
||||
|
||||
def try_assimilate(drone):
|
||||
try:
|
||||
remote = connect_to_collective(None, drone)
|
||||
except socket.error:
|
||||
# Oh well, unable to connect, hopefully the rest will be
|
||||
# in order
|
||||
return
|
||||
tlvdata.send(remote, {'collective': {'operation': 'assimilate',
|
||||
'name': get_myname(),
|
||||
'txcount': cfm._txcount}})
|
||||
tlvdata.recv(remote) # the banner
|
||||
tlvdata.recv(remote) # authpassed... 0..
|
||||
answer = tlvdata.recv(remote)
|
||||
if not answer:
|
||||
log.log(
|
||||
{'error':
|
||||
'No answer from {0} while trying to assimilate'.format(
|
||||
drone),
|
||||
'subsystem': 'collective'})
|
||||
return
|
||||
if 'txcount' in answer:
|
||||
log.log({'info': 'Deferring to {0} due to transaction count'.format(
|
||||
drone), 'subsystem': 'collective'})
|
||||
connect_to_leader(None, None, leader=remote.getpeername()[0])
|
||||
return
|
||||
if 'error' in answer:
|
||||
log.log({
|
||||
'error': 'Error encountered while attempting to '
|
||||
'assimilate {0}: {1}'.format(drone, answer['error']),
|
||||
'subsystem': 'collective'})
|
||||
return
|
||||
log.log({'info': 'Assimilated {0} into collective'.format(drone),
|
||||
'subsystem': 'collective'})
|
||||
|
||||
|
||||
def get_leader(connection):
|
||||
if currentleader is None or connection.getpeername()[0] == currentleader:
|
||||
if currentleader is None:
|
||||
msg = 'Becoming leader as no leader known'
|
||||
else:
|
||||
msg = 'Becoming leader because {0} attempted to connect and it ' \
|
||||
'is current leader'.format(currentleader)
|
||||
log.log({'info': msg, 'subsystem': 'collective'})
|
||||
become_leader(connection)
|
||||
return currentleader
|
||||
|
||||
def retire_as_leader():
|
||||
global currentleader
|
||||
cfm.stop_leading()
|
||||
currentleader = None
|
||||
|
||||
def become_leader(connection):
|
||||
global currentleader
|
||||
global follower
|
||||
global retrythread
|
||||
log.log({'info': 'Becoming leader of collective',
|
||||
'subsystem': 'collective'})
|
||||
if follower:
|
||||
follower.kill()
|
||||
cfm.stop_following()
|
||||
follower = None
|
||||
if retrythread:
|
||||
retrythread.cancel()
|
||||
retrythread = None
|
||||
currentleader = connection.getsockname()[0]
|
||||
skipaddr = connection.getpeername()[0]
|
||||
myname = get_myname()
|
||||
skipem = set(cfm.cfgstreams)
|
||||
skipem.add(currentleader)
|
||||
skipem.add(skipaddr)
|
||||
for member in cfm.list_collective():
|
||||
dronecandidate = cfm.get_collective_member(member)['address']
|
||||
if dronecandidate in skipem or member == myname:
|
||||
continue
|
||||
eventlet.spawn_n(try_assimilate, dronecandidate)
|
||||
|
||||
|
||||
def startup():
|
||||
members = list(cfm.list_collective())
|
||||
if len(members) < 2:
|
||||
# Not in collective mode, return
|
||||
return
|
||||
eventlet.spawn_n(start_collective)
|
||||
|
||||
def start_collective():
|
||||
global follower
|
||||
global retrythread
|
||||
if follower:
|
||||
follower.kill()
|
||||
cfm.stop_following()
|
||||
follower = None
|
||||
try:
|
||||
if cfm.cfgstreams:
|
||||
cfm.check_quorum()
|
||||
# Do not start if we have quorum and are leader
|
||||
return
|
||||
except exc.DegradedCollective:
|
||||
pass
|
||||
if leader_init.active: # do not start trying to connect if we are
|
||||
# xmitting data to a follower
|
||||
return
|
||||
myname = get_myname()
|
||||
for member in sorted(list(cfm.list_collective())):
|
||||
if member == myname:
|
||||
continue
|
||||
if cfm.cfgleader is None:
|
||||
cfm.stop_following(True)
|
||||
ldrcandidate = cfm.get_collective_member(member)['address']
|
||||
log.log({'info': 'Performing startup attempt to {0}'.format(
|
||||
ldrcandidate), 'subsystem': 'collective'})
|
||||
if connect_to_leader(name=myname, leader=ldrcandidate):
|
||||
break
|
||||
else:
|
||||
retrythread = eventlet.spawn_after(30 + random.random(),
|
||||
start_collective)
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2014 IBM Corporation
|
||||
# Copyright 2015 Lenovo
|
||||
# Copyright 2015-2019 Lenovo
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -93,9 +93,10 @@ node = {
|
||||
'description': ('List of static groups for which this node is '
|
||||
'considered a member'),
|
||||
},
|
||||
#'type': {
|
||||
# 'description': ('Classification of node as system, vm, etc')
|
||||
#},
|
||||
'type': {
|
||||
'description': ('Classification of node as server or switch'),
|
||||
'validvalues': ('switch', 'server'),
|
||||
},
|
||||
#'id': {
|
||||
# 'description': ('Numeric identifier for node')
|
||||
#},
|
||||
@@ -148,6 +149,22 @@ node = {
|
||||
# 'autonode.servername, so that would not need to be '
|
||||
# 'copied ')
|
||||
# },
|
||||
'collective.manager': {
|
||||
'description': ('When in collective mode, the member of the '
|
||||
'collective currently considered to be responsible '
|
||||
'for this node. At a future date, this may be '
|
||||
'modified automatically if another attribute '
|
||||
'indicates candidate managers, either for '
|
||||
'high availability or load balancing purposes.')
|
||||
},
|
||||
'discovery.passwordrules': {
|
||||
'description': 'Any specified rules shall be configured on the BMC '
|
||||
'upon discovery. "expiration=no,loginfailures=no,complexity=no,reuse=no" '
|
||||
'would disable password expiration, login failures '
|
||||
'triggering a lockout, password complexity requirements,'
|
||||
'and any restrictions around reusing an old password.',
|
||||
'validlistkeys': ('expiration', 'loginfailures', 'complexity', 'reuse'),
|
||||
},
|
||||
'discovery.policy': {
|
||||
'description': 'Policy to use for auto-configuration of discovered '
|
||||
'and identified nodes. Valid values are "manual", '
|
||||
@@ -157,6 +174,7 @@ node = {
|
||||
'so long as the node has no existing public key. '
|
||||
'"open" allows discovery even if a known public key '
|
||||
'is already stored',
|
||||
'validlist': ('manual', 'permissive', 'pxe', 'open'),
|
||||
},
|
||||
'info.note': {
|
||||
'description': 'A field used for administrators to make arbitrary '
|
||||
@@ -165,6 +183,9 @@ node = {
|
||||
'freeform text data without concern for issues in how '
|
||||
'the server will process it.',
|
||||
},
|
||||
'location.height': {
|
||||
'description': 'Height in RU of the system (defaults to query the systems)',
|
||||
},
|
||||
'location.room': {
|
||||
'description': 'Room description for the node',
|
||||
},
|
||||
@@ -244,11 +265,13 @@ node = {
|
||||
'console.logging': {
|
||||
'description': ('Indicate logging level to apply to console. Valid '
|
||||
'values are currently "full", "interactive", and '
|
||||
'"none". Defaults to "full".')
|
||||
'"none". Defaults to "full".'),
|
||||
'validvalues': ('full', 'interactive', 'none'),
|
||||
},
|
||||
'console.method': {
|
||||
'description': ('Indicate the method used to access the console of '
|
||||
'the managed node.')
|
||||
'the managed node.'),
|
||||
'validvalues': ('ssh', 'ipmi', 'tsmsol'),
|
||||
},
|
||||
# 'virtualization.host': {
|
||||
# 'description': ('Hypervisor where this node does/should reside'),
|
||||
@@ -279,12 +302,17 @@ node = {
|
||||
'description': 'The method used to perform operations such as power '
|
||||
'control, get sensor data, get inventory, and so on. '
|
||||
},
|
||||
'enclosure.manager': {
|
||||
'description': "The management device for this node's chassis",
|
||||
# 'appliesto': ['system'],
|
||||
},
|
||||
'enclosure.bay': {
|
||||
'description': 'The bay in the enclosure, if any',
|
||||
# 'appliesto': ['system'],
|
||||
},
|
||||
'enclosure.extends': {
|
||||
'description': 'When using an extendable enclosure, this is the node '
|
||||
'representing the manager that is one closer to the '
|
||||
'uplink.',
|
||||
},
|
||||
'enclosure.manager': {
|
||||
'description': "The management device for this node's chassis",
|
||||
# 'appliesto': ['system'],
|
||||
},
|
||||
|
||||
|
||||
@@ -16,7 +16,10 @@
|
||||
|
||||
|
||||
# This defines config variable to store the global configuration for confluent
|
||||
import ConfigParser
|
||||
try:
|
||||
import ConfigParser
|
||||
except ModuleNotFoundError:
|
||||
import configparser as ConfigParser
|
||||
import os
|
||||
|
||||
_config = None
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2014 IBM Corporation
|
||||
# Copyright 2017 Lenovo
|
||||
# Copyright 2017-2018 Lenovo
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -23,14 +23,18 @@
|
||||
# there should be no more than one handler per node
|
||||
import codecs
|
||||
import collections
|
||||
import confluent.collective.manager as collective
|
||||
import confluent.config.configmanager as configmodule
|
||||
import confluent.exceptions as exc
|
||||
import confluent.interface.console as conapi
|
||||
import confluent.log as log
|
||||
import confluent.core as plugin
|
||||
import confluent.tlvdata as tlvdata
|
||||
import confluent.util as util
|
||||
import eventlet
|
||||
import eventlet.event
|
||||
import eventlet.green.socket as socket
|
||||
import eventlet.green.ssl as ssl
|
||||
import pyte
|
||||
import random
|
||||
import time
|
||||
@@ -90,7 +94,7 @@ def _utf8_normalize(data, shiftin, decoder):
|
||||
|
||||
|
||||
def pytechars2line(chars, maxlen=None):
|
||||
line = '\x1b[m' # start at default params
|
||||
line = b'\x1b[m' # start at default params
|
||||
lb = False # last bold
|
||||
li = False # last italic
|
||||
lu = False # last underline
|
||||
@@ -102,7 +106,7 @@ def pytechars2line(chars, maxlen=None):
|
||||
len = 1
|
||||
for charidx in range(maxlen):
|
||||
char = chars[charidx]
|
||||
csi = []
|
||||
csi = bytearray([])
|
||||
if char.fg != lfg:
|
||||
csi.append(30 + pytecolors2ansi[char.fg])
|
||||
lfg = char.fg
|
||||
@@ -125,10 +129,13 @@ def pytechars2line(chars, maxlen=None):
|
||||
lr = char.reverse
|
||||
csi.append(7 if lr else 27)
|
||||
if csi:
|
||||
line += b'\x1b[' + b';'.join(['{0}'.format(x) for x in csi]) + b'm'
|
||||
if not hasdata and char.data.encode('utf-8').rstrip():
|
||||
line += b'\x1b[' + b';'.join(['{0}'.format(x).encode('utf-8') for x in csi]) + b'm'
|
||||
if not hasdata and char.data.rstrip():
|
||||
hasdata = True
|
||||
line += char.data.encode('utf-8')
|
||||
chardata = char.data
|
||||
if not isinstance(chardata, bytes):
|
||||
chardata = chardata.encode('utf-8')
|
||||
line += chardata
|
||||
if maxlen and len >= maxlen:
|
||||
break
|
||||
len += 1
|
||||
@@ -138,13 +145,18 @@ def pytechars2line(chars, maxlen=None):
|
||||
class ConsoleHandler(object):
|
||||
_plugin_path = '/nodes/{0}/_console/session'
|
||||
_logtobuffer = True
|
||||
_genwatchattribs = frozenset(('console.method', 'console.logging'))
|
||||
_genwatchattribs = frozenset(('console.method', 'console.logging',
|
||||
'collective.manager'))
|
||||
|
||||
def __init__(self, node, configmanager):
|
||||
def __init__(self, node, configmanager, width=80, height=24):
|
||||
self.clearpending = False
|
||||
self.clearerror = False
|
||||
self.initsize = (width, height)
|
||||
self._dologging = True
|
||||
self._is_local = True
|
||||
self._isondemand = False
|
||||
self.error = None
|
||||
self._retrytime = 0
|
||||
self.cfgmgr = configmanager
|
||||
self.node = node
|
||||
self.connectstate = 'unconnected'
|
||||
@@ -176,7 +188,7 @@ class ConsoleHandler(object):
|
||||
if termstate & 1:
|
||||
self.appmodedetected = True
|
||||
if termstate & 2:
|
||||
self.shiftin = '0'
|
||||
self.shiftin = b'0'
|
||||
self.users = {}
|
||||
self._attribwatcher = None
|
||||
self._console = None
|
||||
@@ -190,7 +202,19 @@ class ConsoleHandler(object):
|
||||
self.connectstate = 'connecting'
|
||||
eventlet.spawn(self._connect)
|
||||
|
||||
def _get_retry_time(self):
|
||||
clustsize = len(self.cfgmgr._cfgstore['nodes'])
|
||||
self._retrytime = self._retrytime * 2 + 1
|
||||
if self._retrytime > 120:
|
||||
self._retrytime = 120
|
||||
retrytime = clustsize * 0.05 * self._retrytime
|
||||
if retrytime > 120:
|
||||
retrytime = 120
|
||||
return retrytime + (retrytime * random.random())
|
||||
|
||||
def feedbuffer(self, data):
|
||||
if not isinstance(data, bytes):
|
||||
data = data.encode('utf-8')
|
||||
try:
|
||||
self.termstream.feed(data)
|
||||
except StopIteration: # corrupt parser state, start over
|
||||
@@ -203,7 +227,7 @@ class ConsoleHandler(object):
|
||||
def check_isondemand(self):
|
||||
self._dologging = True
|
||||
attrvalue = self.cfgmgr.get_node_attributes(
|
||||
(self.node,), ('console.logging',))
|
||||
(self.node,), ('console.logging', 'collective.manager'))
|
||||
if self.node not in attrvalue:
|
||||
self._isondemand = False
|
||||
elif 'console.logging' not in attrvalue[self.node]:
|
||||
@@ -214,6 +238,23 @@ class ConsoleHandler(object):
|
||||
self._isondemand = True
|
||||
if (attrvalue[self.node]['console.logging']['value']) in ('none', 'memory'):
|
||||
self._dologging = False
|
||||
self.check_collective(attrvalue)
|
||||
|
||||
def check_collective(self, attrvalue):
|
||||
myc = attrvalue.get(self.node, {}).get('collective.manager', {}).get(
|
||||
'value', None)
|
||||
if configmodule.list_collective() and not myc:
|
||||
self._is_local = False
|
||||
self._detach()
|
||||
self._disconnect()
|
||||
if myc and myc != collective.get_myname():
|
||||
# Do not do console connect for nodes managed by another
|
||||
# confluent collective member
|
||||
self._is_local = False
|
||||
self._detach()
|
||||
self._disconnect()
|
||||
else:
|
||||
self._is_local = True
|
||||
|
||||
def get_buffer_age(self):
|
||||
"""Return age of buffered data
|
||||
@@ -225,6 +266,10 @@ class ConsoleHandler(object):
|
||||
return False
|
||||
|
||||
def _attribschanged(self, nodeattribs, configmanager, **kwargs):
|
||||
if 'collective.manager' in nodeattribs[self.node]:
|
||||
attrval = configmanager.get_node_attributes(self.node,
|
||||
'collective.manager')
|
||||
self.check_collective(attrval)
|
||||
if 'console.logging' in nodeattribs[self.node]:
|
||||
# decide whether logging changes how we react or not
|
||||
self._dologging = True
|
||||
@@ -269,10 +314,17 @@ class ConsoleHandler(object):
|
||||
|
||||
def clearbuffer(self):
|
||||
self.feedbuffer(
|
||||
'\x1bc[no replay buffer due to console.logging attribute set to '
|
||||
'none or interactive,\r\nconnection loss, or service restart]')
|
||||
'\x1bc[No data has been received from the remote console since ' \
|
||||
'connecting. This could\r\nbe due to having the console.logging ' \
|
||||
'attribute set to none or interactive,\r\nserial console not ' \
|
||||
'being enabled or incorrectly configured in the OS or\r\nfirmware, ' \
|
||||
'or the console simply not having any output since last connection]')
|
||||
self.clearpending = True
|
||||
|
||||
def _detach(self):
|
||||
for ses in list(self.livesessions):
|
||||
ses.detach()
|
||||
|
||||
def _disconnect(self):
|
||||
if self.connectionthread:
|
||||
self.connectionthread.kill()
|
||||
@@ -294,6 +346,8 @@ class ConsoleHandler(object):
|
||||
self._disconnect()
|
||||
|
||||
def _connect(self):
|
||||
if not self._is_local:
|
||||
return
|
||||
if self.connectionthread:
|
||||
self.connectionthread.kill()
|
||||
self.connectionthread = None
|
||||
@@ -308,23 +362,40 @@ class ConsoleHandler(object):
|
||||
if self.reconnect:
|
||||
self.reconnect.cancel()
|
||||
self.reconnect = None
|
||||
strerror = ('The console.method attribute for this node is '
|
||||
'not configured,\r\nset it to a valid value for console '
|
||||
'function')
|
||||
try:
|
||||
self._console = plugin.handle_path(
|
||||
self._console = list(plugin.handle_path(
|
||||
self._plugin_path.format(self.node),
|
||||
"create", self.cfgmgr)
|
||||
"create", self.cfgmgr))[0]
|
||||
except (exc.NotImplementedException, exc.NotFoundException):
|
||||
self._console = None
|
||||
except:
|
||||
_tracelog.log(traceback.format_exc(), ltype=log.DataTypes.event,
|
||||
event=log.Events.stacktrace)
|
||||
except Exception as e:
|
||||
strerror = str(e)
|
||||
if _tracelog:
|
||||
_tracelog.log(traceback.format_exc(), ltype=log.DataTypes.event,
|
||||
event=log.Events.stacktrace)
|
||||
else:
|
||||
print(traceback.format_exc())
|
||||
if not isinstance(self._console, conapi.Console):
|
||||
self.clearbuffer()
|
||||
self.connectstate = 'unconnected'
|
||||
self.error = 'misconfigured'
|
||||
self._send_rcpts({'connectstate': self.connectstate,
|
||||
'error': self.error})
|
||||
self.feedbuffer(
|
||||
'\x1bc\x1b[2J\x1b[1;1H[{0}]'.format(strerror))
|
||||
self._send_rcpts(
|
||||
'\x1bc\x1b[2J\x1b[1;1H[{0}]'.format(strerror))
|
||||
self.clearerror = True
|
||||
return
|
||||
if self.clearerror:
|
||||
self.clearerror = False
|
||||
self.clearbuffer()
|
||||
self._send_rcpts(b'\x1bc\x1b[2J\x1b[1;1H')
|
||||
self.send_break = self._console.send_break
|
||||
self.resize = self._console.resize
|
||||
if self._attribwatcher:
|
||||
self.cfgmgr.remove_watcher(self._attribwatcher)
|
||||
self._attribwatcher = None
|
||||
@@ -336,6 +407,7 @@ class ConsoleHandler(object):
|
||||
self._attribwatcher = self.cfgmgr.watch_attributes(
|
||||
(self.node,), attribstowatch, self._attribschanged)
|
||||
try:
|
||||
self.resize(width=self.initsize[0], height=self.initsize[1])
|
||||
self._console.connect(self.get_console_output)
|
||||
except exc.TargetEndpointBadCredentials:
|
||||
self.clearbuffer()
|
||||
@@ -343,7 +415,7 @@ class ConsoleHandler(object):
|
||||
self.connectstate = 'unconnected'
|
||||
self._send_rcpts({'connectstate': self.connectstate,
|
||||
'error': self.error})
|
||||
retrytime = 120 + (120 * random.random())
|
||||
retrytime = self._get_retry_time()
|
||||
if not self.reconnect:
|
||||
self.reconnect = eventlet.spawn_after(retrytime, self._connect)
|
||||
return
|
||||
@@ -353,7 +425,7 @@ class ConsoleHandler(object):
|
||||
self.connectstate = 'unconnected'
|
||||
self._send_rcpts({'connectstate': self.connectstate,
|
||||
'error': self.error})
|
||||
retrytime = 120 + (120 * random.random())
|
||||
retrytime = self._get_retry_time()
|
||||
if not self.reconnect:
|
||||
self.reconnect = eventlet.spawn_after(retrytime, self._connect)
|
||||
return
|
||||
@@ -365,7 +437,7 @@ class ConsoleHandler(object):
|
||||
self.connectstate = 'unconnected'
|
||||
self._send_rcpts({'connectstate': self.connectstate,
|
||||
'error': self.error})
|
||||
retrytime = 120 + (120 * random.random())
|
||||
retrytime = self._get_retry_time()
|
||||
if not self.reconnect:
|
||||
self.reconnect = eventlet.spawn_after(retrytime, self._connect)
|
||||
return
|
||||
@@ -373,14 +445,15 @@ class ConsoleHandler(object):
|
||||
|
||||
def _got_connected(self):
|
||||
self.connectstate = 'connected'
|
||||
self._retrytime = 0
|
||||
self.log(
|
||||
logdata='console connected', ltype=log.DataTypes.event,
|
||||
event=log.Events.consoleconnect)
|
||||
self._send_rcpts({'connectstate': self.connectstate})
|
||||
|
||||
def _got_disconnected(self):
|
||||
self.clearbuffer()
|
||||
if self.connectstate != 'unconnected':
|
||||
self._console.close()
|
||||
self.connectstate = 'unconnected'
|
||||
self.log(
|
||||
logdata='console disconnected', ltype=log.DataTypes.event,
|
||||
@@ -388,6 +461,8 @@ class ConsoleHandler(object):
|
||||
self._send_rcpts({'connectstate': self.connectstate})
|
||||
if self._isalive:
|
||||
self._connect()
|
||||
else:
|
||||
self.clearbuffer()
|
||||
|
||||
def close(self):
|
||||
self._isalive = False
|
||||
@@ -400,6 +475,9 @@ class ConsoleHandler(object):
|
||||
if self.connectionthread:
|
||||
self.connectionthread.kill()
|
||||
self.connectionthread = None
|
||||
if self._attribwatcher:
|
||||
self.cfgmgr.remove_watcher(self._attribwatcher)
|
||||
self._attribwatcher = None
|
||||
|
||||
def get_console_output(self, data):
|
||||
# Spawn as a greenthread, return control as soon as possible
|
||||
@@ -453,33 +531,37 @@ class ConsoleHandler(object):
|
||||
if data == conapi.ConsoleEvent.Disconnect:
|
||||
self._got_disconnected()
|
||||
return
|
||||
elif data == '':
|
||||
elif data in (b'', u''):
|
||||
# ignore empty strings from a cconsole provider
|
||||
return
|
||||
if '\x1b[?1l' in data: # request for ansi mode cursor keys
|
||||
if not isinstance(data, bytes):
|
||||
data = data.encode('utf-8')
|
||||
if b'\x1b[?1l' in data: # request for ansi mode cursor keys
|
||||
self.appmodedetected = False
|
||||
if '\x1b[?1h' in data: # remember the session wants the client to use
|
||||
if b'\x1b[?1h' in data: # remember the session wants the client to use
|
||||
# 'application mode' Thus far only observed on esxi
|
||||
self.appmodedetected = True
|
||||
if '\x1b)0' in data:
|
||||
if b'\x1b)0' in data:
|
||||
# console indicates it wants access to special drawing characters
|
||||
self.shiftin = '0'
|
||||
self.shiftin = b'0'
|
||||
eventdata = 0
|
||||
if self.appmodedetected:
|
||||
eventdata |= 1
|
||||
if self.shiftin is not None:
|
||||
eventdata |= 2
|
||||
self.log(data, eventdata=eventdata)
|
||||
self.lasttime = util.monotonic_time()
|
||||
self.feedbuffer(data)
|
||||
# TODO: analyze buffer for registered events, examples:
|
||||
# panics
|
||||
# certificate signing request
|
||||
if self.clearpending:
|
||||
if self.clearpending or self.clearerror:
|
||||
self.clearpending = False
|
||||
self.feedbuffer(b'\x1bc')
|
||||
self._send_rcpts(b'\x1bc')
|
||||
self.clearerror = False
|
||||
self.feedbuffer(b'\x1bc\x1b[2J\x1b[1;1H')
|
||||
self._send_rcpts(b'\x1bc\x1b[2J\x1b[1;1H')
|
||||
self._send_rcpts(_utf8_normalize(data, self.shiftin, self.utf8decoder))
|
||||
self.log(data, eventdata=eventdata)
|
||||
self.lasttime = util.monotonic_time()
|
||||
self.feedbuffer(data)
|
||||
|
||||
|
||||
def _send_rcpts(self, data):
|
||||
for rcpt in list(self.livesessions):
|
||||
@@ -514,25 +596,30 @@ class ConsoleHandler(object):
|
||||
if pendingbl:
|
||||
retdata += pendingbl
|
||||
pendingbl = b''
|
||||
retdata += nline + '\r\n'
|
||||
retdata += nline + b'\r\n'
|
||||
else:
|
||||
pendingbl += nline + '\r\n'
|
||||
pendingbl += nline + b'\r\n'
|
||||
if len(retdata) > 6:
|
||||
retdata = retdata[:-2] # remove the last \r\n
|
||||
retdata += b'\x1b[{0};{1}H'.format(self.buffer.cursor.y + 1,
|
||||
self.buffer.cursor.x + 1)
|
||||
cursordata = '\x1b[{0};{1}H'.format(self.buffer.cursor.y + 1,
|
||||
self.buffer.cursor.x + 1)
|
||||
if not isinstance(cursordata, bytes):
|
||||
cursordata = cursordata.encode('utf-8')
|
||||
retdata += cursordata
|
||||
if self.shiftin is not None: # detected that terminal requested a
|
||||
# shiftin character set, relay that to the terminal that cannected
|
||||
retdata += '\x1b)' + self.shiftin
|
||||
retdata += b'\x1b)' + self.shiftin
|
||||
if self.appmodedetected:
|
||||
retdata += '\x1b[?1h'
|
||||
retdata += b'\x1b[?1h'
|
||||
else:
|
||||
retdata += '\x1b[?1l'
|
||||
retdata += b'\x1b[?1l'
|
||||
return retdata, connstate
|
||||
|
||||
def write(self, data):
|
||||
if self.connectstate == 'connected':
|
||||
try:
|
||||
if isinstance(data, str) and not isinstance(data, bytes):
|
||||
data = data.encode('utf-8')
|
||||
self._console.write(data)
|
||||
except Exception:
|
||||
_tracelog.log(traceback.format_exc(), ltype=log.DataTypes.event,
|
||||
@@ -547,15 +634,23 @@ def disconnect_node(node, configmanager):
|
||||
del _handled_consoles[consk]
|
||||
|
||||
|
||||
def _nodechange(added, deleting, configmanager):
|
||||
for node in added:
|
||||
connect_node(node, configmanager)
|
||||
def _nodechange(added, deleting, renamed, configmanager):
|
||||
for node in deleting:
|
||||
disconnect_node(node, configmanager)
|
||||
for node in renamed:
|
||||
disconnect_node(node, configmanager)
|
||||
connect_node(renamed[node], configmanager)
|
||||
for node in added:
|
||||
connect_node(node, configmanager)
|
||||
|
||||
|
||||
def _start_tenant_sessions(cfm):
|
||||
for node in cfm.list_nodes():
|
||||
nodeattrs = cfm.get_node_attributes(cfm.list_nodes(), 'collective.manager')
|
||||
for node in nodeattrs:
|
||||
manager = nodeattrs[node].get('collective.manager', {}).get('value',
|
||||
None)
|
||||
if manager and collective.get_myname() != manager:
|
||||
continue
|
||||
try:
|
||||
connect_node(node, cfm)
|
||||
except:
|
||||
@@ -570,12 +665,130 @@ def start_console_sessions():
|
||||
configmodule.hook_new_configmanagers(_start_tenant_sessions)
|
||||
|
||||
|
||||
def connect_node(node, configmanager, username=None):
|
||||
def connect_node(node, configmanager, username=None, direct=True, width=80,
|
||||
height=24):
|
||||
attrval = configmanager.get_node_attributes(node, 'collective.manager')
|
||||
myc = attrval.get(node, {}).get('collective.manager', {}).get(
|
||||
'value', None)
|
||||
myname = collective.get_myname()
|
||||
if myc and myc != collective.get_myname() and direct:
|
||||
minfo = configmodule.get_collective_member(myc)
|
||||
return ProxyConsole(node, minfo, myname, configmanager, username,
|
||||
width, height)
|
||||
consk = (node, configmanager.tenant)
|
||||
if consk not in _handled_consoles:
|
||||
_handled_consoles[consk] = ConsoleHandler(node, configmanager)
|
||||
_handled_consoles[consk] = ConsoleHandler(node, configmanager, width,
|
||||
height)
|
||||
return _handled_consoles[consk]
|
||||
|
||||
# A stub console handler that just passes through to a remote confluent
|
||||
# collective member. It can skip the multi-session sharing as that is handled
|
||||
# remotely
|
||||
class ProxyConsole(object):
|
||||
_genwatchattribs = frozenset(('collective.manager',))
|
||||
|
||||
def __init__(self, node, managerinfo, myname, configmanager, user, width,
|
||||
height):
|
||||
self.skipreplay = True
|
||||
self.initsize = (width, height)
|
||||
self.managerinfo = managerinfo
|
||||
self.myname = myname
|
||||
self.cfm = configmanager
|
||||
self.node = node
|
||||
self.user = user
|
||||
self.remote = None
|
||||
self.clisession = None
|
||||
self._attribwatcher = configmanager.watch_attributes(
|
||||
(self.node,), self._genwatchattribs, self._attribschanged)
|
||||
|
||||
|
||||
def _attribschanged(self, nodeattribs, configmanager, **kwargs):
|
||||
if self.clisession:
|
||||
self.clisession.detach()
|
||||
self.clisession = None
|
||||
|
||||
def relay_data(self):
|
||||
data = tlvdata.recv(self.remote)
|
||||
while data:
|
||||
self.data_handler(data)
|
||||
data = tlvdata.recv(self.remote)
|
||||
|
||||
def get_buffer_age(self):
|
||||
# the server sends a buffer age if appropriate, no need to handle
|
||||
# it explicitly in the proxy instance
|
||||
return False
|
||||
|
||||
def get_recent(self):
|
||||
# Again, delegate this to the remote collective member
|
||||
self.skipreplay = False
|
||||
return b''
|
||||
|
||||
def write(self, data):
|
||||
# Relay data to the collective manager
|
||||
try:
|
||||
tlvdata.send(self.remote, data)
|
||||
except Exception:
|
||||
if self.clisession:
|
||||
self.clisession.detach()
|
||||
self.clisession = None
|
||||
|
||||
|
||||
def attachsession(self, session):
|
||||
self.clisession = session
|
||||
self.data_handler = session.data_handler
|
||||
termreq = {
|
||||
'proxyconsole': {
|
||||
'name': self.myname,
|
||||
'user': self.user,
|
||||
'tenant': self.cfm.tenant,
|
||||
'node': self.node,
|
||||
'skipreplay': self.skipreplay,
|
||||
'width': self.initsize[0],
|
||||
'height': self.initsize[1],
|
||||
#TODO(jjohnson2): declare myself as a proxy,
|
||||
#facilitate redirect rather than relay on manager change
|
||||
},
|
||||
}
|
||||
try:
|
||||
remote = socket.create_connection((self.managerinfo['address'], 13001))
|
||||
remote = ssl.wrap_socket(remote, cert_reqs=ssl.CERT_NONE,
|
||||
keyfile='/etc/confluent/privkey.pem',
|
||||
certfile='/etc/confluent/srvcert.pem')
|
||||
if not util.cert_matches(self.managerinfo['fingerprint'],
|
||||
remote.getpeercert(binary_form=True)):
|
||||
raise Exception('Invalid peer certificate')
|
||||
except Exception:
|
||||
eventlet.sleep(3)
|
||||
if self.clisession:
|
||||
self.clisession.detach()
|
||||
self.detachsession(None)
|
||||
return
|
||||
tlvdata.recv(remote)
|
||||
tlvdata.recv(remote)
|
||||
tlvdata.send(remote, termreq)
|
||||
self.remote = remote
|
||||
eventlet.spawn(self.relay_data)
|
||||
|
||||
def detachsession(self, session):
|
||||
# we will disappear, so just let that happen...
|
||||
if self.remote:
|
||||
try:
|
||||
tlvdata.send(self.remote, {'operation': 'stop'})
|
||||
except Exception:
|
||||
pass
|
||||
self.clisession = None
|
||||
|
||||
def send_break(self):
|
||||
tlvdata.send(self.remote, {'operation': 'break'})
|
||||
|
||||
def reopen(self):
|
||||
tlvdata.send(self.remote, {'operation': 'reopen'})
|
||||
|
||||
def resize(self, width, height):
|
||||
tlvdata.send(self.remote, {'operation': 'resize', 'width': width,
|
||||
'height': height})
|
||||
|
||||
|
||||
# this represents some api view of a console handler. This handles things like
|
||||
# holding the caller specific queue data, for example, when http api should be
|
||||
# sending data, but there is no outstanding POST request to hold it,
|
||||
@@ -597,10 +810,9 @@ class ConsoleSession(object):
|
||||
'get_next_output' non-functional
|
||||
:param skipreplay: If true, will skip the attempt to redraw the screen
|
||||
"""
|
||||
connector = connect_node
|
||||
|
||||
def __init__(self, node, configmanager, username, datacallback=None,
|
||||
skipreplay=False):
|
||||
skipreplay=False, direct=True, width=80, height=24):
|
||||
self.registered = False
|
||||
self.tenant = configmanager.tenant
|
||||
if not configmanager.is_node(node):
|
||||
@@ -608,6 +820,10 @@ class ConsoleSession(object):
|
||||
self.username = username
|
||||
self.node = node
|
||||
self.configmanager = configmanager
|
||||
self.direct = direct # true if client is directly connected versus
|
||||
# relay
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.connect_session()
|
||||
self.registered = True
|
||||
self._evt = None
|
||||
@@ -636,12 +852,16 @@ class ConsoleSession(object):
|
||||
between console and shell.
|
||||
"""
|
||||
self.conshdl = connect_node(self.node, self.configmanager,
|
||||
self.username)
|
||||
self.username, self.direct, self.width,
|
||||
self.height)
|
||||
def send_break(self):
|
||||
"""Send break to remote system
|
||||
"""
|
||||
self.conshdl.send_break()
|
||||
|
||||
def resize(self, width, height):
|
||||
self.conshdl.resize(width, height)
|
||||
|
||||
def get_buffer_age(self):
|
||||
"""Get the age in seconds of the buffered data
|
||||
|
||||
@@ -667,6 +887,18 @@ class ConsoleSession(object):
|
||||
self._evt = None
|
||||
self.reghdl = None
|
||||
|
||||
def detach(self):
|
||||
"""Handler for the console handler to detach so it can reattach,
|
||||
currently to facilitate changing from one collective.manager to
|
||||
another
|
||||
|
||||
:return:
|
||||
"""
|
||||
self.conshdl.detachsession(self)
|
||||
self.connect_session()
|
||||
self.conshdl.attachsession(self)
|
||||
self.write = self.conshdl.write
|
||||
|
||||
def got_data(self, data):
|
||||
"""Receive data from console and buffer
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2014 IBM Corporation
|
||||
# Copyright 2015-2017 Lenovo
|
||||
# Copyright 2015-2018 Lenovo
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -35,7 +35,11 @@
|
||||
|
||||
import confluent
|
||||
import confluent.alerts as alerts
|
||||
import confluent.log as log
|
||||
import confluent.tlvdata as tlvdata
|
||||
import confluent.config.attributes as attrscheme
|
||||
import confluent.config.configmanager as cfm
|
||||
import confluent.collective.manager as collective
|
||||
import confluent.discovery.core as disco
|
||||
import confluent.interface.console as console
|
||||
import confluent.exceptions as exc
|
||||
@@ -46,11 +50,29 @@ try:
|
||||
import confluent.shellmodule as shellmodule
|
||||
except ImportError:
|
||||
pass
|
||||
try:
|
||||
import OpenSSL.crypto as crypto
|
||||
except ImportError:
|
||||
# Only required for collective mode
|
||||
crypto = None
|
||||
import confluent.util as util
|
||||
import eventlet.greenpool as greenpool
|
||||
import eventlet.green.ssl as ssl
|
||||
import eventlet.queue as queue
|
||||
import itertools
|
||||
import os
|
||||
try:
|
||||
import cPickle as pickle
|
||||
pargs = {}
|
||||
except ImportError:
|
||||
import pickle
|
||||
pargs = {'encoding': 'utf-8'}
|
||||
import socket
|
||||
import struct
|
||||
import sys
|
||||
|
||||
pluginmap = {}
|
||||
dispatch_plugins = (b'ipmi', u'ipmi')
|
||||
|
||||
|
||||
def seek_element(currplace, currkey):
|
||||
@@ -66,7 +88,10 @@ def seek_element(currplace, currkey):
|
||||
|
||||
def nested_lookup(nestdict, key):
|
||||
try:
|
||||
return reduce(seek_element, key, nestdict)
|
||||
currloc = nestdict
|
||||
for currk in key:
|
||||
currloc = seek_element(currloc, currk)
|
||||
return currloc
|
||||
except TypeError:
|
||||
raise exc.NotFoundException("Invalid element requested")
|
||||
|
||||
@@ -86,6 +111,8 @@ def load_plugins():
|
||||
for plugin in os.listdir(plugindir):
|
||||
if plugin.startswith('.'):
|
||||
continue
|
||||
if '__pycache__' in plugin:
|
||||
continue
|
||||
(plugin, plugtype) = os.path.splitext(plugin)
|
||||
if plugtype == '.sh':
|
||||
pluginmap[plugin] = shellmodule.Plugin(
|
||||
@@ -104,7 +131,7 @@ def load_plugins():
|
||||
|
||||
|
||||
rootcollections = ['discovery/', 'events/', 'networking/',
|
||||
'noderange/', 'nodes/', 'nodegroups/', 'users/', 'version']
|
||||
'noderange/', 'nodes/', 'nodegroups/', 'usergroups/' , 'users/', 'version']
|
||||
|
||||
|
||||
class PluginRoute(object):
|
||||
@@ -124,6 +151,7 @@ def _init_core():
|
||||
# be enumerated in any collection
|
||||
noderesources = {
|
||||
'attributes': {
|
||||
'rename': PluginRoute({'handler': 'attributes'}),
|
||||
'all': PluginRoute({'handler': 'attributes'}),
|
||||
'current': PluginRoute({'handler': 'attributes'}),
|
||||
'expression': PluginRoute({'handler': 'attributes'}),
|
||||
@@ -142,10 +170,22 @@ def _init_core():
|
||||
'default': 'ipmi',
|
||||
}),
|
||||
},
|
||||
'clear': PluginRoute({
|
||||
'pluginattrs': ['hardwaremanagement.method'],
|
||||
'default': 'ipmi',
|
||||
}),
|
||||
'users': PluginCollection({
|
||||
'pluginattrs': ['hardwaremanagement.method'],
|
||||
'default': 'ipmi',
|
||||
}),
|
||||
'licenses': PluginCollection({
|
||||
'pluginattrs': ['hardwaremanagement.method'],
|
||||
'default': 'ipmi',
|
||||
}),
|
||||
'save_licenses': PluginRoute({
|
||||
'pluginattrs': ['hardwaremanagement.method'],
|
||||
'default': 'ipmi',
|
||||
}),
|
||||
'net_interfaces': PluginCollection({
|
||||
'pluginattrs': ['hardwaremanagement.method'],
|
||||
'default': 'ipmi',
|
||||
@@ -154,6 +194,10 @@ def _init_core():
|
||||
'pluginattrs': ['hardwaremanagement.method'],
|
||||
'default': 'ipmi',
|
||||
}),
|
||||
'hostname': PluginRoute({
|
||||
'pluginattrs': ['hardwaremanagement.method'],
|
||||
'default': 'ipmi',
|
||||
}),
|
||||
'identifier': PluginRoute({
|
||||
'pluginattrs': ['hardwaremanagement.method'],
|
||||
'default': 'ipmi',
|
||||
@@ -172,7 +216,49 @@ def _init_core():
|
||||
'default': 'ipmi',
|
||||
}),
|
||||
},
|
||||
}
|
||||
'extended': {
|
||||
'all': PluginRoute({
|
||||
'pluginattrs': ['hardwaremanagement.method'],
|
||||
'default': 'ipmi',
|
||||
}),
|
||||
'advanced': PluginRoute({
|
||||
'pluginattrs': ['hardwaremanagement.method'],
|
||||
'default': 'ipmi',
|
||||
}),
|
||||
},
|
||||
},
|
||||
'storage': {
|
||||
'all': PluginRoute({
|
||||
'pluginattrs': ['hardwaremanagement.method'],
|
||||
'default': 'ipmi',
|
||||
}),
|
||||
'arrays': PluginCollection({
|
||||
'pluginattrs': ['hardwaremanagement.method'],
|
||||
'default': 'ipmi',
|
||||
}),
|
||||
'disks': PluginCollection({
|
||||
'pluginattrs': ['hardwaremanagement.method'],
|
||||
'default': 'ipmi',
|
||||
}),
|
||||
'volumes': PluginCollection({
|
||||
'pluginattrs': ['hardwaremanagement.method'],
|
||||
'default': 'ipmi',
|
||||
})
|
||||
},
|
||||
'system': {
|
||||
'all': PluginRoute({
|
||||
'pluginattrs': ['hardwaremanagement.method'],
|
||||
'default': 'ipmi',
|
||||
}),
|
||||
'advanced': PluginRoute({
|
||||
'pluginattrs': ['hardwaremanagement.method'],
|
||||
'default': 'ipmi',
|
||||
}),
|
||||
'clear': PluginRoute({
|
||||
'pluginattrs': ['hardwaremanagement.method'],
|
||||
'default': 'ipmi',
|
||||
})
|
||||
},
|
||||
},
|
||||
'_console': {
|
||||
'session': PluginRoute({
|
||||
@@ -204,6 +290,10 @@ def _init_core():
|
||||
'default': 'ipmi',
|
||||
}),
|
||||
},
|
||||
'description': PluginRoute({
|
||||
'pluginattrs': ['hardwaremanagement.method'],
|
||||
'default': 'ipmi',
|
||||
}),
|
||||
'events': {
|
||||
'hardware': {
|
||||
'log': PluginRoute({
|
||||
@@ -250,6 +340,25 @@ def _init_core():
|
||||
},
|
||||
},
|
||||
},
|
||||
'media': {
|
||||
'uploads': PluginCollection({
|
||||
'pluginattrs': ['hardwaremanagement.method'],
|
||||
'default': 'ipmi',
|
||||
}),
|
||||
'attach': PluginRoute({
|
||||
'pluginattrs': ['hardwaremanagement.method'],
|
||||
'default': 'ipmi',
|
||||
}),
|
||||
'detach': PluginRoute({
|
||||
'pluginattrs': ['hardwaremanagement.method'],
|
||||
'default': 'ipmi',
|
||||
}),
|
||||
'current': PluginRoute({
|
||||
'pluginattrs': ['hardwaremanagement.method'],
|
||||
'default': 'ipmi',
|
||||
}),
|
||||
|
||||
},
|
||||
'power': {
|
||||
'state': PluginRoute({
|
||||
'pluginattrs': ['hardwaremanagement.method'],
|
||||
@@ -286,10 +395,18 @@ def _init_core():
|
||||
},
|
||||
|
||||
},
|
||||
'support': {
|
||||
'servicedata': PluginCollection({
|
||||
'pluginattrs': ['hardwaremanagement.method'],
|
||||
'default': 'ipmi',
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
nodegroupresources = {
|
||||
'attributes': {
|
||||
'check': PluginRoute({'handler': 'attributes'}),
|
||||
'rename': PluginRoute({'handler': 'attributes'}),
|
||||
'all': PluginRoute({'handler': 'attributes'}),
|
||||
'current': PluginRoute({'handler': 'attributes'}),
|
||||
},
|
||||
@@ -305,17 +422,37 @@ def create_user(inputdata, configmanager):
|
||||
configmanager.create_user(username, attributemap=inputdata)
|
||||
|
||||
|
||||
def create_usergroup(inputdata, configmanager):
|
||||
try:
|
||||
groupname = inputdata['name']
|
||||
del inputdata['name']
|
||||
except (KeyError, ValueError):
|
||||
raise exc.InvalidArgumentException()
|
||||
configmanager.create_usergroup(groupname)
|
||||
|
||||
|
||||
def update_usergroup(groupname, attribmap, configmanager):
|
||||
try:
|
||||
configmanager.set_usergroup(groupname, attribmap)
|
||||
except ValueError as e:
|
||||
raise exc.InvalidArgumentException(str(e))
|
||||
|
||||
def update_user(name, attribmap, configmanager):
|
||||
try:
|
||||
configmanager.set_user(name, attribmap)
|
||||
except ValueError:
|
||||
raise exc.InvalidArgumentException()
|
||||
except ValueError as e:
|
||||
raise exc.InvalidArgumentException(str(e))
|
||||
|
||||
|
||||
def show_usergroup(groupname, configmanager):
|
||||
groupinfo = configmanager.get_usergroup(groupname)
|
||||
for attr in groupinfo:
|
||||
yield msg.Attributes(kv={attr: groupinfo[attr]})
|
||||
|
||||
def show_user(name, configmanager):
|
||||
userobj = configmanager.get_user(name)
|
||||
rv = {}
|
||||
for attr in attrscheme.user.iterkeys():
|
||||
for attr in attrscheme.user:
|
||||
rv[attr] = None
|
||||
if attr == 'password':
|
||||
if 'cryptpass' in userobj:
|
||||
@@ -328,6 +465,10 @@ def show_user(name, configmanager):
|
||||
rv[attr] = userobj[attr]
|
||||
yield msg.Attributes(kv={attr: rv[attr]},
|
||||
desc=attrscheme.user[attr]['description'])
|
||||
if 'role' in userobj:
|
||||
yield msg.Attributes(kv={'role': userobj['role']})
|
||||
|
||||
|
||||
|
||||
|
||||
def stripnode(iterablersp, node):
|
||||
@@ -360,6 +501,10 @@ def delete_user(user, configmanager):
|
||||
configmanager.del_user(user)
|
||||
yield msg.DeletedResource(user)
|
||||
|
||||
def delete_usergroup(usergroup, configmanager):
|
||||
configmanager.del_usergroup(usergroup)
|
||||
yield msg.DeletedResource(usergroup)
|
||||
|
||||
|
||||
def delete_nodegroup_collection(collectionpath, configmanager):
|
||||
if len(collectionpath) == 2: # just the nodegroup
|
||||
@@ -519,6 +664,15 @@ class BadPlugin(object):
|
||||
self.node, self.plugin + ' is not a supported plugin')
|
||||
|
||||
|
||||
class BadCollective(object):
|
||||
def __init__(self, node):
|
||||
self.node = node
|
||||
|
||||
def error(self, *args, **kwargs):
|
||||
yield msg.ConfluentNodeError(
|
||||
self.node, 'collective mode is active, but collective.manager '
|
||||
'is not set for this node')
|
||||
|
||||
def abbreviate_noderange(configmanager, inputdata, operation):
|
||||
if operation != 'create':
|
||||
raise exc.InvalidArgumentException('Must be a create with nodes in list')
|
||||
@@ -529,8 +683,74 @@ def abbreviate_noderange(configmanager, inputdata, operation):
|
||||
return (msg.KeyValueData({'noderange': noderange.ReverseNodeRange(inputdata['nodes'], configmanager).noderange}),)
|
||||
|
||||
|
||||
def handle_dispatch(connection, cert, dispatch, peername):
|
||||
cert = crypto.dump_certificate(crypto.FILETYPE_ASN1, cert)
|
||||
if not util.cert_matches(
|
||||
cfm.get_collective_member(peername)['fingerprint'], cert):
|
||||
connection.close()
|
||||
return
|
||||
pversion = 0
|
||||
if bytearray(dispatch)[0] == 0x80:
|
||||
pversion = bytearray(dispatch)[1]
|
||||
dispatch = pickle.loads(dispatch, **pargs)
|
||||
configmanager = cfm.ConfigManager(dispatch['tenant'])
|
||||
nodes = dispatch['nodes']
|
||||
inputdata = dispatch['inputdata']
|
||||
operation = dispatch['operation']
|
||||
pathcomponents = dispatch['path']
|
||||
routespec = nested_lookup(noderesources, pathcomponents)
|
||||
plugroute = routespec.routeinfo
|
||||
plugpath = None
|
||||
nodesbyhandler = {}
|
||||
passvalues = []
|
||||
nodeattr = configmanager.get_node_attributes(
|
||||
nodes, plugroute['pluginattrs'])
|
||||
for node in nodes:
|
||||
for attrname in plugroute['pluginattrs']:
|
||||
if attrname in nodeattr[node]:
|
||||
plugpath = nodeattr[node][attrname]['value']
|
||||
elif 'default' in plugroute:
|
||||
plugpath = plugroute['default']
|
||||
if plugpath:
|
||||
try:
|
||||
hfunc = getattr(pluginmap[plugpath], operation)
|
||||
except KeyError:
|
||||
nodesbyhandler[BadPlugin(node, plugpath).error] = [node]
|
||||
continue
|
||||
if hfunc in nodesbyhandler:
|
||||
nodesbyhandler[hfunc].append(node)
|
||||
else:
|
||||
nodesbyhandler[hfunc] = [node]
|
||||
try:
|
||||
for hfunc in nodesbyhandler:
|
||||
passvalues.append(hfunc(
|
||||
nodes=nodesbyhandler[hfunc], element=pathcomponents,
|
||||
configmanager=configmanager,
|
||||
inputdata=inputdata))
|
||||
for res in itertools.chain(*passvalues):
|
||||
_forward_rsp(connection, res, pversion)
|
||||
except Exception as res:
|
||||
_forward_rsp(connection, res, pversion)
|
||||
connection.sendall('\x00\x00\x00\x00\x00\x00\x00\x00')
|
||||
|
||||
|
||||
def _forward_rsp(connection, res, pversion):
|
||||
try:
|
||||
r = pickle.dumps(res, protocol=pversion)
|
||||
except TypeError:
|
||||
r = pickle.dumps(Exception(
|
||||
'Cannot serialize error, check collective.manager error logs for details' + str(res)), protocol=pversion)
|
||||
rlen = len(r)
|
||||
if not rlen:
|
||||
return
|
||||
connection.sendall(struct.pack('!Q', rlen))
|
||||
connection.sendall(r)
|
||||
|
||||
|
||||
def handle_node_request(configmanager, inputdata, operation,
|
||||
pathcomponents, autostrip=True):
|
||||
if log.logfull:
|
||||
raise exc.TargetResourceUnavailable('Filesystem full, free up space and restart confluent service')
|
||||
iscollection = False
|
||||
routespec = None
|
||||
if pathcomponents[0] == 'noderange':
|
||||
@@ -608,7 +828,7 @@ def handle_node_request(configmanager, inputdata, operation,
|
||||
else:
|
||||
raise Exception("TODO here")
|
||||
del pathcomponents[0:2]
|
||||
passvalues = []
|
||||
passvalues = queue.Queue()
|
||||
plugroute = routespec.routeinfo
|
||||
inputdata = msg.get_input_message(
|
||||
pathcomponents, operation, inputdata, nodes, isnoderange,
|
||||
@@ -625,21 +845,37 @@ def handle_node_request(configmanager, inputdata, operation,
|
||||
if isnoderange:
|
||||
return passvalue
|
||||
elif isinstance(passvalue, console.Console):
|
||||
return passvalue
|
||||
return [passvalue]
|
||||
else:
|
||||
return stripnode(passvalue, nodes[0])
|
||||
elif 'pluginattrs' in plugroute:
|
||||
nodeattr = configmanager.get_node_attributes(
|
||||
nodes, plugroute['pluginattrs'])
|
||||
nodes, plugroute['pluginattrs'] + ['collective.manager'])
|
||||
plugpath = None
|
||||
if 'default' in plugroute:
|
||||
plugpath = plugroute['default']
|
||||
nodesbymanager = {}
|
||||
nodesbyhandler = {}
|
||||
badcollnodes = []
|
||||
for node in nodes:
|
||||
for attrname in plugroute['pluginattrs']:
|
||||
if attrname in nodeattr[node]:
|
||||
plugpath = nodeattr[node][attrname]['value']
|
||||
if plugpath is not None:
|
||||
elif 'default' in plugroute:
|
||||
plugpath = plugroute['default']
|
||||
if plugpath in dispatch_plugins:
|
||||
cfm.check_quorum()
|
||||
manager = nodeattr[node].get('collective.manager', {}).get(
|
||||
'value', None)
|
||||
if manager:
|
||||
if collective.get_myname() != manager:
|
||||
if manager not in nodesbymanager:
|
||||
nodesbymanager[manager] = set([node])
|
||||
else:
|
||||
nodesbymanager[manager].add(node)
|
||||
continue
|
||||
elif list(cfm.list_collective()):
|
||||
badcollnodes.append(node)
|
||||
continue
|
||||
if plugpath:
|
||||
try:
|
||||
hfunc = getattr(pluginmap[plugpath], operation)
|
||||
except KeyError:
|
||||
@@ -649,19 +885,27 @@ def handle_node_request(configmanager, inputdata, operation,
|
||||
nodesbyhandler[hfunc].append(node)
|
||||
else:
|
||||
nodesbyhandler[hfunc] = [node]
|
||||
for bn in badcollnodes:
|
||||
nodesbyhandler[BadCollective(bn).error] = [bn]
|
||||
workers = greenpool.GreenPool()
|
||||
numworkers = 0
|
||||
for hfunc in nodesbyhandler:
|
||||
passvalues.append(hfunc(
|
||||
nodes=nodesbyhandler[hfunc], element=pathcomponents,
|
||||
configmanager=configmanager,
|
||||
inputdata=inputdata))
|
||||
numworkers += 1
|
||||
workers.spawn(addtoqueue, passvalues, hfunc, {'nodes': nodesbyhandler[hfunc],
|
||||
'element': pathcomponents,
|
||||
'configmanager': configmanager,
|
||||
'inputdata': inputdata})
|
||||
for manager in nodesbymanager:
|
||||
numworkers += 1
|
||||
workers.spawn(addtoqueue, passvalues, dispatch_request, {
|
||||
'nodes': nodesbymanager[manager], 'manager': manager,
|
||||
'element': pathcomponents, 'configmanager': configmanager,
|
||||
'inputdata': inputdata, 'operation': operation})
|
||||
if isnoderange or not autostrip:
|
||||
return itertools.chain(*passvalues)
|
||||
return iterate_queue(numworkers, passvalues)
|
||||
else:
|
||||
if len(passvalues) > 0:
|
||||
if isinstance(passvalues[0], console.Console):
|
||||
return passvalues[0]
|
||||
else:
|
||||
return stripnode(passvalues[0], nodes[0])
|
||||
if numworkers > 0:
|
||||
return iterate_queue(numworkers, passvalues, nodes[0])
|
||||
else:
|
||||
raise exc.NotImplementedException()
|
||||
|
||||
@@ -671,6 +915,128 @@ def handle_node_request(configmanager, inputdata, operation,
|
||||
# return stripnode(passvalues[0], nodes[0])
|
||||
|
||||
|
||||
def iterate_queue(numworkers, passvalues, strip=False):
|
||||
completions = 0
|
||||
while completions < numworkers:
|
||||
nv = passvalues.get()
|
||||
if nv == 'theend':
|
||||
completions += 1
|
||||
else:
|
||||
if isinstance(nv, Exception):
|
||||
raise nv
|
||||
if strip and not isinstance(nv, console.Console):
|
||||
nv.strip_node(strip)
|
||||
yield nv
|
||||
|
||||
|
||||
def addtoqueue(theq, fun, kwargs):
|
||||
try:
|
||||
result = fun(**kwargs)
|
||||
if isinstance(result, console.Console):
|
||||
theq.put(result)
|
||||
else:
|
||||
for pv in result:
|
||||
theq.put(pv)
|
||||
except Exception as e:
|
||||
theq.put(e)
|
||||
finally:
|
||||
theq.put('theend')
|
||||
|
||||
|
||||
def dispatch_request(nodes, manager, element, configmanager, inputdata,
|
||||
operation):
|
||||
a = configmanager.get_collective_member(manager)
|
||||
try:
|
||||
remote = socket.create_connection((a['address'], 13001))
|
||||
remote.settimeout(180)
|
||||
remote = ssl.wrap_socket(remote, cert_reqs=ssl.CERT_NONE,
|
||||
keyfile='/etc/confluent/privkey.pem',
|
||||
certfile='/etc/confluent/srvcert.pem')
|
||||
except Exception:
|
||||
for node in nodes:
|
||||
if a:
|
||||
yield msg.ConfluentResourceUnavailable(
|
||||
node, 'Collective member {0} is unreachable'.format(
|
||||
a['name']))
|
||||
else:
|
||||
yield msg.ConfluentResourceUnavailable(
|
||||
node,
|
||||
'"{0}" is not recognized as a collective member'.format(
|
||||
manager))
|
||||
|
||||
return
|
||||
if not util.cert_matches(a['fingerprint'], remote.getpeercert(
|
||||
binary_form=True)):
|
||||
raise Exception("Invalid certificate on peer")
|
||||
banner = tlvdata.recv(remote)
|
||||
vers = banner.split()[2]
|
||||
if vers == b'v0':
|
||||
pvers = 2
|
||||
elif vers == b'v1':
|
||||
pvers = 4
|
||||
if sys.version_info[0] < 3:
|
||||
pvers = 2
|
||||
tlvdata.recv(remote)
|
||||
myname = collective.get_myname()
|
||||
dreq = pickle.dumps({'name': myname, 'nodes': list(nodes),
|
||||
'path': element,'tenant': configmanager.tenant,
|
||||
'operation': operation, 'inputdata': inputdata},
|
||||
protocol=pvers)
|
||||
tlvdata.send(remote, {'dispatch': {'name': myname, 'length': len(dreq)}})
|
||||
remote.sendall(dreq)
|
||||
while True:
|
||||
try:
|
||||
rlen = remote.recv(8)
|
||||
except Exception:
|
||||
for node in nodes:
|
||||
yield msg.ConfluentResourceUnavailable(
|
||||
node, 'Collective member {0} went unreachable'.format(
|
||||
a['name']))
|
||||
return
|
||||
while len(rlen) < 8:
|
||||
try:
|
||||
nlen = remote.recv(8 - len(rlen))
|
||||
except Exception:
|
||||
nlen = 0
|
||||
if not nlen:
|
||||
for node in nodes:
|
||||
yield msg.ConfluentResourceUnavailable(
|
||||
node, 'Collective member {0} went unreachable'.format(
|
||||
a['name']))
|
||||
return
|
||||
rlen += nlen
|
||||
rlen = struct.unpack('!Q', rlen)[0]
|
||||
if rlen == 0:
|
||||
break
|
||||
try:
|
||||
rsp = remote.recv(rlen)
|
||||
except Exception:
|
||||
for node in nodes:
|
||||
yield msg.ConfluentResourceUnavailable(
|
||||
node, 'Collective member {0} went unreachable'.format(
|
||||
a['name']))
|
||||
return
|
||||
while len(rsp) < rlen:
|
||||
try:
|
||||
nrsp = remote.recv(rlen - len(rsp))
|
||||
except Exception:
|
||||
nrsp = 0
|
||||
if not nrsp:
|
||||
for node in nodes:
|
||||
yield msg.ConfluentResourceUnavailable(
|
||||
node, 'Collective member {0} went unreachable'.format(
|
||||
a['name']))
|
||||
return
|
||||
rsp += nrsp
|
||||
try:
|
||||
rsp = pickle.loads(rsp, **pargs)
|
||||
except UnicodeDecodeError:
|
||||
rsp = pickle.loads(rsp, encoding='latin1')
|
||||
if isinstance(rsp, Exception):
|
||||
raise rsp
|
||||
yield rsp
|
||||
|
||||
|
||||
def handle_discovery(pathcomponents, operation, configmanager, inputdata):
|
||||
if pathcomponents[0] == 'detected':
|
||||
pass
|
||||
@@ -711,6 +1077,31 @@ def handle_path(path, operation, configmanager, inputdata=None, autostrip=True):
|
||||
configmanager, inputdata, operation, pathcomponents)
|
||||
elif pathcomponents[0] == 'version':
|
||||
return (msg.Attributes(kv={'version': confluent.__version__}),)
|
||||
elif pathcomponents[0] == 'usergroups':
|
||||
# TODO: when non-administrator accounts exist,
|
||||
# they must only be allowed to see their own user
|
||||
try:
|
||||
usergroup = pathcomponents[1]
|
||||
except IndexError: # it's just users/
|
||||
if operation == 'create':
|
||||
inputdata = msg.get_input_message(
|
||||
pathcomponents, operation, inputdata,
|
||||
configmanager=configmanager)
|
||||
create_usergroup(inputdata.attribs, configmanager)
|
||||
return iterate_collections(configmanager.list_usergroups(),
|
||||
forcecollection=False)
|
||||
if usergroup not in configmanager.list_usergroups():
|
||||
raise exc.NotFoundException("Invalid usergroup %s" % usergroup)
|
||||
if operation == 'retrieve':
|
||||
return show_usergroup(usergroup, configmanager)
|
||||
elif operation == 'delete':
|
||||
return delete_usergroup(usergroup, configmanager)
|
||||
elif operation == 'update':
|
||||
inputdata = msg.get_input_message(
|
||||
pathcomponents, operation, inputdata,
|
||||
configmanager=configmanager)
|
||||
update_usergroup(usergroup, inputdata.attribs, configmanager)
|
||||
return show_usergroup(usergroup, configmanager)
|
||||
elif pathcomponents[0] == 'users':
|
||||
# TODO: when non-administrator accounts exist,
|
||||
# they must only be allowed to see their own user
|
||||
|
||||
@@ -61,11 +61,15 @@
|
||||
# retry until uppercase, lowercase, digit, and symbol all present)
|
||||
# - Apply defined configuration to endpoint
|
||||
|
||||
import base64
|
||||
import confluent.config.configmanager as cfm
|
||||
import confluent.collective.manager as collective
|
||||
import confluent.discovery.protocols.pxe as pxe
|
||||
#import confluent.discovery.protocols.ssdp as ssdp
|
||||
import confluent.discovery.protocols.slp as slp
|
||||
import confluent.discovery.handlers.imm as imm
|
||||
import confluent.discovery.handlers.cpstorage as cpstorage
|
||||
import confluent.discovery.handlers.tsm as tsm
|
||||
import confluent.discovery.handlers.pxe as pxeh
|
||||
import confluent.discovery.handlers.smm as smm
|
||||
import confluent.discovery.handlers.xcc as xcc
|
||||
@@ -75,12 +79,23 @@ import confluent.messages as msg
|
||||
import confluent.networking.macmap as macmap
|
||||
import confluent.noderange as noderange
|
||||
import confluent.util as util
|
||||
import eventlet
|
||||
import traceback
|
||||
webclient = eventlet.import_patched('pyghmi.util.webclient')
|
||||
|
||||
|
||||
import eventlet
|
||||
import eventlet.greenpool
|
||||
import eventlet.semaphore
|
||||
|
||||
autosensors = set()
|
||||
scanner = None
|
||||
|
||||
try:
|
||||
unicode
|
||||
except NameError:
|
||||
unicode = str
|
||||
|
||||
class nesteddict(dict):
|
||||
|
||||
def __missing__(self, key):
|
||||
@@ -92,6 +107,9 @@ nodehandlers = {
|
||||
'service:management-hardware.Lenovo:lenovo-xclarity-controller': xcc,
|
||||
'service:management-hardware.IBM:integrated-management-module2': imm,
|
||||
'pxe-client': pxeh,
|
||||
'service:io-device.Lenovo:management-module': None,
|
||||
'service:thinkagile-storage': cpstorage,
|
||||
'service:lenovo-tsm': tsm,
|
||||
}
|
||||
|
||||
servicenames = {
|
||||
@@ -99,6 +117,9 @@ servicenames = {
|
||||
'service:lenovo-smm': 'lenovo-smm',
|
||||
'service:management-hardware.Lenovo:lenovo-xclarity-controller': 'lenovo-xcc',
|
||||
'service:management-hardware.IBM:integrated-management-module2': 'lenovo-imm2',
|
||||
'service:io-device.Lenovo:management-module': 'lenovo-switch',
|
||||
'service:thinkagile-storage': 'thinkagile-storagebmc',
|
||||
'service:lenovo-tsm': 'lenovo-tsm',
|
||||
}
|
||||
|
||||
servicebyname = {
|
||||
@@ -106,6 +127,9 @@ servicebyname = {
|
||||
'lenovo-smm': 'service:lenovo-smm',
|
||||
'lenovo-xcc': 'service:management-hardware.Lenovo:lenovo-xclarity-controller',
|
||||
'lenovo-imm2': 'service:management-hardware.IBM:integrated-management-module2',
|
||||
'lenovo-switch': 'service:io-device.Lenovo:management-module',
|
||||
'thinkagile-storage': 'service:thinkagile-storagebmc',
|
||||
'lenovo-tsm': 'service:lenovo-tsm',
|
||||
}
|
||||
|
||||
discopool = eventlet.greenpool.GreenPool(500)
|
||||
@@ -145,6 +169,7 @@ known_uuids = nesteddict()
|
||||
known_nodes = nesteddict()
|
||||
unknown_info = {}
|
||||
pending_nodes = {}
|
||||
pending_by_uuid = {}
|
||||
|
||||
|
||||
def enrich_pxe_info(info):
|
||||
@@ -200,6 +225,8 @@ def send_discovery_datum(info):
|
||||
if infotype in servicenames:
|
||||
types.append(servicenames[infotype])
|
||||
yield msg.KeyValueData({'types': types})
|
||||
if 'otheraddresses' in info:
|
||||
yield msg.KeyValueData({'otheripaddrs': list(info['otheraddresses'])})
|
||||
|
||||
|
||||
def _info_matches(info, criteria):
|
||||
@@ -342,7 +369,28 @@ def _parameterize_path(pathcomponents):
|
||||
return validselectors, keyparams, listrequested, childcoll
|
||||
|
||||
|
||||
def handle_autosense_config(operation, inputdata):
|
||||
autosense = cfm.get_global('discovery.autosense')
|
||||
autosense = autosense or autosense is None
|
||||
if operation == 'retrieve':
|
||||
yield msg.KeyValueData({'enabled': autosense})
|
||||
elif operation == 'update':
|
||||
enabled = inputdata['enabled']
|
||||
if type(enabled) in (unicode, bytes):
|
||||
enabled = enabled.lower() in ('true', '1', 'y', 'yes', 'enable',
|
||||
'enabled')
|
||||
if autosense == enabled:
|
||||
return
|
||||
cfm.set_global('discovery.autosense', enabled)
|
||||
if enabled:
|
||||
start_autosense()
|
||||
else:
|
||||
stop_autosense()
|
||||
|
||||
|
||||
def handle_api_request(configmanager, inputdata, operation, pathcomponents):
|
||||
if pathcomponents == ['discovery', 'autosense']:
|
||||
return handle_autosense_config(operation, inputdata)
|
||||
if operation == 'retrieve':
|
||||
return handle_read_api_request(pathcomponents)
|
||||
elif (operation in ('update', 'create') and
|
||||
@@ -351,32 +399,56 @@ def handle_api_request(configmanager, inputdata, operation, pathcomponents):
|
||||
raise exc.InvalidArgumentException()
|
||||
rescan()
|
||||
return (msg.KeyValueData({'rescan': 'started'}),)
|
||||
elif (operation in ('update', 'create')):
|
||||
|
||||
elif operation in ('update', 'create'):
|
||||
if 'node' not in inputdata:
|
||||
raise exc.InvalidArgumentException('Missing node name in input')
|
||||
_, queryparms, _, _ = _parameterize_path(pathcomponents[1:])
|
||||
if 'by-mac' not in queryparms:
|
||||
raise exc.InvalidArgumentException('Must target using "by-mac"')
|
||||
mac = queryparms['by-mac'].replace('-', ':')
|
||||
if mac not in known_info:
|
||||
raise exc.NotFoundException('{0} not found'.format(mac))
|
||||
mac = _get_mac_from_query(pathcomponents)
|
||||
info = known_info[mac]
|
||||
if info['handler'] is None:
|
||||
raise exc.NotImplementedException(
|
||||
'Unable to {0} to {1}'.format(operation,
|
||||
'/'.join(pathcomponents)))
|
||||
handler = info['handler'].NodeHandler(info, configmanager)
|
||||
eval_node(configmanager, handler, info, inputdata['node'],
|
||||
manual=True)
|
||||
try:
|
||||
eval_node(configmanager, handler, info, inputdata['node'],
|
||||
manual=True)
|
||||
except Exception as e:
|
||||
# or... incorrect passworod provided..
|
||||
if 'Incorrect password' in str(e) or 'Unauthorized name' in str(e):
|
||||
return [msg.ConfluentTargetInvalidCredentials(
|
||||
inputdata['node'])]
|
||||
raise
|
||||
return [msg.AssignedResource(inputdata['node'])]
|
||||
elif operation == 'delete':
|
||||
mac = _get_mac_from_query(pathcomponents)
|
||||
del known_info[mac]
|
||||
return [msg.DeletedResource(mac)]
|
||||
raise exc.NotImplementedException(
|
||||
'Unable to {0} to {1}'.format(operation, '/'.join(pathcomponents)))
|
||||
|
||||
|
||||
def _get_mac_from_query(pathcomponents):
|
||||
_, queryparms, _, _ = _parameterize_path(pathcomponents[1:])
|
||||
if 'by-mac' not in queryparms:
|
||||
raise exc.InvalidArgumentException('Must target using "by-mac"')
|
||||
mac = queryparms['by-mac'].replace('-', ':')
|
||||
if mac not in known_info:
|
||||
raise exc.NotFoundException('{0} not found'.format(mac))
|
||||
return mac
|
||||
|
||||
|
||||
def handle_read_api_request(pathcomponents):
|
||||
# TODO(jjohnson2): This should be more generalized...
|
||||
# odd indexes into components are 'by-'*, even indexes
|
||||
# starting at 2 are parameters to previous index
|
||||
if pathcomponents == ['discovery', 'rescan']:
|
||||
return (msg.KeyValueData({'scanning': bool(scanner)}),)
|
||||
subcats, queryparms, indexof, coll = _parameterize_path(pathcomponents[1:])
|
||||
if len(pathcomponents) == 1:
|
||||
dirlist = [msg.ChildCollection(x + '/') for x in sorted(list(subcats))]
|
||||
dirlist.append(msg.ChildCollection('rescan'))
|
||||
dirlist.append(msg.ChildCollection('autosense'))
|
||||
return dirlist
|
||||
if not coll:
|
||||
return show_info(queryparms['by-mac'])
|
||||
@@ -414,6 +486,9 @@ def _recheck_nodes_backend(nodeattribs, configmanager):
|
||||
_map_unique_ids(nodeattribs)
|
||||
# for the nodes whose attributes have changed, consider them as potential
|
||||
# strangers
|
||||
if nodeattribs:
|
||||
macmap.vintage = 0 # expire current mac map data, in case
|
||||
# the attributes changed impacted the result
|
||||
for node in nodeattribs:
|
||||
if node in known_nodes:
|
||||
for somemac in known_nodes[node]:
|
||||
@@ -431,6 +506,8 @@ def _recheck_nodes_backend(nodeattribs, configmanager):
|
||||
for nodename in pending_nodes:
|
||||
info = pending_nodes[nodename]
|
||||
try:
|
||||
if info['handler'] is None:
|
||||
next
|
||||
handler = info['handler'].NodeHandler(info, configmanager)
|
||||
discopool.spawn_n(eval_node, configmanager, handler, info, nodename)
|
||||
except Exception:
|
||||
@@ -440,10 +517,14 @@ def _recheck_nodes_backend(nodeattribs, configmanager):
|
||||
|
||||
|
||||
def _recheck_single_unknown(configmanager, mac):
|
||||
info = unknown_info.get(mac, None)
|
||||
_recheck_single_unknown_info(configmanager, info)
|
||||
|
||||
|
||||
def _recheck_single_unknown_info(configmanager, info):
|
||||
global rechecker
|
||||
global rechecktime
|
||||
info = unknown_info.get(mac, None)
|
||||
if not info:
|
||||
if not info or info['handler'] is None:
|
||||
return
|
||||
if info['handler'] != pxeh and not info.get('addresses', None):
|
||||
#log.log({'info': 'Missing address information in ' + repr(info)})
|
||||
@@ -492,7 +573,7 @@ def _recheck_single_unknown(configmanager, mac):
|
||||
|
||||
|
||||
def safe_detected(info):
|
||||
if 'hwaddr' not in info:
|
||||
if 'hwaddr' not in info or not info['hwaddr']:
|
||||
return
|
||||
if info['hwaddr'] in runningevals:
|
||||
# Do not evaluate the same mac multiple times at once
|
||||
@@ -513,7 +594,7 @@ def detected(info):
|
||||
global rechecktime
|
||||
# later, manual and CMM discovery may act on SN and/or UUID
|
||||
for service in info['services']:
|
||||
if nodehandlers.get(service, None):
|
||||
if service in nodehandlers:
|
||||
if service not in known_services:
|
||||
known_services[service] = set([])
|
||||
handler = nodehandlers[service]
|
||||
@@ -521,6 +602,11 @@ def detected(info):
|
||||
break
|
||||
else: # no nodehandler, ignore for now
|
||||
return
|
||||
if (handler and not handler.NodeHandler.adequate(info) and
|
||||
info.get('protocol', None)):
|
||||
eventlet.spawn_after(10, info['protocol'].fix_info, info,
|
||||
safe_detected)
|
||||
return
|
||||
try:
|
||||
snum = info['attributes']['enclosure-serial-number'][0].strip()
|
||||
if snum:
|
||||
@@ -561,17 +647,21 @@ def detected(info):
|
||||
return
|
||||
known_info[info['hwaddr']] = info
|
||||
cfg = cfm.ConfigManager(None)
|
||||
handler = handler.NodeHandler(info, cfg)
|
||||
handler.scan()
|
||||
if handler:
|
||||
handler = handler.NodeHandler(info, cfg)
|
||||
handler.scan()
|
||||
uuid = info.get('uuid', None)
|
||||
if uuid_is_valid(uuid):
|
||||
known_uuids[uuid][info['hwaddr']] = info
|
||||
if handler.https_supported and not handler.https_cert:
|
||||
info['otheraddresses'] = set([])
|
||||
for i4addr in info.get('attributes', {}).get('ipv4-address', []):
|
||||
info['otheraddresses'].add(i4addr)
|
||||
if handler and handler.https_supported and not handler.https_cert:
|
||||
if handler.cert_fail_reason == 'unreachable':
|
||||
log.log(
|
||||
{
|
||||
'info': '{0} with hwaddr {1} is not reachable at {2}'
|
||||
''.format(
|
||||
'info': '{0} with hwaddr {1} is not reachable by https '
|
||||
'at address {2}'.format(
|
||||
handler.devname, info['hwaddr'], handler.ipaddr
|
||||
)})
|
||||
info['addresses'] = [x for x in info.get('addresses', []) if x != handler.ipaddr]
|
||||
@@ -592,7 +682,7 @@ def detected(info):
|
||||
# influence periodic recheck to shorten delay?
|
||||
return
|
||||
nodename, info['maccount'] = get_nodename(cfg, handler, info)
|
||||
if nodename and handler.https_supported:
|
||||
if nodename and handler and handler.https_supported:
|
||||
dp = cfg.get_node_attributes([nodename],
|
||||
('pubkeys.tls_hardwaremanager',))
|
||||
lastfp = dp.get(nodename, {}).get('pubkeys.tls_hardwaremanager',
|
||||
@@ -605,9 +695,9 @@ def detected(info):
|
||||
#TODO(jjohnson2): We might have to get UUID for certain searches...
|
||||
#for now defer probe until inside eval_node. We might not have
|
||||
#a nodename without probe in the future.
|
||||
if nodename:
|
||||
if nodename and handler:
|
||||
eval_node(cfg, handler, info, nodename)
|
||||
else:
|
||||
elif handler:
|
||||
log.log(
|
||||
{'info': 'Detected unknown {0} with hwaddr {1} at '
|
||||
'address {2}'.format(
|
||||
@@ -617,16 +707,109 @@ def detected(info):
|
||||
unknown_info[info['hwaddr']] = info
|
||||
|
||||
|
||||
|
||||
def b64tohex(b64str):
|
||||
bd = base64.b64decode(b64str)
|
||||
bd = bytearray(bd)
|
||||
return ''.join(['{0:02x}'.format(x) for x in bd])
|
||||
|
||||
|
||||
def get_enclosure_chain_head(nodename, cfg):
|
||||
ne = True
|
||||
members = [nodename]
|
||||
while ne:
|
||||
ne = cfg.get_node_attributes(
|
||||
nodename, 'enclosure.extends').get(nodename, {}).get(
|
||||
'enclosure.extends', {}).get('value', None)
|
||||
if not ne:
|
||||
return nodename
|
||||
if ne in members:
|
||||
raise exc.InvalidArgumentException(
|
||||
'Circular chain that includes ' + nodename)
|
||||
if not cfg.is_node(ne):
|
||||
raise exc.InvalidArgumentException(
|
||||
'{0} is chained to nonexistent node {1} '.format(
|
||||
nodename, ne))
|
||||
nodename = ne
|
||||
members.append(nodename)
|
||||
return nodename
|
||||
|
||||
|
||||
def get_chained_smm_name(nodename, cfg, handler, nl=None, checkswitch=True):
|
||||
# nodename is the head of the chain, cfg is a configmanager, handler
|
||||
# is the handler of the current candidate, nl is optional indication
|
||||
# of the next link in the chain, checkswitch can disable the switch
|
||||
# search if not indicated by current situation
|
||||
# returns the new name and whether it has been securely validated or not
|
||||
# first we check to see if directly connected
|
||||
mycert = handler.https_cert
|
||||
if checkswitch:
|
||||
fprints = macmap.get_node_fingerprints(nodename, cfg)
|
||||
for fprint in fprints:
|
||||
if util.cert_matches(fprint[0], mycert):
|
||||
# ok we have a direct match, it is this node
|
||||
return nodename, fprint[1]
|
||||
# ok, unable to get it, need to traverse the chain from the beginning
|
||||
if not nl:
|
||||
nl = list(cfg.filter_node_attributes(
|
||||
'enclosure.extends=' + nodename))
|
||||
while nl:
|
||||
if len(nl) != 1:
|
||||
raise exc.InvalidArgumentException('Multiple enclosures trying to '
|
||||
'extend a single enclosure')
|
||||
cd = cfg.get_node_attributes(nodename, ['hardwaremanagement.manager',
|
||||
'pubkeys.tls_hardwaremanager'])
|
||||
smmaddr = cd[nodename]['hardwaremanagement.manager']['value']
|
||||
pkey = cd[nodename].get('pubkeys.tls_hardwaremanager', {}).get(
|
||||
'value', None)
|
||||
if not pkey:
|
||||
# We cannot continue through a break in the chain
|
||||
return None, False
|
||||
if pkey:
|
||||
cv = util.TLSCertVerifier(
|
||||
cfg, nodename, 'pubkeys.tls_hardwaremanager').verify_cert
|
||||
for fprint in get_smm_neighbor_fingerprints(smmaddr, cv):
|
||||
if util.cert_matches(fprint, mycert):
|
||||
# a trusted chain member vouched for the cert
|
||||
# so it's validated
|
||||
return nl[0], True
|
||||
# advance down the chain by one and try again
|
||||
nodename = nl[0]
|
||||
nl = list(cfg.filter_node_attributes(
|
||||
'enclosure.extends=' + nodename))
|
||||
return None, False
|
||||
|
||||
|
||||
def get_smm_neighbor_fingerprints(smmaddr, cv):
|
||||
if ':' in smmaddr:
|
||||
smmaddr = '[{0}]'.format(smmaddr)
|
||||
wc = webclient.SecureHTTPConnection(smmaddr, verifycallback=cv)
|
||||
neighs = wc.grab_json_response('/scripts/neighdata.json')
|
||||
if not neighs:
|
||||
return
|
||||
for idx in (4, 5):
|
||||
if 'sha256' not in neighs[idx]:
|
||||
continue
|
||||
yield 'sha256$' + b64tohex(neighs[idx]['sha256'])
|
||||
|
||||
|
||||
def get_nodename(cfg, handler, info):
|
||||
nodename = None
|
||||
maccount = None
|
||||
info['verified'] = False
|
||||
if not handler:
|
||||
return None, None
|
||||
if handler.https_supported:
|
||||
currcert = handler.https_cert
|
||||
if not currcert:
|
||||
info['discofailure'] = 'nohttps'
|
||||
return None, None
|
||||
currprint = util.get_fingerprint(currcert)
|
||||
currprint = util.get_fingerprint(currcert, 'sha256')
|
||||
nodename = nodes_by_fprint.get(currprint, None)
|
||||
if not nodename:
|
||||
# Try SHA512 as well
|
||||
currprint = util.get_fingerprint(currcert)
|
||||
nodename = nodes_by_fprint.get(currprint, None)
|
||||
if not nodename:
|
||||
curruuid = info.get('uuid', None)
|
||||
if uuid_is_valid(curruuid):
|
||||
@@ -634,9 +817,31 @@ def get_nodename(cfg, handler, info):
|
||||
if nodename is None:
|
||||
_map_unique_ids()
|
||||
nodename = nodes_by_uuid.get(curruuid, None)
|
||||
if not nodename: # as a last resort, search switch for info
|
||||
if not nodename:
|
||||
# Ok, see if it is something with a chassis-uuid and discover by
|
||||
# chassis
|
||||
nodename = get_nodename_from_enclosures(cfg, info)
|
||||
if not nodename and handler.devname == 'SMM':
|
||||
nodename = get_nodename_from_chained_smms(cfg, handler, info)
|
||||
if not nodename: # as a last resort, search switches for info
|
||||
# This is the slowest potential operation, so we hope for the
|
||||
# best to occur prior to this
|
||||
nodename, macinfo = macmap.find_nodeinfo_by_mac(info['hwaddr'], cfg)
|
||||
maccount = macinfo['maccount']
|
||||
if nodename:
|
||||
if handler.devname == 'SMM':
|
||||
nl = list(cfg.filter_node_attributes(
|
||||
'enclosure.extends=' + nodename))
|
||||
if nl:
|
||||
# We found an SMM, and it's in a chain per configuration
|
||||
# we need to ask the switch for the fingerprint to see
|
||||
# if we have a match or not
|
||||
newnodename, v = get_chained_smm_name(nodename, cfg,
|
||||
handler, nl)
|
||||
if newnodename:
|
||||
# while this started by switch, it was disambiguated
|
||||
info['verified'] = v
|
||||
return newnodename, None
|
||||
if (nodename and
|
||||
not handler.discoverable_by_switch(macinfo['maccount'])):
|
||||
if handler.devname == 'SMM':
|
||||
@@ -650,13 +855,47 @@ def get_nodename(cfg, handler, info):
|
||||
return nodename, maccount
|
||||
|
||||
|
||||
def get_nodename_from_chained_smms(cfg, handler, info):
|
||||
nodename = None
|
||||
for fprint in get_smm_neighbor_fingerprints(
|
||||
handler.ipaddr, lambda x: True):
|
||||
if fprint in nodes_by_fprint:
|
||||
# need to chase the whole chain
|
||||
# to support either direction
|
||||
chead = get_enclosure_chain_head(nodes_by_fprint[fprint],
|
||||
cfg)
|
||||
newnodename, v = get_chained_smm_name(
|
||||
chead, cfg, handler, checkswitch=False)
|
||||
if newnodename:
|
||||
info['verified'] = v
|
||||
nodename = newnodename
|
||||
return nodename
|
||||
|
||||
|
||||
def get_nodename_from_enclosures(cfg, info):
|
||||
nodename = None
|
||||
cuuid = info.get('attributes', {}).get('chassis-uuid', [None])[0]
|
||||
if cuuid and cuuid in nodes_by_uuid:
|
||||
encl = nodes_by_uuid[cuuid]
|
||||
bay = info.get('enclosure.bay', None)
|
||||
if bay:
|
||||
tnl = cfg.filter_node_attributes('enclosure.manager=' + encl)
|
||||
tnl = list(
|
||||
cfg.filter_node_attributes('enclosure.bay={0}'.format(bay),
|
||||
tnl))
|
||||
if len(tnl) == 1:
|
||||
# This is not a secure assurance, because it's by
|
||||
# uuid instead of a key
|
||||
nodename = tnl[0]
|
||||
return nodename
|
||||
|
||||
|
||||
def eval_node(cfg, handler, info, nodename, manual=False):
|
||||
try:
|
||||
handler.probe() # unicast interrogation as possible to get more data
|
||||
# for now, we search switch only, ideally we search cmm, smm, and
|
||||
# switch concurrently
|
||||
# do some preconfig, for example, to bring a SMM online if applicable
|
||||
handler.preconfig()
|
||||
handler.preconfig(nodename)
|
||||
except Exception as e:
|
||||
unknown_info[info['hwaddr']] = info
|
||||
info['discostatus'] = 'unidentified'
|
||||
@@ -684,6 +923,7 @@ def eval_node(cfg, handler, info, nodename, manual=False):
|
||||
if not handler.is_enclosure and nl:
|
||||
# The specified node is an enclosure (has nodes mapped to it), but
|
||||
# what we are talking to is *not* an enclosure
|
||||
# might be ambiguous, need to match chassis-uuid as well..
|
||||
if 'enclosure.bay' not in info:
|
||||
unknown_info[info['hwaddr']] = info
|
||||
info['discostatus'] = 'unidentified'
|
||||
@@ -695,14 +935,39 @@ def eval_node(cfg, handler, info, nodename, manual=False):
|
||||
raise exc.InvalidArgumentException(errorstr)
|
||||
log.log({'error': errorstr})
|
||||
return
|
||||
enl = list(cfg.filter_node_attributes('enclosure.extends=' + nodename))
|
||||
if enl:
|
||||
# ambiguous SMM situation according to the configuration, we need
|
||||
# to match uuid
|
||||
encuuid = info['attributes'].get('chassis-uuid', None)
|
||||
if encuuid:
|
||||
encuuid = encuuid[0]
|
||||
enl = list(cfg.filter_node_attributes('id.uuid=' + encuuid))
|
||||
if len(enl) != 1:
|
||||
# errorstr = 'No SMM by given UUID known, *yet*'
|
||||
# if manual:
|
||||
# raise exc.InvalidArgumentException(errorstr)
|
||||
# log.log({'error': errorstr})
|
||||
if encuuid in pending_by_uuid:
|
||||
pending_by_uuid[encuuid].append(info)
|
||||
else:
|
||||
pending_by_uuid[encuuid] = [info]
|
||||
return
|
||||
# We found the real smm, replace the list with the actual smm
|
||||
# to continue
|
||||
nl = list(cfg.filter_node_attributes(
|
||||
'enclosure.manager=' + enl[0]))
|
||||
else:
|
||||
errorstr = 'Chained SMM configuration with older XCC, ' \
|
||||
'unable to perform zero power discovery'
|
||||
if manual:
|
||||
raise exc.InvalidArgumentException(errorstr)
|
||||
log.log({'error': errorstr})
|
||||
return
|
||||
# search for nodes fitting our description using filters
|
||||
# lead with the most specific to have a small second pass
|
||||
nl = cfg.filter_node_attributes(
|
||||
'enclosure.bay={0}'.format(info['enclosure.bay']), nl)
|
||||
nl = list(nl)
|
||||
# sadly, we cannot detect when user has a daisy chain smm config
|
||||
# without ability to disambiguate, however the SMM will be caught so
|
||||
# at least one actionable error will be encountered.
|
||||
nl = list(cfg.filter_node_attributes(
|
||||
'enclosure.bay={0}'.format(info['enclosure.bay']), nl))
|
||||
if len(nl) != 1:
|
||||
info['discofailure'] = 'ambigconfig'
|
||||
if len(nl):
|
||||
@@ -731,16 +996,26 @@ def eval_node(cfg, handler, info, nodename, manual=False):
|
||||
# we can and did accurately discover by switch or in enclosure
|
||||
# but... is this really ok? could be on an upstream port or
|
||||
# erroneously put in the enclosure with no nodes yet
|
||||
if (info['maccount'] and
|
||||
not handler.discoverable_by_switch(info['maccount'])):
|
||||
errorstr = 'The detected node {0} was detected using switch, ' \
|
||||
'however the relevant port has too many macs learned ' \
|
||||
'for this type of device ({1}) to be discovered by ' \
|
||||
'switch.'.format(nodename, handler.devname)
|
||||
if manual:
|
||||
raise exc.InvalidArgumentException(errorstr)
|
||||
log.log({'error': errorstr})
|
||||
return
|
||||
# so first, see if the candidate node is a chain host
|
||||
if not manual:
|
||||
if info.get('maccount', False):
|
||||
# discovery happened through switch
|
||||
nl = list(cfg.filter_node_attributes(
|
||||
'enclosure.extends=' + nodename))
|
||||
if nl:
|
||||
# The candidate nodename is the head of a chain, we must
|
||||
# validate the smm certificate by the switch
|
||||
macmap.get_node_fingerprint(nodename, cfg)
|
||||
util.handler.cert_matches(fprint, handler.https_cert)
|
||||
return
|
||||
if (info.get('maccount', False) and
|
||||
not handler.discoverable_by_switch(info['maccount'])):
|
||||
errorstr = 'The detected node {0} was detected using switch, ' \
|
||||
'however the relevant port has too many macs learned ' \
|
||||
'for this type of device ({1}) to be discovered by ' \
|
||||
'switch.'.format(nodename, handler.devname)
|
||||
log.log({'error': errorstr})
|
||||
return
|
||||
if not discover_node(cfg, handler, info, nodename, manual):
|
||||
pending_nodes[nodename] = info
|
||||
|
||||
@@ -764,6 +1039,7 @@ def discover_node(cfg, handler, info, nodename, manual):
|
||||
# the pubkeys, which is deferred for a little bit
|
||||
# Also, 'secure', when we have the needed infrastructure done
|
||||
# in some product or another.
|
||||
curruuid = info.get('uuid', False)
|
||||
if 'pxe' in policies and info['handler'] == pxeh:
|
||||
return do_pxe_discovery(cfg, handler, info, manual, nodename, policies)
|
||||
elif ('permissive' in policies and handler.https_supported and lastfp and
|
||||
@@ -794,6 +1070,14 @@ def discover_node(cfg, handler, info, nodename, manual):
|
||||
traceback.print_exc()
|
||||
return False
|
||||
newnodeattribs = {}
|
||||
if cfm.list_collective():
|
||||
# We are in a collective, check collective.manager
|
||||
cmc = cfg.get_node_attributes(nodename, 'collective.manager')
|
||||
cm = cmc.get(nodename, {}).get('collective.manager', {}).get('value', None)
|
||||
if not cm:
|
||||
# Node is being discovered in collective, but no collective.manager, default
|
||||
# to the collective member actually able to execute the discovery
|
||||
newnodeattribs['collective.manager'] = collective.get_myname()
|
||||
if 'uuid' in info:
|
||||
newnodeattribs['id.uuid'] = info['uuid']
|
||||
if 'serialnumber' in info:
|
||||
@@ -802,12 +1086,18 @@ def discover_node(cfg, handler, info, nodename, manual):
|
||||
newnodeattribs['id.model'] = info['modelnumber']
|
||||
if handler.https_cert:
|
||||
newnodeattribs['pubkeys.tls_hardwaremanager'] = \
|
||||
util.get_fingerprint(handler.https_cert)
|
||||
util.get_fingerprint(handler.https_cert, 'sha256')
|
||||
if newnodeattribs:
|
||||
cfg.set_node_attributes({nodename: newnodeattribs})
|
||||
log.log({'info': 'Discovered {0} ({1})'.format(nodename,
|
||||
handler.devname)})
|
||||
info['discostatus'] = 'discovered'
|
||||
for i in pending_by_uuid.get(curruuid, []):
|
||||
eventlet.spawn_n(_recheck_single_unknown_info, cfg, i)
|
||||
try:
|
||||
del pending_by_uuid[curruuid]
|
||||
except KeyError:
|
||||
pass
|
||||
return True
|
||||
log.log({'info': 'Detected {0}, but discovery.policy is not set to a '
|
||||
'value allowing discovery (open or permissive)'.format(
|
||||
@@ -857,6 +1147,7 @@ needaddhandled = False
|
||||
def _handle_nodelist_change(configmanager):
|
||||
global needaddhandled
|
||||
global nodeaddhandler
|
||||
macmap.vintage = 0 # the current mac map is probably inaccurate
|
||||
_recheck_nodes((), configmanager)
|
||||
if needaddhandled:
|
||||
needaddhandled = False
|
||||
@@ -865,16 +1156,26 @@ def _handle_nodelist_change(configmanager):
|
||||
nodeaddhandler = None
|
||||
|
||||
|
||||
def newnodes(added, deleting, configmanager):
|
||||
def newnodes(added, deleting, renamed, configmanager):
|
||||
global attribwatcher
|
||||
global needaddhandled
|
||||
global nodeaddhandler
|
||||
alldeleting = set(deleting) | set(renamed)
|
||||
for node in alldeleting:
|
||||
if node not in known_nodes:
|
||||
continue
|
||||
for mac in known_nodes[node]:
|
||||
if mac in known_info:
|
||||
del known_info[mac]
|
||||
del known_nodes[node]
|
||||
_map_unique_ids()
|
||||
configmanager.remove_watcher(attribwatcher)
|
||||
allnodes = configmanager.list_nodes()
|
||||
attribwatcher = configmanager.watch_attributes(
|
||||
allnodes, ('discovery.policy', 'net*.switch',
|
||||
'hardwaremanagement.manager', 'net*.switchport', 'id.uuid',
|
||||
'pubkeys.tls_hardwaremanager', 'net*.bootable'), _recheck_nodes)
|
||||
'hardwaremanagement.manager', 'net*.switchport',
|
||||
'id.uuid', 'pubkeys.tls_hardwaremanager',
|
||||
'net*.bootable'), _recheck_nodes)
|
||||
if nodeaddhandler:
|
||||
needaddhandled = True
|
||||
else:
|
||||
@@ -906,12 +1207,17 @@ def _periodic_recheck(configmanager):
|
||||
|
||||
def rescan():
|
||||
_map_unique_ids()
|
||||
eventlet.spawn_n(slp.active_scan, safe_detected)
|
||||
global scanner
|
||||
if scanner:
|
||||
return
|
||||
else:
|
||||
scanner = eventlet.spawn(slp.active_scan, safe_detected, slp)
|
||||
|
||||
|
||||
def start_detection():
|
||||
global attribwatcher
|
||||
global rechecker
|
||||
global rechecktime
|
||||
_map_unique_ids()
|
||||
cfg = cfm.ConfigManager(None)
|
||||
allnodes = cfg.list_nodes()
|
||||
@@ -920,14 +1226,23 @@ def start_detection():
|
||||
'hardwaremanagement.manager', 'net*.switchport', 'id.uuid',
|
||||
'pubkeys.tls_hardwaremanager'), _recheck_nodes)
|
||||
cfg.watch_nodecollection(newnodes)
|
||||
eventlet.spawn_n(slp.snoop, safe_detected)
|
||||
eventlet.spawn_n(pxe.snoop, safe_detected)
|
||||
autosense = cfm.get_global('discovery.autosense')
|
||||
if autosense or autosense is None:
|
||||
start_autosense()
|
||||
if rechecker is None:
|
||||
rechecktime = util.monotonic_time() + 900
|
||||
rechecker = eventlet.spawn_after(900, _periodic_recheck, cfg)
|
||||
|
||||
# eventlet.spawn_n(ssdp.snoop, safe_detected)
|
||||
|
||||
def stop_autosense():
|
||||
for watcher in list(autosensors):
|
||||
watcher.kill()
|
||||
autosensors.discard(watcher)
|
||||
|
||||
def start_autosense():
|
||||
autosensors.add(eventlet.spawn(slp.snoop, safe_detected, slp))
|
||||
autosensors.add(eventlet.spawn(pxe.snoop, safe_detected, pxe))
|
||||
|
||||
|
||||
nodes_by_fprint = {}
|
||||
@@ -949,7 +1264,7 @@ def _map_unique_ids(nodes=None):
|
||||
uuid_by_nodes = {}
|
||||
fprint_by_nodes = {}
|
||||
for uuid in nodes_by_uuid:
|
||||
if not uuid_is_valid():
|
||||
if not uuid_is_valid(uuid):
|
||||
continue
|
||||
node = nodes_by_uuid[uuid]
|
||||
if node in bigmap:
|
||||
|
||||
@@ -26,16 +26,27 @@ ipmicommand.session.threading = eventlet.green.threading
|
||||
ipmicommand.session.socket.getaddrinfo = eventlet.support.greendns.getaddrinfo
|
||||
getaddrinfo = eventlet.support.greendns.getaddrinfo
|
||||
|
||||
DEFAULT_USER = 'USERID'
|
||||
DEFAULT_PASS = 'PASSW0RD'
|
||||
|
||||
|
||||
class NodeHandler(generic.NodeHandler):
|
||||
DEFAULT_USER = 'USERID'
|
||||
DEFAULT_PASS = 'PASSW0RD'
|
||||
|
||||
def _get_ipmicmd(self, user=DEFAULT_USER, password=DEFAULT_PASS):
|
||||
return ipmicommand.Command(self.ipaddr, user, password)
|
||||
def _get_ipmicmd(self, user=None, password=None):
|
||||
priv = None
|
||||
if user is None or password is None:
|
||||
if self.trieddefault:
|
||||
raise pygexc.IpmiException()
|
||||
priv = 4 # manually indicate priv to avoid double-attempt
|
||||
if user is None:
|
||||
user = self.DEFAULT_USER
|
||||
if password is None:
|
||||
password = self.DEFAULT_PASS
|
||||
return ipmicommand.Command(self.ipaddr, user, password,
|
||||
privlevel=priv, keepalive=False)
|
||||
|
||||
def __init__(self, info, configmanager):
|
||||
self.trieddefault = None
|
||||
super(NodeHandler, self).__init__(info, configmanager)
|
||||
|
||||
def probe(self):
|
||||
@@ -45,35 +56,37 @@ class NodeHandler(generic.NodeHandler):
|
||||
def config(self, nodename, reset=False):
|
||||
self._bmcconfig(nodename, reset)
|
||||
|
||||
def _bmcconfig(self, nodename, reset=False):
|
||||
def _bmcconfig(self, nodename, reset=False, customconfig=None, vc=None):
|
||||
# TODO(jjohnson2): set ip parameters, user/pass, alert cfg maybe
|
||||
# In general, try to use https automation, to make it consistent
|
||||
# between hypothetical secure path and today.
|
||||
creds = self.configmanager.get_node_attributes(
|
||||
nodename,
|
||||
['secret.hardwaremanagementuser',
|
||||
'secret.hardwaremanagementpassword'], decrypt=True)
|
||||
user = creds.get(nodename, {}).get(
|
||||
'secret.hardwaremanagementuser', {}).get('value', None)
|
||||
passwd = creds.get(nodename, {}).get(
|
||||
'secret.hardwaremanagementpassword', {}).get('value', None)
|
||||
try:
|
||||
ic = self._get_ipmicmd()
|
||||
passwd = DEFAULT_PASS
|
||||
passwd = self.DEFAULT_PASS
|
||||
except pygexc.IpmiException as pi:
|
||||
creds = self.configmanager.get_node_attributes(
|
||||
nodename,
|
||||
['secret.hardwaremanagementuser',
|
||||
'secret.hardwaremanagementpassword'], decrypt=True)
|
||||
user = creds.get(nodename, {}).get(
|
||||
'secret.hardwaremanagementuser', {}).get('value', None)
|
||||
havecustomcreds = False
|
||||
if user is not None and user != DEFAULT_USER:
|
||||
if user is not None and user != self.DEFAULT_USER:
|
||||
havecustomcreds = True
|
||||
else:
|
||||
user = DEFAULT_USER
|
||||
passwd = creds.get(nodename, {}).get(
|
||||
'secret.hardwaremanagementpassword', {}).get('value', None)
|
||||
if passwd is not None and passwd != DEFAULT_PASS:
|
||||
user = self.DEFAULT_USER
|
||||
if passwd is not None and passwd != self.DEFAULT_PASS:
|
||||
havecustomcreds = True
|
||||
else:
|
||||
passwd = DEFAULT_PASS
|
||||
passwd = self.DEFAULT_PASS
|
||||
if havecustomcreds:
|
||||
ic = self._get_ipmicmd(user, passwd)
|
||||
else:
|
||||
raise
|
||||
if vc:
|
||||
ic.register_key_handler(vc)
|
||||
currusers = ic.get_users()
|
||||
lanchan = ic.get_network_channel()
|
||||
userdata = ic.xraw_command(netfn=6, command=0x44, data=(lanchan,
|
||||
@@ -93,45 +106,33 @@ class NodeHandler(generic.NodeHandler):
|
||||
raise exc.TargetEndpointBadCredentials(
|
||||
'secret.hardwaremanagementuser and/or '
|
||||
'secret.hardwaremanagementpassword was not configured')
|
||||
if ('hardwaremanagement.manager' in cd and
|
||||
cd['hardwaremanagement.manager']['value'] and
|
||||
not cd['hardwaremanagement.manager']['value'].startswith(
|
||||
'fe80::')):
|
||||
newip = cd['hardwaremanagement.manager']['value']
|
||||
newipinfo = getaddrinfo(newip, 0)[0]
|
||||
# This getaddrinfo is repeated in get_nic_config, could be
|
||||
# optimized, albeit with a more convoluted api..
|
||||
newip = newipinfo[-1][0]
|
||||
if ':' in newip:
|
||||
raise exc.NotImplementedException('IPv6 remote config TODO')
|
||||
netconfig = netutil.get_nic_config(cfg, nodename, ip=newip)
|
||||
plen = netconfig['prefix']
|
||||
newip = '{0}/{1}'.format(newip, plen)
|
||||
ic.set_net_configuration(ipv4_address=newip,
|
||||
ipv4_configuration='static',
|
||||
ipv4_gateway=netconfig['ipv4_gateway'])
|
||||
elif self.ipaddr.startswith('fe80::'):
|
||||
cfg.set_node_attributes(
|
||||
{nodename: {'hardwaremanagement.manager': self.ipaddr}})
|
||||
else:
|
||||
raise exc.TargetEndpointUnreachable(
|
||||
'hardwaremanagement.manager must be set to desired address')
|
||||
newuser = cd['secret.hardwaremanagementuser']['value']
|
||||
newpass = cd['secret.hardwaremanagementpassword']['value']
|
||||
for uid in currusers:
|
||||
if currusers[uid]['name'] == newuser:
|
||||
# Use existing account that has been created
|
||||
newuserslot = uid
|
||||
if newpass != passwd: # don't mess with existing if no change
|
||||
ic.set_user_password(newuserslot, password=newpass)
|
||||
ic = self._get_ipmicmd(user, passwd)
|
||||
if vc:
|
||||
ic.register_key_handler(vc)
|
||||
break
|
||||
else:
|
||||
newuserslot = lockedusers + 1
|
||||
if newuserslot < 2:
|
||||
newuserslot = 2
|
||||
if newpass != passwd: # don't mess with existing if no change
|
||||
ic.set_user_password(newuserslot, password=newpass)
|
||||
ic.set_user_name(newuserslot, newuser)
|
||||
ic.set_user_access(newuserslot, lanchan,
|
||||
privilege_level='administrator')
|
||||
if newpass != passwd: # don't mess with existing if no change
|
||||
ic.set_user_password(newuserslot, password=newpass)
|
||||
if havecustomcreds:
|
||||
ic = self._get_ipmicmd(user, passwd)
|
||||
if vc:
|
||||
ic.register_key_handler(vc)
|
||||
#We are remote operating on the account we are
|
||||
#using, no need to try to set user access
|
||||
#ic.set_user_access(newuserslot, lanchan,
|
||||
# privilege_level='administrator')
|
||||
# Now to zap others
|
||||
for uid in currusers:
|
||||
if uid != newuserslot:
|
||||
@@ -152,6 +153,36 @@ class NodeHandler(generic.NodeHandler):
|
||||
# name...
|
||||
# the user will remain, but that is life
|
||||
raise
|
||||
if customconfig:
|
||||
customconfig(ic)
|
||||
if ('hardwaremanagement.manager' in cd and
|
||||
cd['hardwaremanagement.manager']['value'] and
|
||||
not cd['hardwaremanagement.manager']['value'].startswith(
|
||||
'fe80::')):
|
||||
newip = cd['hardwaremanagement.manager']['value']
|
||||
newipinfo = getaddrinfo(newip, 0)[0]
|
||||
# This getaddrinfo is repeated in get_nic_config, could be
|
||||
# optimized, albeit with a more convoluted api..
|
||||
newip = newipinfo[-1][0]
|
||||
if ':' in newip:
|
||||
raise exc.NotImplementedException('IPv6 remote config TODO')
|
||||
netconfig = netutil.get_nic_config(cfg, nodename, ip=newip)
|
||||
plen = netconfig['prefix']
|
||||
newip = '{0}/{1}'.format(newip, plen)
|
||||
currcfg = ic.get_net_configuration()
|
||||
if currcfg['ipv4_address'] != newip:
|
||||
# do not change the ipv4_config if the current config looks
|
||||
# like it is already accurate
|
||||
ic.set_net_configuration(ipv4_address=newip,
|
||||
ipv4_configuration='static',
|
||||
ipv4_gateway=netconfig[
|
||||
'ipv4_gateway'])
|
||||
elif self.ipaddr.startswith('fe80::'):
|
||||
cfg.set_node_attributes(
|
||||
{nodename: {'hardwaremanagement.manager': self.ipaddr}})
|
||||
else:
|
||||
raise exc.TargetEndpointUnreachable(
|
||||
'hardwaremanagement.manager must be set to desired address')
|
||||
if reset:
|
||||
ic.reset_bmc()
|
||||
return ic
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
# Copyright 2019 Lenovo
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import confluent.discovery.handlers.bmc as bmchandler
|
||||
|
||||
class NodeHandler(bmchandler.NodeHandler):
|
||||
DEFAULT_USER = 'admin'
|
||||
DEFAULT_PASS = 'admin'
|
||||
devname = 'BMC'
|
||||
maxmacs = 2
|
||||
@@ -40,6 +40,12 @@ class NodeHandler(object):
|
||||
targsa = info['addresses'][0]
|
||||
self.ipaddr = targsa[0]
|
||||
|
||||
@classmethod
|
||||
def adequate(cls, info):
|
||||
# Check if the referenced info is really enough, if false, a rescan
|
||||
# may occur against the target in a short while
|
||||
return True
|
||||
|
||||
def scan(self):
|
||||
# Do completely passive things to enhance data.
|
||||
# Probe is permitted to for example attempt a login
|
||||
@@ -51,7 +57,7 @@ class NodeHandler(object):
|
||||
# serial number and uuid to flesh out data as needed
|
||||
return
|
||||
|
||||
def preconfig(self):
|
||||
def preconfig(self, possiblenode):
|
||||
return
|
||||
|
||||
def discoverable_by_switch(self, macs):
|
||||
@@ -63,6 +69,27 @@ class NodeHandler(object):
|
||||
self._fp = certificate
|
||||
return True
|
||||
|
||||
def get_node_credentials(self, nodename, creds, defuser, defpass):
|
||||
user = creds.get(nodename, {}).get(
|
||||
'secret.hardwaremanagementuser', {}).get('value', None)
|
||||
havecustomcreds = False
|
||||
if user and not isinstance(user, str):
|
||||
user = user.decode('utf8')
|
||||
if user is not None and user != defuser:
|
||||
havecustomcreds = True
|
||||
else:
|
||||
user = defuser
|
||||
passwd = creds.get(nodename, {}).get(
|
||||
'secret.hardwaremanagementpassword', {}).get('value', None)
|
||||
if passwd and not isinstance(passwd, str):
|
||||
passwd = passwd.decode('utf8')
|
||||
if passwd is not None and passwd != defpass:
|
||||
havecustomcreds = True
|
||||
else:
|
||||
passwd = defpass
|
||||
return user, passwd, not havecustomcreds
|
||||
|
||||
|
||||
@property
|
||||
def cert_fail_reason(self):
|
||||
if self._certfailreason == 1:
|
||||
|
||||
@@ -12,15 +12,22 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import codecs
|
||||
import confluent.discovery.handlers.bmc as bmchandler
|
||||
import pyghmi.exceptions as pygexc
|
||||
import pyghmi.ipmi.private.util as pygutil
|
||||
import string
|
||||
import confluent.util as util
|
||||
import struct
|
||||
|
||||
class NodeHandler(bmchandler.NodeHandler):
|
||||
devname = 'IMM'
|
||||
|
||||
@classmethod
|
||||
def adequate(cls, info):
|
||||
# We can sometimes receive a partially initialized SLP packet
|
||||
# This is not adequate for being satisfied
|
||||
return bool(info.get('attributes', {}))
|
||||
|
||||
def scan(self):
|
||||
slpattrs = self.info.get('attributes', {})
|
||||
self.isdense = False
|
||||
@@ -32,13 +39,14 @@ class NodeHandler(bmchandler.NodeHandler):
|
||||
if wronguuid:
|
||||
# we need to fix the first three portions of the uuid
|
||||
uuidprefix = wronguuid.split('-')[:3]
|
||||
uuidprefix = struct.pack(
|
||||
'<IHH', *[int(x, 16) for x in uuidprefix]).encode('hex')
|
||||
uuidprefix = codecs.encode(struct.pack(
|
||||
'<IHH', *[int(x, 16) for x in uuidprefix]), 'hex')
|
||||
uuidprefix = util.stringify(uuidprefix)
|
||||
uuidprefix = uuidprefix[:8] + '-' + uuidprefix[8:12] + '-' + \
|
||||
uuidprefix[12:16]
|
||||
self.info['uuid'] = uuidprefix + '-' + '-'.join(
|
||||
wronguuid.split('-')[3:])
|
||||
self.info['uuid'] = string.lower(self.info['uuid'])
|
||||
self.info['uuid'] = self.info['uuid'].lower()
|
||||
if ff not in ('dense-computing', 'BC2'):
|
||||
# do not probe unless it's a dense platform
|
||||
return
|
||||
|
||||
@@ -12,21 +12,36 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import codecs
|
||||
import confluent.discovery.handlers.bmc as bmchandler
|
||||
import confluent.exceptions as exc
|
||||
import eventlet
|
||||
webclient = eventlet.import_patched('pyghmi.util.webclient')
|
||||
import struct
|
||||
try:
|
||||
from urllib import urlencode
|
||||
except ImportError:
|
||||
from urllib.parse import urlencode
|
||||
import eventlet.support.greendns
|
||||
import confluent.netutil as netutil
|
||||
import confluent.util as util
|
||||
getaddrinfo = eventlet.support.greendns.getaddrinfo
|
||||
|
||||
from xml.etree.ElementTree import fromstring
|
||||
|
||||
def fixuuid(baduuid):
|
||||
# SMM dumps it out in hex
|
||||
uuidprefix = (baduuid[:8], baduuid[8:12], baduuid[12:16])
|
||||
a = struct.pack('<IHH', *[int(x, 16) for x in uuidprefix]).encode(
|
||||
a = codecs.encode(struct.pack('<IHH', *[int(x, 16) for x in uuidprefix]),
|
||||
'hex')
|
||||
a = util.stringify(a)
|
||||
uuid = (a[:8], a[8:12], a[12:16], baduuid[16:20], baduuid[20:])
|
||||
return '-'.join(uuid).lower()
|
||||
|
||||
class NodeHandler(bmchandler.NodeHandler):
|
||||
is_enclosure = True
|
||||
devname = 'SMM'
|
||||
maxmacs = 5 # support an enclosure, but try to avoid catching daisy chain
|
||||
maxmacs = 6 # support an enclosure, but try to avoid catching daisy chain
|
||||
|
||||
def scan(self):
|
||||
# the UUID is in a weird order, fix it up to match
|
||||
@@ -36,9 +51,151 @@ class NodeHandler(bmchandler.NodeHandler):
|
||||
uuid = fixuuid(uuid[0])
|
||||
self.info['uuid'] = uuid
|
||||
|
||||
def _validate_cert(self, certificate):
|
||||
# Assumption is by the time we call config, that discovery core has
|
||||
# vetted self._fp. Our job here then is just to make sure that
|
||||
# the currect connection matches the previously saved cert
|
||||
if not self._fp: # circumstances are that we haven't validated yet
|
||||
self._fp = certificate
|
||||
return certificate == self._fp
|
||||
|
||||
def _webconfigrules(self, wc):
|
||||
rules = []
|
||||
for rule in self.ruleset.split(','):
|
||||
if '=' not in rule:
|
||||
continue
|
||||
name, value = rule.split('=')
|
||||
if value.lower() in ('no', 'none', 'disable', 'disabled'):
|
||||
value = '0'
|
||||
if name.lower() in ('expiry', 'expiration'):
|
||||
rules.append('passwordDurationDays:' + value)
|
||||
warndays = '5' if int(value) > 5 else value
|
||||
rules.append('passwordExpireWarningDays:' + warndays)
|
||||
if name.lower() in ('lockout', 'loginfailures'):
|
||||
rules.append('passwordFailAllowdNum:' + value)
|
||||
if name.lower() == 'reuse':
|
||||
rules.append('passwordReuseCheckNum:' + value)
|
||||
if rules:
|
||||
apirequest = 'set={0}'.format(','.join(rules))
|
||||
wc.request('POST', '/data', apirequest)
|
||||
wc.getresponse().read()
|
||||
|
||||
def _webconfignet(self, wc, nodename):
|
||||
cfg = self.configmanager
|
||||
cd = cfg.get_node_attributes(
|
||||
nodename, ['hardwaremanagement.manager'])
|
||||
smmip = cd.get(nodename, {}).get('hardwaremanagement.manager', {}).get('value', None)
|
||||
if smmip and ':' not in smmip:
|
||||
smmip = getaddrinfo(smmip, 0)[0]
|
||||
smmip = smmip[-1][0]
|
||||
if smmip and ':' in smmip:
|
||||
raise exc.NotImplementedException('IPv6 not supported')
|
||||
netconfig = netutil.get_nic_config(cfg, nodename, ip=smmip)
|
||||
netmask = netutil.cidr_to_mask(netconfig['prefix'])
|
||||
setdata = 'set=ifIndex:0,v4DHCPEnabled:0,v4IPAddr:{0},v4NetMask:{1}'.format(smmip, netmask)
|
||||
gateway = netconfig.get('ipv4_gateway', None)
|
||||
if gateway:
|
||||
setdata += ',v4Gateway:{0}'.format(gateway)
|
||||
wc.request('POST', '/data', setdata)
|
||||
rsp = wc.getresponse()
|
||||
rspdata = util.stringify(rsp.read())
|
||||
if '<statusCode>0' not in rspdata:
|
||||
raise Exception("Error configuring SMM Network")
|
||||
return
|
||||
if smmip and ':' in smmip and not smmip.startswith('fe80::'):
|
||||
raise exc.NotImplementedException('IPv6 configuration TODO')
|
||||
if self.ipaddr.startswith('fe80::'):
|
||||
cfg.set_node_attributes(
|
||||
{nodename: {'hardwaremanagement.manager': self.ipaddr}})
|
||||
|
||||
def _webconfigcreds(self, username, password):
|
||||
wc = webclient.SecureHTTPConnection(self.ipaddr, 443, verifycallback=self._validate_cert)
|
||||
wc.connect()
|
||||
authdata = { # start by trying factory defaults
|
||||
'user': 'USERID',
|
||||
'password': 'PASSW0RD',
|
||||
}
|
||||
headers = {'Connection': 'keep-alive', 'Content-Type': 'application/x-www-form-urlencoded'}
|
||||
wc.request('POST', '/data/login', urlencode(authdata), headers)
|
||||
rsp = wc.getresponse()
|
||||
rspdata = util.stringify(rsp.read())
|
||||
if 'authResult>0' not in rspdata:
|
||||
# default credentials are refused, try with the actual
|
||||
authdata['user'] = username
|
||||
authdata['password'] = password
|
||||
wc.request('POST', '/data/login', urlencode(authdata), headers)
|
||||
rsp = wc.getresponse()
|
||||
rspdata = util.stringify(rsp.read())
|
||||
if 'renew_account' in rspdata:
|
||||
raise Exception('Configured password has expired')
|
||||
if 'authResult>0' not in rspdata:
|
||||
raise Exception('Unknown username/password on SMM')
|
||||
tokens = fromstring(rspdata)
|
||||
st2 = tokens.findall('st2')[0].text
|
||||
wc.set_header('ST2', st2)
|
||||
return wc
|
||||
if 'renew_account' in rspdata:
|
||||
passwdchange = {'oripwd': 'PASSW0RD', 'newpwd': password}
|
||||
tokens = fromstring(rspdata)
|
||||
st2 = tokens.findall('st2')[0].text
|
||||
wc.set_header('ST2', st2)
|
||||
wc.request('POST', '/data/changepwd', urlencode(passwdchange))
|
||||
rsp = wc.getresponse()
|
||||
rspdata = rsp.read()
|
||||
authdata['password'] = password
|
||||
wc.request('POST', '/data/login', urlencode(authdata), headers)
|
||||
rsp = wc.getresponse()
|
||||
rspdata = util.stringify(rsp.read())
|
||||
if 'authResult>0' in rspdata:
|
||||
tokens = fromstring(rspdata)
|
||||
st2 = tokens.findall('st2')[0].text
|
||||
wc.set_header('ST2', st2)
|
||||
if username == 'USERID':
|
||||
return wc
|
||||
wc.request('POST', '/data', 'set=user(2,1,{0},511,,4,15,0)'.format(username))
|
||||
rsp = wc.getresponse()
|
||||
rspdata = rsp.read()
|
||||
wc.request('POST', '/data/logout')
|
||||
rsp = wc.getresponse()
|
||||
rspdata = rsp.read()
|
||||
authdata['user'] = username
|
||||
wc.request('POST', '/data/login', urlencode(authdata, headers))
|
||||
rsp = wc.getresponse()
|
||||
rspdata = rsp.read()
|
||||
tokens = fromstring(rspdata)
|
||||
st2 = tokens.findall('st2')[0].text
|
||||
wc.set_header('ST2', st2)
|
||||
return wc
|
||||
|
||||
def config(self, nodename):
|
||||
# SMM for now has to reset to assure configuration applies
|
||||
super(NodeHandler, self).config(nodename)
|
||||
dpp = self.configmanager.get_node_attributes(
|
||||
nodename, 'discovery.passwordrules')
|
||||
self.ruleset = dpp.get(nodename, {}).get(
|
||||
'discovery.passwordrules', {}).get('value', '')
|
||||
creds = self.configmanager.get_node_attributes(
|
||||
nodename,
|
||||
['secret.hardwaremanagementuser',
|
||||
'secret.hardwaremanagementpassword'], decrypt=True)
|
||||
username = creds.get(nodename, {}).get(
|
||||
'secret.hardwaremanagementuser', {}).get('value', 'USERID')
|
||||
passwd = creds.get(nodename, {}).get(
|
||||
'secret.hardwaremanagementpassword', {}).get('value', 'PASSW0RD')
|
||||
if not isinstance(username, str):
|
||||
username = username.decode('utf8')
|
||||
if not isinstance(passwd, str):
|
||||
passwd = passwd.decode('utf8')
|
||||
if passwd == 'PASSW0RD' and self.ruleset:
|
||||
raise Exception('Cannot support default password and setting password rules at same time')
|
||||
if passwd == 'PASSW0RD':
|
||||
# We must avoid hitting the web interface due to forced password change, best effert
|
||||
self._bmcconfig(nodename)
|
||||
else:
|
||||
# Switch to full web based configuration, to mitigate risks with the SMM
|
||||
wc = self._webconfigcreds(username, passwd)
|
||||
self._webconfigrules(wc)
|
||||
self._webconfignet(wc, nodename)
|
||||
|
||||
|
||||
# notes for smm:
|
||||
# POST to:
|
||||
@@ -53,4 +210,4 @@ class NodeHandler(bmchandler.NodeHandler):
|
||||
# with body user=USERID&password=Passw0rd!4321
|
||||
# yields:
|
||||
# <?xml version="1.0" encoding="UTF-8"?><root> <status>ok</status> <authResult>0</authResult> <forwardUrl>index.html</forwardUrl> </root>
|
||||
# note forwardUrl, if password change needed, will indicate something else
|
||||
# note forwardUrl, if password change needed, will indicate something else
|
||||
|
||||
@@ -0,0 +1,221 @@
|
||||
# Copyright 2019 Lenovo
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import confluent.discovery.handlers.generic as generic
|
||||
import confluent.exceptions as exc
|
||||
import confluent.netutil as netutil
|
||||
import confluent.util as util
|
||||
import eventlet.support.greendns
|
||||
import json
|
||||
try:
|
||||
from urllib import urlencode
|
||||
except ImportError:
|
||||
from urllib.parse import urlencode
|
||||
|
||||
getaddrinfo = eventlet.support.greendns.getaddrinfo
|
||||
|
||||
webclient = eventlet.import_patched('pyghmi.util.webclient')
|
||||
|
||||
class NodeHandler(generic.NodeHandler):
|
||||
devname = 'TSM'
|
||||
DEFAULT_USER = 'USERID'
|
||||
DEFAULT_PASS = 'PASSW0RD'
|
||||
|
||||
def __init__(self, info, configmanager):
|
||||
self.trieddefault = None
|
||||
self.targuser = None
|
||||
self.curruser = None
|
||||
self.currpass = None
|
||||
self.targpass = None
|
||||
self.nodename = None
|
||||
self.csrftok = None
|
||||
self.channel = None
|
||||
self.atdefault = True
|
||||
super(NodeHandler, self).__init__(info, configmanager)
|
||||
|
||||
def scan(self):
|
||||
c = webclient.SecureHTTPConnection(self.ipaddr, 443, verifycallback=self.validate_cert)
|
||||
i = c.grab_json_response('/redfish/v1/')
|
||||
uuid = i.get('UUID', None)
|
||||
if uuid:
|
||||
self.info['uuid'] = uuid
|
||||
|
||||
def validate_cert(self, certificate):
|
||||
# broadly speaking, merely checks consistency moment to moment,
|
||||
# but if https_cert gets stricter, this check means something
|
||||
fprint = util.get_fingerprint(self.https_cert)
|
||||
return util.cert_matches(fprint, certificate)
|
||||
|
||||
def _get_wc(self):
|
||||
authdata = { # start by trying factory defaults
|
||||
'username': self.DEFAULT_USER,
|
||||
'password': self.DEFAULT_PASS,
|
||||
}
|
||||
wc = webclient.SecureHTTPConnection(self.ipaddr, 443, verifycallback=self.validate_cert)
|
||||
wc.set_header('Content-Type', 'application/json')
|
||||
authmode = 0
|
||||
if not self.trieddefault:
|
||||
rsp, status = wc.grab_json_response_with_status('/api/session', authdata)
|
||||
if status == 403:
|
||||
wc.set_header('Content-Type', 'application/x-www-form-urlencoded')
|
||||
authmode = 1
|
||||
rsp, status = wc.grab_json_response_with_status('/api/session', urlencode(authdata))
|
||||
else:
|
||||
authmode = 2
|
||||
if status > 400:
|
||||
rsp = util.stringify(rsp)
|
||||
self.trieddefault = True
|
||||
if '555' in rsp:
|
||||
passchange = {
|
||||
'Password': self.targpass,
|
||||
'RetypePassword': self.targpass,
|
||||
'param': 4,
|
||||
'default_password': self.DEFAULT_PASS,
|
||||
'username': self.DEFAULT_USER
|
||||
}
|
||||
if authmode == 2:
|
||||
rsp, status = wc.grab_json_response_with_status('/api/reset-pass', passchange)
|
||||
else:
|
||||
rsp, status = wc.grab_json_response_with_status('/api/reset-pass', urlencode(passchange))
|
||||
authdata['password'] = self.targpass
|
||||
if authmode == 2:
|
||||
rsp, status = wc.grab_json_response_with_status('/api/session', authdata)
|
||||
else:
|
||||
rsp, status = wc.grab_json_response_with_status('/api/session', urlencode(authdata))
|
||||
self.csrftok = rsp['CSRFToken']
|
||||
self.channel = rsp['channel']
|
||||
self.curruser = self.DEFAULT_USER
|
||||
self.currpass = self.targpass
|
||||
return wc
|
||||
else:
|
||||
self.curruser = self.DEFAULT_USER
|
||||
self.currpass = self.DEFAULT_PASS
|
||||
self.csrftok = rsp['CSRFToken']
|
||||
self.channel = rsp['channel']
|
||||
return wc
|
||||
if self.curruser:
|
||||
authdata['username'] = self.curruser
|
||||
authdata['password'] = self.currpass
|
||||
if authmode != 1:
|
||||
rsp, status = wc.grab_json_response_with_status('/api/session', authdata)
|
||||
if authmode == 1 or status == 403:
|
||||
wc.set_header('Content-Type', 'application/x-www-form-urlencoded')
|
||||
rsp, status = wc.grab_json_response_with_status('/api/session', urlencode(authdata))
|
||||
if status != 200:
|
||||
return None
|
||||
self.csrftok = rsp['CSRFToken']
|
||||
self.channel = rsp['channel']
|
||||
return wc
|
||||
authdata['username'] = self.targuser
|
||||
authdata['password'] = self.targpass
|
||||
if authmode != 1:
|
||||
rsp, status = wc.grab_json_response_with_status('/api/session', authdata)
|
||||
if authmode == 1 or status == 403:
|
||||
wc.set_header('Content-Type', 'application/x-www-form-urlencoded')
|
||||
rsp, status = wc.grab_json_response_with_status('/api/session', urlencode(authdata))
|
||||
if status != 200:
|
||||
return None
|
||||
self.curruser = self.targuser
|
||||
self.currpass = self.targpass
|
||||
self.csrftok = rsp['CSRFToken']
|
||||
self.channel = rsp['channel']
|
||||
return wc
|
||||
|
||||
def config(self, nodename):
|
||||
self.nodename = nodename
|
||||
creds = self.configmanager.get_node_attributes(
|
||||
nodename, ['secret.hardwaremanagementuser',
|
||||
'secret.hardwaremanagementpassword',
|
||||
'hardwaremanagement.manager', 'hardwaremanagement.method', 'console.method'],
|
||||
True)
|
||||
cd = creds.get(nodename, {})
|
||||
user, passwd, _ = self.get_node_credentials(
|
||||
nodename, creds, self.DEFAULT_USER, self.DEFAULT_PASS)
|
||||
user = util.stringify(user)
|
||||
passwd = util.stringify(passwd)
|
||||
self.targuser = user
|
||||
self.targpass = passwd
|
||||
wc = self._get_wc()
|
||||
wc.set_header('X-CSRFTOKEN', self.csrftok)
|
||||
curruserinfo = {}
|
||||
authupdate = False
|
||||
wc.set_header('Content-Type', 'application/json')
|
||||
if user != self.curruser:
|
||||
authupdate = True
|
||||
if not curruserinfo:
|
||||
curruserinfo = wc.grab_json_response('/api/settings/users')
|
||||
authchg = curruserinfo[1]
|
||||
authchg['name'] = user
|
||||
if passwd != self.currpass:
|
||||
authupdate = True
|
||||
if not curruserinfo:
|
||||
curruserinfo = wc.grab_json_response('/api/settings/users')
|
||||
authchg = curruserinfo[1]
|
||||
authchg['changepassword'] = 0
|
||||
authchg['password_size'] = 'bytes_20'
|
||||
authchg['password'] = passwd
|
||||
authchg['confirm_password'] = passwd
|
||||
if authupdate:
|
||||
rsp, status = wc.grab_json_response_with_status('/api/settings/users/2', authchg, method='PUT')
|
||||
if (cd.get('hardwaremanagement.method', {}).get('value', 'ipmi') != 'redfish'
|
||||
or cd.get('console.method', {}).get('value', None) == 'ipmi'):
|
||||
# IPMI must be enabled per user config
|
||||
wc.grab_json_response('/api/settings/ipmilanconfig', {
|
||||
'ipv4_enable': 1, 'ipv6_enable': 1,
|
||||
'uncheckedipv4lanEnable': 0, 'uncheckedipv6lanEnable': 0,
|
||||
'checkedipv4lanEnable': 1, 'checkedipv6lanEnable': 1})
|
||||
if ('hardwaremanagement.manager' in cd and
|
||||
cd['hardwaremanagement.manager']['value'] and
|
||||
not cd['hardwaremanagement.manager']['value'].startswith(
|
||||
'fe80::')):
|
||||
newip = cd['hardwaremanagement.manager']['value']
|
||||
newipinfo = getaddrinfo(newip, 0)[0]
|
||||
newip = newipinfo[-1][0]
|
||||
if ':' in newip:
|
||||
raise exc.NotImplementedException('IPv6 remote config TODO')
|
||||
currnet = wc.grab_json_response('/api/settings/network')
|
||||
for net in currnet:
|
||||
if net['channel_number'] == self.channel and net['lan_enable'] == 0:
|
||||
# ignore false indication and switch to 8 (dedicated)
|
||||
self.channel = 8
|
||||
if net['channel_number'] == self.channel:
|
||||
# we have found the interface to potentially manipulate
|
||||
if net['ipv4_address'] != newip:
|
||||
netconfig = netutil.get_nic_config(self.configmanager, nodename, ip=newip)
|
||||
newmask = netutil.cidr_to_mask(netconfig['prefix'])
|
||||
net['ipv4_address'] = newip
|
||||
net['ipv4_subnet'] = newmask
|
||||
if netconfig['ipv4_gateway']:
|
||||
net['ipv4_gateway'] = netconfig['ipv4_gateway']
|
||||
net['ipv4_dhcp_enable'] = 0
|
||||
rsp, status = wc.grab_json_response_with_status(
|
||||
'/api/settings/network/{0}'.format(net['id']), net, method='PUT')
|
||||
break
|
||||
elif self.ipaddr.startswith('fe80::'):
|
||||
self.configmanager.set_node_attributes(
|
||||
{nodename: {'hardwaremanagement.manager': self.ipaddr}})
|
||||
else:
|
||||
raise exc.TargetEndpointUnreachable(
|
||||
'hardwaremanagement.manager must be set to desired address (No IPv6 Link Local detected)')
|
||||
rsp, status = wc.grab_json_response_with_status('/api/session', method='DELETE')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
import confluent.config.configmanager as cfm
|
||||
c = cfm.ConfigManager(None)
|
||||
import sys
|
||||
info = {'addresses': [[sys.argv[1]]] }
|
||||
print(repr(info))
|
||||
testr = NodeHandler(info, c)
|
||||
testr.config(sys.argv[2])
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright 2017 Lenovo
|
||||
# Copyright 2017-2019 Lenovo
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -12,51 +12,415 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import base64
|
||||
import codecs
|
||||
import confluent.discovery.handlers.imm as immhandler
|
||||
import confluent.netutil as netutil
|
||||
import confluent.util as util
|
||||
import errno
|
||||
import eventlet
|
||||
import eventlet.support.greendns
|
||||
import json
|
||||
import os
|
||||
import pyghmi.exceptions as pygexc
|
||||
import pyghmi.ipmi.oem.lenovo.imm as imm
|
||||
import eventlet.green.socket as socket
|
||||
webclient = eventlet.import_patched('pyghmi.util.webclient')
|
||||
import struct
|
||||
getaddrinfo = eventlet.support.greendns.getaddrinfo
|
||||
|
||||
|
||||
def fixup_uuid(uuidprop):
|
||||
baduuid = ''.join(uuidprop.split())
|
||||
uuidprefix = (baduuid[:8], baduuid[8:12], baduuid[12:16])
|
||||
a = codecs.encode(struct.pack('<IHH', *[int(x, 16) for x in uuidprefix]), 'hex')
|
||||
a = util.stringify(a)
|
||||
uuid = (a[:8], a[8:12], a[12:16], baduuid[16:20], baduuid[20:])
|
||||
return '-'.join(uuid).upper()
|
||||
|
||||
|
||||
|
||||
|
||||
class NodeHandler(immhandler.NodeHandler):
|
||||
devname = 'XCC'
|
||||
|
||||
def preconfig(self):
|
||||
def __init__(self, info, configmanager):
|
||||
self._wc = None
|
||||
self.nodename = None
|
||||
self.tmpnodename = None
|
||||
self.tmppasswd = None
|
||||
self._atdefaultcreds = True
|
||||
self._needpasswordchange = True
|
||||
self._currcreds = (None, None)
|
||||
super(NodeHandler, self).__init__(info, configmanager)
|
||||
|
||||
@classmethod
|
||||
def adequate(cls, info):
|
||||
# We can sometimes receive a partially initialized SLP packet
|
||||
# This is not adequate for being satisfied
|
||||
return bool(info.get('attributes', {}))
|
||||
|
||||
def preconfig(self, possiblenode):
|
||||
self.tmpnodename = possiblenode
|
||||
ff = self.info.get('attributes', {}).get('enclosure-form-factor', '')
|
||||
if ff not in ('dense-computing', [u'dense-computing']):
|
||||
# skip preconfig for non-SD530 servers
|
||||
return
|
||||
self.trieddefault = None # Reset state on a preconfig attempt
|
||||
# attempt to enable SMM
|
||||
#it's normal to get a 'not supported' (193) for systems without an SMM
|
||||
# need to branch on 3.00+ firmware
|
||||
currfirm = self.info.get('attributes', {}).get('firmware-image-info', [''])[0]
|
||||
currfirm = currfirm.split(':')
|
||||
if len(currfirm) > 1:
|
||||
currfirm = float(currfirm[1])
|
||||
disableipmi = False
|
||||
if currfirm >= 3:
|
||||
# IPMI is disabled and we need it, also we need to go to *some* password
|
||||
wc = self.wc
|
||||
if not wc:
|
||||
# We cannot try to enable SMM here without risking real credentials
|
||||
# on the wire to untrusted parties
|
||||
return
|
||||
wc.grab_json_response('/api/providers/logout')
|
||||
wc.set_basic_credentials(self._currcreds[0], self._currcreds[1])
|
||||
rsp = wc.grab_json_response('/redfish/v1/Managers/1/NetworkProtocol')
|
||||
if not rsp.get('IPMI', {}).get('ProtocolEnabled', True):
|
||||
disableipmi = True
|
||||
_, _ = wc.grab_json_response_with_status(
|
||||
'/redfish/v1/Managers/1/NetworkProtocol',
|
||||
{'IPMI': {'ProtocolEnabled': True}}, method='PATCH')
|
||||
ipmicmd = None
|
||||
try:
|
||||
ipmicmd = self._get_ipmicmd()
|
||||
ipmicmd = self._get_ipmicmd(self._currcreds[0], self._currcreds[1])
|
||||
ipmicmd.xraw_command(netfn=0x3a, command=0xf1, data=(1,))
|
||||
except pygexc.IpmiException as e:
|
||||
if (e.ipmicode != 193 and 'Unauthorized name' not in str(e) and
|
||||
'Incorrect password' not in str(e)):
|
||||
# raise an issue if anything other than to be expected
|
||||
if disableipmi:
|
||||
_, _ = wc.grab_json_response_with_status(
|
||||
'/redfish/v1/Managers/1/NetworkProtocol',
|
||||
{'IPMI': {'ProtocolEnabled': False}}, method='PATCH')
|
||||
raise
|
||||
self.trieddefault = True
|
||||
if disableipmi:
|
||||
_, _ = wc.grab_json_response_with_status(
|
||||
'/redfish/v1/Managers/1/NetworkProtocol',
|
||||
{'IPMI': {'ProtocolEnabled': False}}, method='PATCH')
|
||||
#TODO: decide how to clean out if important
|
||||
#as it stands, this can step on itself
|
||||
#if ipmicmd:
|
||||
# ipmicmd.ipmi_session.logout()
|
||||
|
||||
def validate_cert(self, certificate):
|
||||
# broadly speaking, merely checks consistency moment to moment,
|
||||
# but if https_cert gets stricter, this check means something
|
||||
fprint = util.get_fingerprint(self.https_cert)
|
||||
return util.cert_matches(fprint, certificate)
|
||||
|
||||
def get_webclient(self, username, password, newpassword):
|
||||
wc = self._wc.dupe()
|
||||
try:
|
||||
wc.connect()
|
||||
except socket.error as se:
|
||||
if se.errno != errno.ECONNREFUSED:
|
||||
raise
|
||||
return (None, None)
|
||||
pwdchanged = False
|
||||
adata = json.dumps({'username': util.stringify(username),
|
||||
'password': util.stringify(password)
|
||||
})
|
||||
headers = {'Connection': 'keep-alive',
|
||||
'Content-Type': 'application/json'}
|
||||
wc.request('POST', '/api/login', adata, headers)
|
||||
rsp = wc.getresponse()
|
||||
if rsp.status != 200 and password == 'PASSW0RD':
|
||||
rsp.read()
|
||||
adata = json.dumps({
|
||||
'username': username,
|
||||
'password': newpassword,
|
||||
})
|
||||
headers = {'Connection': 'keep-alive',
|
||||
'Content-Type': 'application/json'}
|
||||
wc.request('POST', '/api/login', adata, headers)
|
||||
rsp = wc.getresponse()
|
||||
if rsp.status == 200:
|
||||
pwdchanged = True
|
||||
password = newpassword
|
||||
else:
|
||||
rsp.read()
|
||||
return (None, None)
|
||||
if rsp.status == 200:
|
||||
self._currcreds = (username, password)
|
||||
wc.set_basic_credentials(username, password)
|
||||
rspdata = json.loads(rsp.read())
|
||||
wc.set_header('Content-Type', 'application/json')
|
||||
wc.set_header('Authorization', 'Bearer ' + rspdata['access_token'])
|
||||
if '_csrf_token' in wc.cookies:
|
||||
wc.set_header('X-XSRF-TOKEN', wc.cookies['_csrf_token'])
|
||||
if rspdata.get('pwchg_required', None) == 'true':
|
||||
wc.request('POST', '/api/function', json.dumps(
|
||||
{'USER_UserPassChange': '1,{0}'.format(newpassword)}))
|
||||
rsp = wc.getresponse()
|
||||
rsp.read()
|
||||
if rsp.status != 200:
|
||||
return (None, None)
|
||||
self._currcreds = (username, newpassword)
|
||||
wc.set_basic_credentials(username, newpassword)
|
||||
pwdchanged = True
|
||||
if '_csrf_token' in wc.cookies:
|
||||
wc.set_header('X-XSRF-TOKEN', wc.cookies['_csrf_token'])
|
||||
if pwdchanged:
|
||||
# Remove the minimum change interval, to allow sane
|
||||
# password changes after provisional changes
|
||||
self.set_password_policy('')
|
||||
return (wc, pwdchanged)
|
||||
|
||||
@property
|
||||
def wc(self):
|
||||
passwd = None
|
||||
isdefault = True
|
||||
if self._wc is None:
|
||||
self._wc = webclient.SecureHTTPConnection(
|
||||
self.ipaddr, 443, verifycallback=self.validate_cert)
|
||||
self._wc.connect()
|
||||
nodename = None
|
||||
if self.nodename:
|
||||
nodename = self.nodename
|
||||
inpreconfig = False
|
||||
elif self.tmpnodename:
|
||||
nodename = None
|
||||
inpreconfig = True
|
||||
if self._currcreds[0] is not None:
|
||||
wc, pwdchanged = self.get_webclient(self._currcreds[0], self._currcreds[1], None)
|
||||
if wc:
|
||||
return wc
|
||||
if nodename:
|
||||
creds = self.configmanager.get_node_attributes(
|
||||
nodename, ['secret.hardwaremanagementuser',
|
||||
'secret.hardwaremanagementpassword'], decrypt=True)
|
||||
user, passwd, isdefault = self.get_node_credentials(
|
||||
nodename, creds, 'USERID', 'PASSW0RD')
|
||||
if not self.trieddefault:
|
||||
if not passwd:
|
||||
# So in preconfig context, we don't have admin permission to
|
||||
# actually divulge anything to the target
|
||||
# however the target *will* demand a new password... if it's currently
|
||||
# PASSW0RD
|
||||
# use TempW0rd42 to avoid divulging a real password on the line
|
||||
# This is replacing one well known password (PASSW0RD) with another
|
||||
# (TempW0rd42)
|
||||
passwd = 'TempW0rd42'
|
||||
wc, pwdchanged = self.get_webclient('USERID', 'PASSW0RD', passwd)
|
||||
if wc:
|
||||
if pwdchanged:
|
||||
if inpreconfig:
|
||||
self.tmppasswd = passwd
|
||||
else:
|
||||
self._needpasswordchange = False
|
||||
return wc
|
||||
self.trieddefault = True
|
||||
if isdefault:
|
||||
return
|
||||
self._atdefaultcreds = False
|
||||
if self.tmppasswd:
|
||||
wc, _ = self.get_webclient('USERID', self.tmppasswd, passwd)
|
||||
else:
|
||||
wc, _ = self.get_webclient(user, passwd, None)
|
||||
if wc:
|
||||
return wc
|
||||
|
||||
def set_password_policy(self, strruleset):
|
||||
ruleset = {'USER_GlobalMinPassChgInt': '0'}
|
||||
for rule in strruleset.split(','):
|
||||
if '=' not in rule:
|
||||
continue
|
||||
name, value = rule.split('=')
|
||||
if value.lower() in ('no', 'none', 'disable', 'disabled'):
|
||||
value = '0'
|
||||
if name.lower() in ('expiry', 'expiration'):
|
||||
ruleset['USER_GlobalPassExpPeriod'] = value
|
||||
if int(value) < 5:
|
||||
ruleset['USER_GlobalPassExpWarningPeriod'] = value
|
||||
if name.lower() in ('lockout', 'loginfailures'):
|
||||
if value.lower() in ('no', 'none', 'disable', 'disabled'):
|
||||
value = '0'
|
||||
ruleset['USER_GlobalMaxLoginFailures'] = value
|
||||
if name.lower() == 'complexity':
|
||||
ruleset['USER_GlobalPassComplexRequired'] = value
|
||||
if name.lower() == 'reuse':
|
||||
ruleset['USER_GlobalMinPassReuseCycle'] = value
|
||||
try:
|
||||
wc = self.wc
|
||||
wc.grab_json_response('/api/dataset', ruleset)
|
||||
wc.grab_json_response('/api/providers/logout')
|
||||
except Exception as e:
|
||||
print(repr(e))
|
||||
pass
|
||||
|
||||
def _get_next_userid(self, wc):
|
||||
userinfo = wc.grab_json_response('/api/dataset/imm_users')
|
||||
userinfo = userinfo['items'][0]['users']
|
||||
for user in userinfo:
|
||||
if user['users_user_name'] == '':
|
||||
return user['users_user_id']
|
||||
|
||||
def _setup_xcc_account(self, username, passwd, wc):
|
||||
userinfo = wc.grab_json_response('/api/dataset/imm_users')
|
||||
uid = None
|
||||
for user in userinfo['items'][0]['users']:
|
||||
if user['users_user_name'] == username:
|
||||
uid = user['users_user_id']
|
||||
break
|
||||
else:
|
||||
for user in userinfo['items'][0]['users']:
|
||||
if user['users_user_name'] == 'USERID':
|
||||
uid = user['users_user_id']
|
||||
break
|
||||
if not uid:
|
||||
raise Exception("XCC has neither the default user nor configured user")
|
||||
# The following will work if the password is force change or normal..
|
||||
if self._needpasswordchange and self.tmppasswd != passwd:
|
||||
wc.grab_json_response('/api/function',
|
||||
{'USER_UserPassChange': '{0},{1}'.format(uid, passwd)})
|
||||
if username != 'USERID':
|
||||
wc.grab_json_response(
|
||||
'/api/function',
|
||||
{'USER_UserModify': '{0},{1},,1,4,0,0,0,0,,8,'.format(uid, username)})
|
||||
self.tmppasswd = None
|
||||
self._currcreds = (username, passwd)
|
||||
|
||||
def _convert_sha256account(self, user, passwd, wc):
|
||||
# First check if the specified user is sha256...
|
||||
userinfo = wc.grab_json_response('/api/dataset/imm_users')
|
||||
curruser = None
|
||||
uid = None
|
||||
user = util.stringify(user)
|
||||
passwd = util.stringify(passwd)
|
||||
for userent in userinfo['items'][0]['users']:
|
||||
if userent['users_user_name'] == user:
|
||||
curruser = userent
|
||||
break
|
||||
if curruser.get('users_pass_is_sha256', 0):
|
||||
self._wc = None
|
||||
wc = self.wc
|
||||
nwc = wc.dupe()
|
||||
# Have to convert it for being useful with most Lenovo automation tools
|
||||
# This requires deleting the account entirely and trying again
|
||||
tmpuid = self._get_next_userid(wc)
|
||||
try:
|
||||
tpass = base64.b64encode(os.urandom(9)) + 'Iw47$'
|
||||
userparams = "{0},6pmu0ezczzcp,{1},1,4,0,0,0,0,,8,".format(tmpuid, tpass)
|
||||
result = wc.grab_json_response('/api/function', {'USER_UserCreate': userparams})
|
||||
wc.grab_json_response('/api/providers/logout')
|
||||
adata = json.dumps({
|
||||
'username': '6pmu0ezczzcp',
|
||||
'password': tpass,
|
||||
})
|
||||
headers = {'Connection': 'keep-alive', 'Content-Type': 'application/json'}
|
||||
nwc.request('POST', '/api/login', adata, headers)
|
||||
rsp = nwc.getresponse()
|
||||
if rsp.status == 200:
|
||||
rspdata = json.loads(rsp.read())
|
||||
nwc.set_header('Content-Type', 'application/json')
|
||||
nwc.set_header('Authorization', 'Bearer ' + rspdata['access_token'])
|
||||
if '_csrf_token' in wc.cookies:
|
||||
nwc.set_header('X-XSRF-TOKEN', wc.cookies['_csrf_token'])
|
||||
if rspdata.get('reason', False):
|
||||
newpass = base64.b64encode(os.urandom(9)) + 'q4J$'
|
||||
nwc.grab_json_response(
|
||||
'/api/function',
|
||||
{'USER_UserPassChange': '{0},{1}'.format(tmpuid, newpass)})
|
||||
nwc.grab_json_response('/api/function', {'USER_UserDelete': "{0},{1}".format(curruser['users_user_id'], user)})
|
||||
userparams = "{0},{1},{2},1,4,0,0,0,0,,8,".format(curruser['users_user_id'], user, tpass)
|
||||
nwc.grab_json_response('/api/function', {'USER_UserCreate': userparams})
|
||||
nwc.grab_json_response('/api/providers/logout')
|
||||
nwc, pwdchanged = self.get_webclient(user, tpass, passwd)
|
||||
if not pwdchanged:
|
||||
nwc.grab_json_response(
|
||||
'/api/function',
|
||||
{'USER_UserPassChange': '{0},{1}'.format(curruser['users_user_id'], passwd)})
|
||||
nwc.grab_json_response('/api/providers/logout')
|
||||
finally:
|
||||
self._wc = None
|
||||
wc = self.wc
|
||||
wc.grab_json_response('/api/function', {'USER_UserDelete': "{0},{1}".format(tmpuid, '6pmu0ezczzcp')})
|
||||
wc.grab_json_response('/api/providers/logout')
|
||||
|
||||
def config(self, nodename, reset=False):
|
||||
self.nodename = nodename
|
||||
# TODO(jjohnson2): set ip parameters, user/pass, alert cfg maybe
|
||||
# In general, try to use https automation, to make it consistent
|
||||
# between hypothetical secure path and today.
|
||||
ic = self._bmcconfig(nodename)
|
||||
dpp = self.configmanager.get_node_attributes(
|
||||
nodename, 'discovery.passwordrules')
|
||||
strruleset = dpp.get(nodename, {}).get(
|
||||
'discovery.passwordrules', {}).get('value', '')
|
||||
wc = self.wc
|
||||
creds = self.configmanager.get_node_attributes(
|
||||
self.nodename, ['secret.hardwaremanagementuser',
|
||||
'secret.hardwaremanagementpassword'], decrypt=True)
|
||||
user, passwd, isdefault = self.get_node_credentials(nodename, creds, 'USERID', 'PASSW0RD')
|
||||
self.set_password_policy(strruleset)
|
||||
if self._atdefaultcreds:
|
||||
if isdefault and self.tmppasswd:
|
||||
raise Exception(
|
||||
'Request to use default credentials, but refused by target after it has been changed to {0}'.format(self.tmppasswd))
|
||||
if not isdefault:
|
||||
self._setup_xcc_account(user, passwd, wc)
|
||||
self._convert_sha256account(user, passwd, wc)
|
||||
cd = self.configmanager.get_node_attributes(
|
||||
nodename, ['secret.hardwaremanagementuser',
|
||||
'secret.hardwaremanagementpassword',
|
||||
'hardwaremanagement.manager', 'hardwaremanagement.method', 'console.method'],
|
||||
True)
|
||||
cd = cd.get(nodename, {})
|
||||
if (cd.get('hardwaremanagement.method', {}).get('value', 'ipmi') != 'redfish'
|
||||
or cd.get('console.method', {}).get('value', None) == 'ipmi'):
|
||||
nwc = wc.dupe()
|
||||
nwc.set_basic_credentials(self._currcreds[0], self._currcreds[1])
|
||||
rsp = nwc.grab_json_response('/redfish/v1/Managers/1/NetworkProtocol')
|
||||
if not rsp.get('IPMI', {}).get('ProtocolEnabled', True):
|
||||
# User has indicated IPMI support, but XCC is currently disabled
|
||||
# change XCC to be consistent
|
||||
_, _ = nwc.grab_json_response_with_status(
|
||||
'/redfish/v1/Managers/1/NetworkProtocol',
|
||||
{'IPMI': {'ProtocolEnabled': True}}, method='PATCH')
|
||||
if ('hardwaremanagement.manager' in cd and
|
||||
cd['hardwaremanagement.manager']['value'] and
|
||||
not cd['hardwaremanagement.manager']['value'].startswith(
|
||||
'fe80::')):
|
||||
newip = cd['hardwaremanagement.manager']['value']
|
||||
newipinfo = getaddrinfo(newip, 0)[0]
|
||||
newip = newipinfo[-1][0]
|
||||
if ':' in newip:
|
||||
raise exc.NotImplementedException('IPv6 remote config TODO')
|
||||
netconfig = netutil.get_nic_config(self.configmanager, nodename, ip=newip)
|
||||
newmask = netutil.cidr_to_mask(netconfig['prefix'])
|
||||
currinfo = wc.grab_json_response('/api/providers/logoninfo')
|
||||
currip = currinfo.get('items', [{}])[0].get('ipv4_address', '')
|
||||
# do not change the ipv4_config if the current config looks right already
|
||||
if currip != newip:
|
||||
statargs = {
|
||||
'ENET_IPv4Ena': '1', 'ENET_IPv4AddrSource': '0',
|
||||
'ENET_IPv4StaticIPAddr': newip, 'ENET_IPv4StaticIPNetMask': newmask
|
||||
}
|
||||
if netconfig['ipv4_gateway']:
|
||||
statargs['ENET_IPv4GatewayIPAddr'] = netconfig['ipv4_gateway']
|
||||
wc.grab_json_response('/api/dataset', statargs)
|
||||
elif self.ipaddr.startswith('fe80::'):
|
||||
self.configmanager.set_node_attributes(
|
||||
{nodename: {'hardwaremanagement.manager': self.ipaddr}})
|
||||
else:
|
||||
raise exc.TargetEndpointUnreachable(
|
||||
'hardwaremanagement.manager must be set to desired address (No IPv6 Link Local detected)')
|
||||
wc.grab_json_response('/api/providers/logout')
|
||||
ff = self.info.get('attributes', {}).get('enclosure-form-factor', '')
|
||||
if ff not in ('dense-computing', [u'dense-computing']):
|
||||
return
|
||||
# Ok, we can get the enclosure uuid now..
|
||||
ic.oem_init()
|
||||
enclosureuuid = ic._oem.immhandler.get_property(
|
||||
'/v2/ibmc/smm/chassis/uuid')
|
||||
enclosureuuid = ic._oem.immhandler.get_property(
|
||||
'/v2/ibmc/smm/chassis/uuid')
|
||||
enclosureuuid = self.info.get('attributes', {}).get('chassis-uuid', [None])[0]
|
||||
if enclosureuuid:
|
||||
enclosureuuid = imm.fixup_uuid(enclosureuuid).lower()
|
||||
enclosureuuid = enclosureuuid.lower()
|
||||
em = self.configmanager.get_node_attributes(nodename,
|
||||
'enclosure.manager')
|
||||
em = em.get(nodename, {}).get('enclosure.manager', {}).get(
|
||||
@@ -65,8 +429,3 @@ class NodeHandler(immhandler.NodeHandler):
|
||||
if em:
|
||||
self.configmanager.set_node_attributes(
|
||||
{em: {'id.uuid': enclosureuuid}})
|
||||
|
||||
# TODO(jjohnson2): web based init config for future prevalidated cert scheme
|
||||
# def config(self, nodename):
|
||||
# return
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user