mirror of
https://github.com/xcat2/confluent.git
synced 2026-09-29 08:41:00 +00:00
Compare commits
853 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6973736c6a | |||
| f9055a258e | |||
| c784a4ec9b | |||
| f2dd501de9 | |||
| e9ba49a4aa | |||
| deb90fbca9 | |||
| 3105b9b1f9 | |||
| f525c25ba6 | |||
| 3012de1fe4 | |||
| be930fc076 | |||
| 2d0199a4e9 | |||
| 6b70a4322a | |||
| 6a784e3a1c | |||
| 3b2b96a4cf | |||
| 32ddb33de3 | |||
| b77ed8dbff | |||
| d5c093a30d | |||
| cf9d2a43e8 | |||
| 2f566fb81d | |||
| 94c8cf3ff2 | |||
| 8741a27c24 | |||
| 1c494fc4fc | |||
| 1ee418392f | |||
| 2a7eeb6e08 | |||
| 5c83c78a90 | |||
| 6a466b0100 | |||
| 6b4a21d613 | |||
| 5f46899358 | |||
| 20a37f8db5 | |||
| c6b8aaf372 | |||
| 5baab5bef4 | |||
| 73c06fd25e | |||
| 8d9a082739 | |||
| 32602fbba3 | |||
| 2f616d4586 | |||
| 15dc4937ee | |||
| 10cb1b77dd | |||
| d86e1fc4eb | |||
| 78a1741e0e | |||
| 4329c1d388 | |||
| b0b5493ff7 | |||
| 326f56219b | |||
| e098c0ba91 | |||
| 61e7c90ad1 | |||
| e57cdf9a7b | |||
| 10ce7a9de9 | |||
| 0724ad812b | |||
| a3b0b0240d | |||
| 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 | |||
| 60e8338b47 | |||
| 8897eb5dcf | |||
| 208eb46cc2 | |||
| 2c5432454a | |||
| c5dd024557 | |||
| 72af8f1631 | |||
| d69cca46d0 | |||
| 4bef5f7917 | |||
| 03293d88b0 | |||
| b78266eff7 | |||
| e63d7f9fe3 | |||
| 16297b048f | |||
| 6d88dbb374 | |||
| 43c5ecd6ae | |||
| 9d2ec60b50 | |||
| a16d4b12ea | |||
| 76bfb29d60 | |||
| 3e201a5f4f | |||
| 0b0f325240 | |||
| 29330aee74 | |||
| 78dea26d06 | |||
| a00747c79c | |||
| 4bbc05699e | |||
| 06eb91c355 | |||
| 5b52582302 | |||
| 4de797be05 | |||
| 80864d78b3 | |||
| cc4950ef75 | |||
| fcae11bf96 | |||
| f5889e7029 | |||
| 148329dd8e | |||
| d9ffa10422 | |||
| a6bae944a2 | |||
| 65bcb8a35b | |||
| 31a2fce521 | |||
| ea49108c78 | |||
| 21fce4098b | |||
| 8eca6b156b | |||
| 7f4ea287bc | |||
| 9f3fe01a49 | |||
| de6ecdab38 | |||
| 02018da187 | |||
| 8bc766e740 | |||
| 8c21c59459 | |||
| d5be1ccf8c | |||
| 45b8a18f14 | |||
| d502cd5159 | |||
| 52673a990b | |||
| 43c60bc180 | |||
| c86d9f3e33 | |||
| 68221b8158 | |||
| 34fb159801 | |||
| 3372a8401a | |||
| f07a0e333f | |||
| 301014f3b5 | |||
| aa0255bf62 | |||
| 444ba917eb | |||
| 6659b31106 | |||
| 3e93cb2434 | |||
| ffaabd5ae3 | |||
| d26f6259a7 | |||
| 757df0f571 | |||
| bfc2b65e99 | |||
| 3a1e9fe0bc | |||
| bb03e4a961 | |||
| f7b5280d6a | |||
| f1f6c3b066 | |||
| b7b26a1069 | |||
| e3b869fa34 | |||
| a962a5afc3 | |||
| b2fdaeffe7 | |||
| 8694eca40b | |||
| d5efa87288 | |||
| 6a39a3f9a2 | |||
| 9b1e70893b | |||
| 672c127621 | |||
| 158a9705db | |||
| e7c6dfab2b | |||
| c891cff926 | |||
| a8a32118db | |||
| fdc4e959f7 | |||
| 91ff08158f | |||
| 79b47bd0b7 | |||
| c95d3cf906 | |||
| 9c6b10a166 | |||
| db89cdc62c | |||
| 697716b296 | |||
| 60b7083dce | |||
| f4cfe8758e | |||
| cc0fc52064 | |||
| 526eb45969 | |||
| 3606aab3f0 | |||
| eb70b21573 | |||
| e95b5da8b7 | |||
| b5e4d302f9 | |||
| 1fb6eb230b | |||
| e298ead944 | |||
| c0ab8532df | |||
| 4e1ebc290b | |||
| 4236f3fd78 | |||
| d4265fa406 | |||
| 563ee96879 | |||
| 529e3b2239 | |||
| 2ed80d86b1 | |||
| 317809f449 | |||
| ea27125587 | |||
| 0741e3953e | |||
| f44eb412c5 | |||
| c571601a29 | |||
| dad86242a9 | |||
| a0802dd66e | |||
| c966b09581 | |||
| b631699656 | |||
| aa6857b1c7 | |||
| 9c5f50341f | |||
| eb1b80f2c9 | |||
| 368666823f | |||
| d8216f2472 | |||
| 0db2e1cea6 | |||
| 6a1f7e7fa4 | |||
| e2e4c8c7e1 | |||
| ee7e044ad8 | |||
| 12cbfff24f | |||
| 8774ae0305 | |||
| cb00c5d35d | |||
| 2a0b038c30 | |||
| deb2c3a94a | |||
| 3f1bb1c151 | |||
| 0e708f4b26 | |||
| 5c93976bdb | |||
| 312af72d9f | |||
| 19f75474fc | |||
| 695e038ced | |||
| a98d8dc43c | |||
| 69cbeecf1b | |||
| 5cd461c6ad | |||
| de30c53d21 | |||
| a599c346f8 | |||
| 1e88ce2d41 | |||
| d337de671e | |||
| 68c7ab4c90 | |||
| 4af5e2d615 | |||
| 299c5c4e71 | |||
| e5c330bb1c | |||
| e1eafa46f9 | |||
| 9798f30ef8 | |||
| d1bafa98bb | |||
| 2cdfa8fcae | |||
| b93a2e60d2 | |||
| e478f8aef3 | |||
| b35463e594 | |||
| 54f25cfa9e | |||
| 9ff7829373 | |||
| f3b56cf187 | |||
| 783837b9be | |||
| 1b18edcccb | |||
| d972e2c12c | |||
| c5cf829117 | |||
| 2f55a251cd | |||
| d63f6ec9f0 | |||
| 9d582262e6 | |||
| 2063436ccc | |||
| 6addd05de2 | |||
| 28259511ed | |||
| 0393e55eb1 | |||
| f7b964b2ce | |||
| da0941c229 | |||
| 7cec311b22 | |||
| 8ada0e7cf4 | |||
| 87e2e481c2 | |||
| 38546514e2 | |||
| 1e54abd29b | |||
| a571faa215 | |||
| 87da7b62ae | |||
| 8d02d7cc7f | |||
| 5f1f41b975 | |||
| 7c1bbe1618 | |||
| 76e6f2dd17 | |||
| 67f2282cc4 | |||
| b98eea81ad | |||
| 3bf84ee1e7 | |||
| 498803f70a | |||
| 9ac7158bdd | |||
| 4110d18f43 | |||
| efe3db11ce | |||
| c8fbb58ae9 | |||
| 05715c6826 | |||
| 5d7df6dceb | |||
| dbcc33629a | |||
| 085a9fed37 | |||
| 659c4e4217 | |||
| 28ba40ccfa | |||
| e2b7440a5c | |||
| a55ba02d11 | |||
| 2ef549c2ae | |||
| 8123a77451 | |||
| 9e44087047 | |||
| 6e51e02e1b | |||
| 3e90dd6462 | |||
| 4a929eb9b5 | |||
| 858647bb6d | |||
| 8a416ccc5e | |||
| 12d0fe21cd | |||
| 7a88a2825d | |||
| bc9a498683 | |||
| b5f016ad50 | |||
| eb3f5a8bbc | |||
| 1d0b78267d | |||
| a40f015076 | |||
| 4574d1bcc7 | |||
| 0bf21238aa | |||
| 577456d999 | |||
| 1dd40d36a1 | |||
| ba9ea1acd8 | |||
| 60756d9b41 | |||
| 2795dfe7b9 | |||
| a922de2ed6 | |||
| 5a2e4a669b | |||
| c2115f4df9 | |||
| fa6650a072 | |||
| 1116fed1e7 | |||
| 0745ab0fdf | |||
| aeb3bd2444 | |||
| 5dc2d8c8be | |||
| dc23793d32 | |||
| 3bcf236744 | |||
| ea9caa470b | |||
| a894624403 | |||
| a3c06a00bf | |||
| 86ed339b48 | |||
| ff6e4d7ab6 | |||
| 8126ec3791 | |||
| 8fab1ce9eb | |||
| 9078fb01c4 | |||
| e4186841b4 | |||
| 1526a9b92d | |||
| bc1b1a9676 | |||
| f4267e6013 | |||
| e0cc67f57a | |||
| 6117a90372 | |||
| 5d1423d5ce | |||
| 2055c6d698 | |||
| 44892ae1f9 | |||
| 812e34f59b | |||
| 3a53b2c014 | |||
| 168f60de06 | |||
| f4c191c280 | |||
| 443b678d8a | |||
| 2bc7a0c21f | |||
| e429a703e2 | |||
| 342c1fb499 | |||
| d79e117c55 | |||
| e96b869206 | |||
| 68037473d0 | |||
| 0e5ac5c683 | |||
| 6ab4d3c886 | |||
| 5471aca5b4 | |||
| cf97bbe299 | |||
| f91ee5bcdd | |||
| ca25e9fa78 | |||
| b14a53a226 | |||
| b597702146 | |||
| b64858e0b3 | |||
| 50e85df33c | |||
| f6a0500e4a | |||
| 27294103cf | |||
| ec90ef889b | |||
| 86e502a47a | |||
| 2042bcb8f0 | |||
| 43211cf9a7 | |||
| db15992894 | |||
| d4329ab0f1 | |||
| e230f803ce | |||
| 3cae3ed983 | |||
| 2c3127418a | |||
| 563a2b11c3 | |||
| cff2c22fda | |||
| cfd8ac4c75 | |||
| 236d889d5d | |||
| 919dab9b55 | |||
| eb18796d94 | |||
| fd64a2e68c | |||
| 43b51eec20 | |||
| 011ba66314 | |||
| 49b3ca7381 | |||
| 16f2a2b1eb | |||
| f2bbb7c355 | |||
| c78b7fa146 | |||
| 1b44d2d781 | |||
| 2d8004000d | |||
| b714cfdf0f | |||
| 60a1ba77b7 | |||
| bbfed443fc | |||
| f3cfe4ee26 | |||
| 2dcdfe58c3 | |||
| 2e547129d1 | |||
| b39ae42955 | |||
| 9e593f0554 | |||
| ecc6bcf96c | |||
| 6ad383c6ad | |||
| 446d2270c9 | |||
| 5395f97a21 | |||
| 04781e0ece | |||
| 9bd0b7af9d | |||
| ae806e55b0 | |||
| 393ea41696 | |||
| b87cb87c2a | |||
| a91d7047b2 | |||
| 638842beec | |||
| 5678942186 | |||
| f46269a6f2 | |||
| 583e3474ac | |||
| ffbe1ab156 | |||
| 9e4bb84932 | |||
| ffd1bdfae3 | |||
| 02dd29b027 | |||
| 3b38d8ac43 | |||
| c381fefc49 | |||
| a288136a80 | |||
| cbc0ffbc1c | |||
| cfc6fd04fc | |||
| 4a4b1a623f | |||
| f1867f900d | |||
| 12ed7c50fb | |||
| 85205867b3 | |||
| 60c1cab56f | |||
| 15c84e8a9b | |||
| 1028f1cb60 | |||
| d07e5a08c9 | |||
| d2e8fa6521 | |||
| eca18a451b |
@@ -1 +1,2 @@
|
||||
include confluent_env.sh
|
||||
include confluent_env.sh
|
||||
include confluent_env.csh
|
||||
|
||||
Executable
+111
@@ -0,0 +1,111 @@
|
||||
#!/usr/bin/env python
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 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.
|
||||
|
||||
# This is a utility to bring xcoll (plus enhancements) to confluent
|
||||
# the core engine is textgroup.py, this simply provides a CLI to use
|
||||
# generically
|
||||
|
||||
import optparse
|
||||
import os
|
||||
import select
|
||||
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.textgroup as tg
|
||||
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('-d', '--diff', action='store_true',
|
||||
help='Show what differs between most common '
|
||||
'output group and others')
|
||||
argparser.add_option('-w', '--watch', action='store_true',
|
||||
help='Show intermediate results while running')
|
||||
argparser.add_option('-s', '--skipcommon', action='store_true',
|
||||
help='Do not print most common result, only non modal '
|
||||
'groups, useful when combined with -d')
|
||||
argparser.add_option('-c', '--count', action='store_true',
|
||||
help='Also display count of nodes in a given group')
|
||||
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()
|
||||
sys.exit(1)
|
||||
|
||||
grouped = tg.GroupedData()
|
||||
|
||||
if options.abbreviate:
|
||||
grouped = tg.GroupedData(confluent.client.Command())
|
||||
else:
|
||||
grouped = tg.GroupedData()
|
||||
|
||||
def print_current():
|
||||
if options.diff:
|
||||
grouped.print_deviants(skipmodal=options.skipcommon, count=options.count,
|
||||
reverse=options.reverse)
|
||||
else:
|
||||
grouped.print_all(skipmodal=options.skipcommon,
|
||||
count=options.count,
|
||||
reverse=options.reverse)
|
||||
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:
|
||||
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 printpending:
|
||||
if clearpending:
|
||||
sys.stdout.write('\x1b[2J\x1b[;H') # clear screen
|
||||
print_current()
|
||||
|
||||
+156
-20
@@ -47,6 +47,7 @@ import optparse
|
||||
import os
|
||||
import select
|
||||
import shlex
|
||||
import signal
|
||||
import socket
|
||||
import sys
|
||||
import time
|
||||
@@ -56,7 +57,10 @@ try:
|
||||
import tty
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
try:
|
||||
signal.signal(signal.SIGPIPE, signal.SIG_DFL)
|
||||
except AttributeError:
|
||||
pass
|
||||
exitcode = 0
|
||||
consoleonly = False
|
||||
consolename = ""
|
||||
@@ -72,6 +76,7 @@ import confluent.tlvdata as tlvdata
|
||||
import confluent.client as client
|
||||
|
||||
conserversequence = '\x05c' # ctrl-e, c
|
||||
clearpowermessage = False
|
||||
|
||||
oldtcattr = None
|
||||
fd = sys.stdin
|
||||
@@ -84,7 +89,34 @@ netserver = None
|
||||
laststate = {}
|
||||
|
||||
|
||||
def print_help():
|
||||
print("confetty provides a filesystem like interface to confluent. "
|
||||
"Navigation is done using the same commands as would be used in a "
|
||||
"filesystem. Tab completion is supported to aid in navigation,"
|
||||
"as is up arrow to recall previous commands and control-r to search"
|
||||
"previous command history, similar to using bash\n\n"
|
||||
"The supported commands are:\n"
|
||||
"cd [location] - Set the current command context, similar to a "
|
||||
"working directory.\n"
|
||||
"show [resource] - Present the information about the specified "
|
||||
"resource, or current context if omitted.\n"
|
||||
"create [resource] attributename=value attributename=value - Create "
|
||||
"a new instance of a resource.\n"
|
||||
"remove [resource] - Remove a resource from a list\n"
|
||||
"set [resource] attributename=value attributename=value - Change "
|
||||
"the specified attributes value for the given resource name\n"
|
||||
"unset [resource] attributename - Clear any value for the given "
|
||||
"attribute names on a resource.\n"
|
||||
"start [resource] - When used on a text session resource, it "
|
||||
"enters remote terminal mode. In this mode, use 'ctrl-e, c, ?' for "
|
||||
"help"
|
||||
)
|
||||
#TODO(jjohnson2): lookup context help for 'target' variable, perhaps
|
||||
#common with the api document
|
||||
|
||||
|
||||
def updatestatus(stateinfo={}):
|
||||
global powerstate, powertime, clearpowermessage
|
||||
status = consolename
|
||||
info = []
|
||||
for statekey in stateinfo:
|
||||
@@ -99,6 +131,15 @@ def updatestatus(stateinfo={}):
|
||||
# error will be repeated if relevant
|
||||
# avoid keeping it around as stale
|
||||
del laststate['error']
|
||||
if 'state' in stateinfo: # currently only read power means anything
|
||||
newpowerstate = stateinfo['state']['value']
|
||||
if newpowerstate != powerstate and newpowerstate == 'off':
|
||||
sys.stdout.write("\x1b[2J\x1b[;H[powered off]\r\n")
|
||||
clearpowermessage = True
|
||||
if newpowerstate == 'on' and clearpowermessage:
|
||||
sys.stdout.write("\x1b[2J\x1b[;H")
|
||||
clearpowermessage = False
|
||||
powerstate = newpowerstate
|
||||
if 'clientcount' in laststate and laststate['clientcount'] != 1:
|
||||
info.append('clients: %d' % laststate['clientcount'])
|
||||
if 'bufferage' in stateinfo and stateinfo['bufferage'] is not None:
|
||||
@@ -106,14 +147,14 @@ def updatestatus(stateinfo={}):
|
||||
if 'showtime' in laststate:
|
||||
showtime = laststate['showtime']
|
||||
age = time.time() - laststate['showtime']
|
||||
if age > 86400: # older than one day
|
||||
if age > 86400: # older than one day
|
||||
# disambiguate by putting date in and time
|
||||
info.append(time.strftime('%m-%dT%H:%M', time.localtime(showtime)))
|
||||
else:
|
||||
info.append(time.strftime('%H:%M', time.localtime(showtime)))
|
||||
if info:
|
||||
status += ' [' + ','.join(info) + ']'
|
||||
if os.environ['TERM'] not in ('linux'):
|
||||
if os.environ.get('TERM', '') not in ('linux'):
|
||||
sys.stdout.write('\x1b]0;console: %s\x07' % status)
|
||||
sys.stdout.flush()
|
||||
|
||||
@@ -145,7 +186,7 @@ def recurse_format(datum, levels=0):
|
||||
|
||||
|
||||
def prompt():
|
||||
if os.environ['TERM'] not in ('linux'):
|
||||
if os.environ.get('TERM', '') not in ('linux'):
|
||||
sys.stdout.write('\x1b]0;confetty: %s\x07' % target)
|
||||
try:
|
||||
return raw_input(target + ' -> ')
|
||||
@@ -169,6 +210,7 @@ valid_commands = [
|
||||
'remove',
|
||||
'rm',
|
||||
'delete',
|
||||
'help',
|
||||
]
|
||||
|
||||
candidates = None
|
||||
@@ -235,7 +277,11 @@ def rcompleter(text, state):
|
||||
|
||||
|
||||
def parse_command(command):
|
||||
args = shlex.split(command, posix=True)
|
||||
try:
|
||||
args = shlex.split(command, posix=True)
|
||||
except ValueError as ve:
|
||||
print('Error: ' + str(ve))
|
||||
return []
|
||||
return args
|
||||
|
||||
|
||||
@@ -249,14 +295,17 @@ def print_result(res):
|
||||
if 'databynode' in res:
|
||||
print_result(res['databynode'])
|
||||
return
|
||||
for key in res.iterkeys():
|
||||
for key in sorted(res):
|
||||
notes = []
|
||||
if res[key] is None:
|
||||
attrstr = '%s=""' % key
|
||||
elif type(res[key]) == list:
|
||||
attrstr = '%s=%s' % (key, recurse_format(res[key]))
|
||||
elif not isinstance(res[key], dict):
|
||||
print '{0}: {1}'.format(key, res[key])
|
||||
try:
|
||||
print '{0}: {1}'.format(key, res[key])
|
||||
except UnicodeEncodeError:
|
||||
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'])
|
||||
@@ -302,7 +351,11 @@ def do_command(command, server):
|
||||
return
|
||||
argv[0] = argv[0].lower()
|
||||
if argv[0] == 'exit':
|
||||
if os.environ.get('TERM', '') not in ('linux'):
|
||||
sys.stdout.write('\x1b]0;\x07')
|
||||
sys.exit(0)
|
||||
elif argv[0] in ('help', '?'):
|
||||
return print_help()
|
||||
elif argv[0] == 'cd':
|
||||
otarget = target
|
||||
if len(argv) > 1:
|
||||
@@ -344,6 +397,21 @@ def do_command(command, server):
|
||||
elif argv[0] in ('cat', 'show', 'ls', 'dir'):
|
||||
if len(argv) > 1:
|
||||
targpath = fullpath_target(argv[1])
|
||||
if argv[0] in ('ls', 'dir'):
|
||||
if targpath[-1] != '/':
|
||||
# could still be a directory, fetch the parent..
|
||||
childname = targpath[targpath.rindex('/') + 1:]
|
||||
parentpath = targpath[:targpath.rindex('/') + 1]
|
||||
if parentpath != '/noderange/':
|
||||
# if it were /noderange/, then it's a directory
|
||||
# even though parent won't tell us that
|
||||
for res in session.read(parentpath, server):
|
||||
try:
|
||||
if res['item']['href'] == childname:
|
||||
print(childname)
|
||||
return
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
targpath = target
|
||||
for res in session.read(targpath):
|
||||
@@ -406,14 +474,21 @@ def createresource(args):
|
||||
if keydata is None:
|
||||
return
|
||||
targpath = fullpath_target(resname)
|
||||
collection, _, resname = targpath.rpartition('/')
|
||||
keydata['name'] = resname
|
||||
if targpath.startswith('/noderange//'):
|
||||
collection = targpath
|
||||
else:
|
||||
collection, _, resname = targpath.rpartition('/')
|
||||
keydata['name'] = resname
|
||||
makecall(session.create, (collection, keydata))
|
||||
|
||||
|
||||
def makecall(callout, args):
|
||||
global exitcode
|
||||
for response in callout(*args):
|
||||
if 'deleted' in response:
|
||||
print("Deleted: " + response['deleted'])
|
||||
if 'created' in response:
|
||||
print("Created: " + response['created'])
|
||||
if 'error' in response:
|
||||
if 'errorcode' in response:
|
||||
exitcode = response['errorcode']
|
||||
@@ -522,7 +597,12 @@ def quitconfetty(code=0, fullexit=False, fixterm=True):
|
||||
fcntl.fcntl(sys.stdin.fileno(), fcntl.F_SETFL, currfl ^ os.O_NONBLOCK)
|
||||
if oldtcattr is not None:
|
||||
termios.tcsetattr(sys.stdin.fileno(), termios.TCSANOW, oldtcattr)
|
||||
# Request default color scheme, to undo potential weirdness of terminal
|
||||
if sys.stdout.isatty():
|
||||
sys.stdout.write('\x1b[m')
|
||||
if fullexit:
|
||||
if sys.stdout.isatty() and os.environ.get('TERM', '') not in ('linux'):
|
||||
sys.stdout.write('\x1b]0;\x07')
|
||||
sys.exit(code)
|
||||
else:
|
||||
tlvdata.send(session.connection, {'operation': 'stop',
|
||||
@@ -572,7 +652,7 @@ def conserver_command(filehandle, localcommand):
|
||||
tlvdata.send(session.connection, {'operation': 'break',
|
||||
'path': currconsole})
|
||||
print("break sent]\r")
|
||||
elif localcommand[0] == 'p': # print
|
||||
elif localcommand[0] == 'p': # power
|
||||
cmdlen += 1
|
||||
localcommand = get_command_bytes(filehandle, localcommand, cmdlen)
|
||||
|
||||
@@ -647,9 +727,26 @@ def conserver_command(filehandle, localcommand):
|
||||
else:
|
||||
print("Unknown power state.]\r")
|
||||
|
||||
check_power_state()
|
||||
elif localcommand[0] == 'r':
|
||||
sys.stdout.write('\x1b7\x1b[999;999H\x1b[6n')
|
||||
sys.stdout.flush()
|
||||
reply = ''
|
||||
while 'R' not in reply:
|
||||
try:
|
||||
reply += sys.stdin.read(1)
|
||||
except IOError:
|
||||
pass
|
||||
reply = reply.replace('\x1b[', '')
|
||||
reply = reply.replace('R', '')
|
||||
height, width = reply.split(';')
|
||||
sys.stdout.write('\x1b8')
|
||||
sys.stdout.flush()
|
||||
print('sending stty commands]')
|
||||
return 'stty columns {0}\rstty rows {1}\r'.format(width, height)
|
||||
elif localcommand[0] == '?':
|
||||
print("help]\r")
|
||||
print(". disconnect\r")
|
||||
print(". exit console\r")
|
||||
print("b break\r")
|
||||
print("o reopen\r")
|
||||
print("po power off\r")
|
||||
@@ -657,6 +754,7 @@ def conserver_command(filehandle, localcommand):
|
||||
print("pbs boot to setup\r")
|
||||
print("pbn boot to network\r")
|
||||
print("pb<ent> boot to default\r")
|
||||
print("r send stty command to resize terminal\r")
|
||||
print("<cr> abort command\r")
|
||||
elif localcommand[0] == '\x0d':
|
||||
print("ignored]\r")
|
||||
@@ -666,7 +764,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
|
||||
@@ -679,7 +780,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()
|
||||
@@ -738,6 +842,8 @@ 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
|
||||
@@ -753,10 +859,25 @@ elif shellargs:
|
||||
do_command(command, netserver)
|
||||
quitconfetty(fullexit=True, fixterm=False)
|
||||
|
||||
powerstate = None
|
||||
powertime = None
|
||||
|
||||
|
||||
def check_power_state():
|
||||
tlvdata.send(
|
||||
session.connection,
|
||||
{'operation': 'retrieve',
|
||||
'path': '/nodes/' + consolename + '/power/state'})
|
||||
return
|
||||
|
||||
|
||||
while inconsole or not doexit:
|
||||
if inconsole:
|
||||
rdylist, _, _ = select.select(
|
||||
(sys.stdin, session.connection), (), (), 60)
|
||||
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
|
||||
@@ -772,7 +893,15 @@ while inconsole or not doexit:
|
||||
updatestatus(data)
|
||||
continue
|
||||
if data is not None:
|
||||
sys.stdout.write(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):
|
||||
@@ -802,10 +931,17 @@ while inconsole or not doexit:
|
||||
sys.stdout.write("\r\n[remote disconnected]\r\n")
|
||||
break
|
||||
else:
|
||||
myinput = fh.read()
|
||||
myinput = check_escape_seq(myinput, fh)
|
||||
if myinput:
|
||||
tlvdata.send(session.connection, myinput)
|
||||
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:
|
||||
|
||||
@@ -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/* ::
|
||||
Executable
+131
@@ -0,0 +1,131 @@
|
||||
#!/usr/bin/env python
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 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__ = 'alin37'
|
||||
|
||||
from getpass import getpass
|
||||
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='''\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')
|
||||
(options, args) = argparser.parse_args()
|
||||
|
||||
|
||||
#setting minimal output to only output current information
|
||||
showtype = 'current'
|
||||
requestargs=None
|
||||
try:
|
||||
noderange = args[0]
|
||||
nodelist = '/noderange/{0}/nodes/'.format(noderange)
|
||||
except IndexError:
|
||||
argparser.print_help()
|
||||
sys.exit(1)
|
||||
client.check_globbing(noderange)
|
||||
session = client.Command()
|
||||
exitcode = 0
|
||||
|
||||
#Sets attributes
|
||||
nodetype="noderange"
|
||||
|
||||
if len(args) > 1:
|
||||
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)
|
||||
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, noderange, options, argassign)
|
||||
try:
|
||||
# setting user output to what the user inputs
|
||||
if args[1] == 'all':
|
||||
showtype = 'all'
|
||||
requestargs=args[2:]
|
||||
elif args[1] == 'current':
|
||||
showtype = 'current'
|
||||
requestargs=args[2:]
|
||||
else:
|
||||
showtype = 'all'
|
||||
requestargs=args[1:]
|
||||
except:
|
||||
pass
|
||||
|
||||
if exitcode != 0:
|
||||
sys.exit(exitcode)
|
||||
|
||||
# Lists all attributes
|
||||
if len(args) > 0:
|
||||
# setting output to all so it can search since if we do have something to search, we want to show all outputs even if it is blank.
|
||||
if requestargs is None:
|
||||
showtype = 'current'
|
||||
elif requestargs == []:
|
||||
#showtype already set
|
||||
pass
|
||||
else:
|
||||
try:
|
||||
requestargs.remove('all')
|
||||
requestargs.remove('current')
|
||||
except ValueError:
|
||||
pass
|
||||
exitcode = client.printattributes(session, requestargs, showtype,nodetype, noderange, options)
|
||||
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)
|
||||
Executable
+62
@@ -0,0 +1,62 @@
|
||||
#!/usr/bin/env python
|
||||
# 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.
|
||||
|
||||
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>")
|
||||
(options, args) = argparser.parse_args()
|
||||
try:
|
||||
noderange = args[0]
|
||||
except IndexError:
|
||||
argparser.print_help()
|
||||
sys.exit(1)
|
||||
client.check_globbing(noderange)
|
||||
session = client.Command()
|
||||
exitcode = 0
|
||||
|
||||
errorNodes = set([])
|
||||
|
||||
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
|
||||
|
||||
allNodes = set([])
|
||||
|
||||
for node in session.read('/noderange/{0}/nodes/'.format(noderange)):
|
||||
allNodes.add(node['item']['href'].replace("/", ""))
|
||||
|
||||
goodNodes = allNodes - errorNodes
|
||||
|
||||
for node in goodNodes:
|
||||
print node + ": BMC Reset Successful"
|
||||
|
||||
|
||||
sys.exit(success)
|
||||
Regular → Executable
+16
-6
@@ -17,8 +17,13 @@
|
||||
|
||||
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'):
|
||||
@@ -30,6 +35,9 @@ 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 '
|
||||
@@ -44,6 +52,7 @@ except IndexError:
|
||||
'Usage: {0} <noderange> [default|cd|network|setup|hd]\n'.format(
|
||||
sys.argv[0]))
|
||||
sys.exit(1)
|
||||
client.check_globbing(noderange)
|
||||
bootdev = None
|
||||
if len(sys.argv) > 2:
|
||||
bootdev = sys.argv[2]
|
||||
@@ -56,11 +65,12 @@ if options.biosmode:
|
||||
else:
|
||||
bootmode = 'uefi'
|
||||
|
||||
errnodes = set([])
|
||||
rc = session.simple_noderange_command(noderange, '/boot/nextdevice', bootdev,
|
||||
bootmode=bootmode,
|
||||
persistent=options.persist)
|
||||
|
||||
if rc:
|
||||
sys.exit(rc)
|
||||
else:
|
||||
sys.exit(session.simple_noderange_command(noderange, '/power/state', 'boot'))
|
||||
persistent=options.persist,
|
||||
errnodes=errnodes)
|
||||
if errnodes:
|
||||
noderange = noderange + ',-(' + ','.join(errnodes) + ')'
|
||||
rc |= session.simple_noderange_command(noderange, '/power/state', 'boot')
|
||||
sys.exit(rc)
|
||||
|
||||
Executable
+241
@@ -0,0 +1,241 @@
|
||||
#!/usr/bin/env python
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 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.
|
||||
|
||||
|
||||
import os
|
||||
import signal
|
||||
import optparse
|
||||
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
|
||||
|
||||
class NullOpt(object):
|
||||
blame = None
|
||||
clear = None
|
||||
|
||||
|
||||
def bailout(msg, code=1):
|
||||
sys.stderr.write(msg + '\n')
|
||||
sys.exit(code)
|
||||
|
||||
|
||||
argparser = optparse.OptionParser()
|
||||
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('-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')
|
||||
(options, args) = argparser.parse_args()
|
||||
|
||||
cfgpaths = {
|
||||
'bmc.ipv4_address': (
|
||||
'configuration/management_controller/net_interfaces/management',
|
||||
'ipv4_address'),
|
||||
'bmc.ipv4_method': (
|
||||
'configuration/management_controller/net_interfaces/management',
|
||||
'ipv4_configuration'),
|
||||
'bmc.ipv4_gateway': (
|
||||
'configuration/management_controller/net_interfaces/management',
|
||||
'ipv4_gateway'),
|
||||
'bmc.hostname': (
|
||||
'configuration/management_controller/hostname', 'hostname'),
|
||||
}
|
||||
|
||||
autodeps = {
|
||||
'bmc.ipv4_address': (('bmc.ipv4_method', 'static'),)
|
||||
}
|
||||
|
||||
try:
|
||||
noderange = args[0]
|
||||
except IndexError:
|
||||
argparser.print_help()
|
||||
sys.exit(1)
|
||||
client.check_globbing(noderange)
|
||||
setmode = None
|
||||
assignment = {}
|
||||
queryparms = {}
|
||||
printsys = []
|
||||
setsys = {}
|
||||
forceset = False
|
||||
needval = None
|
||||
|
||||
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
|
||||
|
||||
|
||||
def _assign_value():
|
||||
if key not in cfgpaths:
|
||||
setsys[key] = value
|
||||
for depkey, depval in autodeps.get(key, []):
|
||||
assignment[depkey] = depval
|
||||
assignment[key] = value
|
||||
|
||||
|
||||
for param in args[1:]:
|
||||
if param == 'show':
|
||||
continue # forgive muscle memory of pasu users
|
||||
if param == 'set':
|
||||
setmode = True
|
||||
forceset = True
|
||||
continue
|
||||
if needval:
|
||||
key = needval
|
||||
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:
|
||||
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
|
||||
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:
|
||||
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
|
||||
session = client.Command()
|
||||
rcode = 0
|
||||
if options.restoredefault and 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:
|
||||
sys.stderr.write(
|
||||
'Unrecognized component to restore defaults: {0}\n'.format(
|
||||
options.restoredefault))
|
||||
sys.exit(1)
|
||||
if setmode:
|
||||
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:
|
||||
path = 'configuration/system/all'
|
||||
attrib = key
|
||||
else:
|
||||
path, attrib = cfgpaths[key]
|
||||
if path not in updatebypath:
|
||||
updatebypath[path] = {}
|
||||
attrnamebypath[path] = {}
|
||||
updatebypath[path][attrib] = assignment[key]
|
||||
attrnamebypath[path][attrib] = key
|
||||
# well, we want to expand things..
|
||||
# check ipv4, if requested change method to static
|
||||
for path in updatebypath:
|
||||
for fr in session.update('/noderange/{0}/{1}'.format(noderange, path),
|
||||
updatebypath[path]):
|
||||
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:
|
||||
if options.comparedefault:
|
||||
continue
|
||||
rc = client.print_attrib_path(path, session, list(queryparms[path]),
|
||||
NullOpt(), queryparms[path])
|
||||
if rc:
|
||||
sys.exit(rc)
|
||||
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)
|
||||
Executable
+58
@@ -0,0 +1,58 @@
|
||||
#!/usr/bin/env python
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 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.
|
||||
|
||||
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='''\n %prog noderange attribute1=value1 attribute2=value,...
|
||||
\n ''')
|
||||
(options, args) = argparser.parse_args()
|
||||
requestargs=None
|
||||
try:
|
||||
noderange = args[0]
|
||||
except IndexError:
|
||||
argparser.print_help()
|
||||
sys.exit(1)
|
||||
client.check_globbing(noderange)
|
||||
session = client.Command()
|
||||
exitcode = 0
|
||||
attribs = {'name': noderange}
|
||||
for arg in args[1:]:
|
||||
key, val = arg.split('=', 1)
|
||||
attribs[key] = val
|
||||
for r in session.create('/noderange/', attribs):
|
||||
if 'error' in r:
|
||||
sys.stderr.write(r['error'] + '\n')
|
||||
exitcode |= 1
|
||||
if 'created' in r:
|
||||
print('{0}: created'.format(r['created']))
|
||||
sys.exit(exitcode)
|
||||
Executable
+286
@@ -0,0 +1,286 @@
|
||||
#!/usr/bin/env python
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 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.
|
||||
|
||||
import csv
|
||||
import optparse
|
||||
import os
|
||||
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
|
||||
|
||||
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]
|
||||
|
||||
|
||||
def dumpmacs(procinfo):
|
||||
return ','.join(procinfo['macs']) # + procinfo.get('relatedmacs', []))
|
||||
|
||||
|
||||
def print_disco(options, session, currmac):
|
||||
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))
|
||||
|
||||
|
||||
def process_header(header):
|
||||
# normalize likely header titles
|
||||
fields = []
|
||||
broken = False
|
||||
for datum in header:
|
||||
datum = datum.lower()
|
||||
if datum.startswith('node') or datum.startswith('name'):
|
||||
fields.append('node')
|
||||
elif datum in ('nodegroup', 'nodegroups', 'group', 'groups'):
|
||||
fields.append('groups')
|
||||
elif datum.startswith('mac') or datum.startswith('ether'):
|
||||
fields.append('mac')
|
||||
elif datum.startswith('serial') or datum in ('sn', 's/n'):
|
||||
fields.append('serial')
|
||||
elif datum == 'uuid':
|
||||
fields.append('uuid')
|
||||
elif datum in ('bmc', 'imm', 'xcc'):
|
||||
fields.append('hardwaremanagement.manager')
|
||||
elif datum in ('bmc gateway', 'xcc gateway', 'imm gateway'):
|
||||
fields.append('net.bmc.gateway')
|
||||
elif datum in ('bmcuser', 'username', 'user'):
|
||||
fields.append('secret.hardwaremanagementuser')
|
||||
elif datum in ('bmcpass', 'password', 'pass'):
|
||||
fields.append('secret.hardwaremanagementpassword')
|
||||
else:
|
||||
print("Unrecognized column name {0}".format(datum))
|
||||
broken = True
|
||||
if broken:
|
||||
sys.exit(1)
|
||||
return tuple(fields)
|
||||
|
||||
|
||||
def datum_complete(datum):
|
||||
if 'node' not in datum or not datum['node']:
|
||||
sys.stderr.write('Nodename is a required field')
|
||||
return False
|
||||
provided = set(datum)
|
||||
required = set(['serial', 'uuid', 'mac'])
|
||||
for field in provided & required:
|
||||
if datum[field]:
|
||||
break
|
||||
else:
|
||||
sys.stderr.write('One of the fields "Serial Number", "UUID", or '
|
||||
'"MAC Address" must be provided')
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
searchkeys = set(['mac', 'serial', 'uuid'])
|
||||
|
||||
|
||||
def search_record(datum, options, session):
|
||||
for searchkey in searchkeys:
|
||||
options.__dict__[searchkey] = None
|
||||
for searchkey in searchkeys & set(datum):
|
||||
options.__dict__[searchkey] = datum[searchkey]
|
||||
return list(list_matching_macs(options, session))
|
||||
|
||||
|
||||
def datum_to_attrib(datum):
|
||||
for key in ('serial', 'uuid', 'mac'):
|
||||
try:
|
||||
del datum[key]
|
||||
except KeyError:
|
||||
pass
|
||||
datum['name'] = datum['node']
|
||||
del datum['node']
|
||||
return datum
|
||||
|
||||
def import_csv(options, session):
|
||||
nodedata = []
|
||||
with open(options.importfile, 'r') as datasrc:
|
||||
records = csv.reader(datasrc)
|
||||
fields = process_header(next(records))
|
||||
for record in records:
|
||||
currfields = list(fields)
|
||||
nodedatum = {}
|
||||
for datum in record:
|
||||
nodedatum[currfields.pop(0)] = datum
|
||||
if not datum_complete(nodedatum):
|
||||
sys.exit(1)
|
||||
if not search_record(nodedatum, options, session):
|
||||
sys.stderr.write(
|
||||
"Could not match the following data: " +
|
||||
repr(nodedatum) + '\n')
|
||||
sys.exit(1)
|
||||
nodedata.append(nodedatum)
|
||||
for datum in nodedata:
|
||||
maclist = search_record(datum, options, session)
|
||||
datum = datum_to_attrib(datum)
|
||||
nodename = datum['name']
|
||||
for res in session.create('/nodes/', datum):
|
||||
if 'error' in res:
|
||||
sys.stderr.write(res['error'] + '\n')
|
||||
continue
|
||||
elif 'created' in res:
|
||||
print('Defined ' + res['created'])
|
||||
else:
|
||||
print(repr(res))
|
||||
for mac in maclist:
|
||||
for res in session.update('/discovery/by-mac/{0}'.format(mac),
|
||||
{'node': nodename}):
|
||||
if 'error' in res:
|
||||
sys.stderr.write(res['error'] + '\n')
|
||||
continue
|
||||
elif 'assigned' in res:
|
||||
print('Discovered ' + res['assigned'])
|
||||
else:
|
||||
print(repr(res))
|
||||
|
||||
|
||||
def list_discovery(options, session):
|
||||
if options.csv:
|
||||
csv.writer(sys.stdout).writerow(columns)
|
||||
else:
|
||||
print(tabformat.format(*columns))
|
||||
print(tabformat.format(*delimit))
|
||||
for mac in list_matching_macs(options, session):
|
||||
print_disco(options, session, mac)
|
||||
|
||||
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):
|
||||
path = '/discovery/'
|
||||
if options.model:
|
||||
path += 'by-model/{0}/'.format(options.model)
|
||||
if options.serial:
|
||||
path += 'by-serial/{0}/'.format(options.serial)
|
||||
if options.uuid:
|
||||
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]
|
||||
if 'error' in result:
|
||||
return []
|
||||
return [options.mac.replace(':', '-')]
|
||||
else:
|
||||
path += 'by-mac/'
|
||||
return [x['item']['href'] for x in session.read(path)]
|
||||
|
||||
def assign_discovery(options, session):
|
||||
abort = False
|
||||
if options.importfile:
|
||||
return import_csv(options, session)
|
||||
if 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)
|
||||
if not matches:
|
||||
sys.stderr.write("No matching discovery candidates found\n")
|
||||
sys.exit(1)
|
||||
for res in session.update('/discovery/by-mac/{0}'.format(matches[0]),
|
||||
{'node': options.node}):
|
||||
if 'assigned' in res:
|
||||
print('Assigned: {0}'.format(res['assigned']))
|
||||
else:
|
||||
print(repr(res))
|
||||
|
||||
|
||||
|
||||
def main():
|
||||
parser = optparse.OptionParser(
|
||||
usage='Usage: %prog [list|assign|rescan|clear] [options]')
|
||||
# -a for 'address' maybe?
|
||||
# order by
|
||||
# show state (discovered or..
|
||||
# nodediscover approve?
|
||||
# flush to clear old data out? (e.g. no good way to age pxe data)
|
||||
# also delete discovery datum... more targeted
|
||||
# defect: -t lenovo-imm returns all
|
||||
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')
|
||||
parser.add_option('-u', '--uuid', dest='uuid',
|
||||
help='Operate against the system matching the specified '
|
||||
'UUID', metavar='UUID')
|
||||
parser.add_option('-n', '--node', help='Operate with the given nodename')
|
||||
parser.add_option('-e', '--ethaddr', dest='mac',
|
||||
help='Operate against the system with the specified MAC '
|
||||
'address', metavar='MAC')
|
||||
parser.add_option('-t', '--type', dest='type',
|
||||
help='Operate against the system of the specified type',
|
||||
metavar='TYPE')
|
||||
parser.add_option('-c', '--csv', dest='csv',
|
||||
help='Use CSV formatted output', action='store_true')
|
||||
parser.add_option('-i', '--import', dest='importfile',
|
||||
help='Import bulk assignment data from given CSV file',
|
||||
metavar='IMPORT.CSV')
|
||||
(options, args) = parser.parse_args()
|
||||
if len(args) == 0 or args[0] not in ('list', 'assign', '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] == 'rescan':
|
||||
list(session.update('/discovery/rescan', {'rescan': 'start'}))
|
||||
print("Rescan initiated")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -15,11 +15,17 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import codecs
|
||||
from datetime import datetime as dt
|
||||
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'):
|
||||
@@ -27,19 +33,27 @@ if path.startswith('/opt'):
|
||||
|
||||
import confluent.client as client
|
||||
|
||||
sys.stdout = codecs.getwriter('utf8')(sys.stdout)
|
||||
|
||||
argparser = optparse.OptionParser(
|
||||
usage="Usage: %prog [options] noderange (clear)")
|
||||
usage="Usage: %prog [options] noderange [clear]")
|
||||
(options, args) = argparser.parse_args()
|
||||
try:
|
||||
noderange = args[0]
|
||||
except IndexError:
|
||||
argparser.print_help()
|
||||
sys.exit(1)
|
||||
|
||||
client.check_globbing(noderange)
|
||||
deletemode = False
|
||||
if len(sys.argv) > 3:
|
||||
argparser.print_help()
|
||||
sys.exit(1)
|
||||
if len(sys.argv) == 3:
|
||||
if sys.argv[2] == 'clear':
|
||||
deletemode = True
|
||||
else:
|
||||
argparser.print_help()
|
||||
sys.exit(1)
|
||||
|
||||
session = client.Command()
|
||||
exitcode = 0
|
||||
|
||||
@@ -17,29 +17,24 @@
|
||||
|
||||
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 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:
|
||||
@@ -57,28 +52,115 @@ 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>")
|
||||
argparser = optparse.OptionParser(
|
||||
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')
|
||||
(options, args) = argparser.parse_args()
|
||||
upfile = None
|
||||
try:
|
||||
noderange = args[0]
|
||||
if len(args) > 1:
|
||||
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)
|
||||
client.check_globbing(noderange)
|
||||
|
||||
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 update_firmware(session, filename):
|
||||
global exitcode
|
||||
output = sq.ScreenPrinter(noderange, session)
|
||||
nodeurls = {}
|
||||
filename = os.path.abspath(filename)
|
||||
resource = '/noderange/{0}/inventory/firmware/updates/active'.format(
|
||||
noderange)
|
||||
upargs = {'filename': filename}
|
||||
if options.backup:
|
||||
upargs['bank'] = 'backup'
|
||||
noderrs = {}
|
||||
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 updating ({0})!\n'.format(allerrnodes))
|
||||
for node in noderrs:
|
||||
sys.stderr.write('{0}: {1}\n'.format(node, noderrs[node]))
|
||||
|
||||
def show_firmware(session):
|
||||
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 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:
|
||||
argparser.print_help()
|
||||
|
||||
|
||||
try:
|
||||
session = client.Command()
|
||||
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]:
|
||||
continue
|
||||
for inv in res['databynode'][node]['firmware']:
|
||||
for prefix in inv:
|
||||
printfirm(node, prefix, inv[prefix])
|
||||
if upfile is None:
|
||||
show_firmware(session)
|
||||
else:
|
||||
update_firmware(session, upfile)
|
||||
except KeyboardInterrupt:
|
||||
print('')
|
||||
sys.exit(exitcode)
|
||||
Executable
+128
@@ -0,0 +1,128 @@
|
||||
#!/usr/bin/env python
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 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__ = 'alin37'
|
||||
|
||||
from getpass import getpass
|
||||
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='''\n %prog [options] \
|
||||
\n %prog [options] nodegroup [list of attributes] \
|
||||
\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()
|
||||
|
||||
|
||||
#setting minimal output to only output current information
|
||||
showtype = 'current'
|
||||
requestargs=None
|
||||
nodetype="nodegroups"
|
||||
|
||||
try:
|
||||
nodegroups = args[0]
|
||||
nodelist = '/{0}/{1}/'.format(nodetype,nodegroups)
|
||||
except IndexError:
|
||||
nodelist = '/nodegroups/'
|
||||
client.check_globbing(nodegroups)
|
||||
session = client.Command()
|
||||
exitcode = 0
|
||||
|
||||
#Sets attributes
|
||||
|
||||
if len(args) > 1:
|
||||
showtype = 'all'
|
||||
if "=" in args[1] and options.clear:
|
||||
print("Can not clear and set at the same time!")
|
||||
argparser.print_help()
|
||||
sys.exit(1)
|
||||
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':
|
||||
showtype = 'all'
|
||||
elif args[1] == 'current':
|
||||
showtype = 'current'
|
||||
|
||||
requestargs=args[1:]
|
||||
except Exception as e:
|
||||
print str(e)
|
||||
|
||||
if exitcode != 0:
|
||||
sys.exit(exitcode)
|
||||
|
||||
# Lists all attributes
|
||||
if len(args) > 0:
|
||||
# setting output to all so it can search since if we do have something to search, we want to show all outputs even if it is blank.
|
||||
if requestargs is None:
|
||||
showtype = 'current'
|
||||
elif requestargs == []:
|
||||
#showtype already set
|
||||
pass
|
||||
else:
|
||||
try:
|
||||
requestargs.remove('all')
|
||||
requestargs.remove('current')
|
||||
except ValueError:
|
||||
pass
|
||||
exitcode = client.printgroupattributes(session, requestargs, showtype,nodetype, nodegroups, options)
|
||||
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)
|
||||
Executable
+58
@@ -0,0 +1,58 @@
|
||||
#!/usr/bin/env python
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 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.
|
||||
|
||||
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='''\n %prog noderange attribute1=value1 attribute2=value,...
|
||||
\n ''')
|
||||
(options, args) = argparser.parse_args()
|
||||
requestargs=None
|
||||
try:
|
||||
noderange = args[0]
|
||||
except IndexError:
|
||||
argparser.print_help()
|
||||
sys.exit(1)
|
||||
client.check_globbing(noderange)
|
||||
session = client.Command()
|
||||
exitcode = 0
|
||||
attribs = {'name': noderange}
|
||||
for arg in args[1:]:
|
||||
key, val = arg.split('=', 1)
|
||||
attribs[key] = val
|
||||
for r in session.create('/nodegroups/', attribs):
|
||||
if 'error' in r:
|
||||
sys.stderr.write(r['error'] + '\n')
|
||||
exitcode |= 1
|
||||
if 'created' in r:
|
||||
print('{0}: created'.format(r['created']))
|
||||
sys.exit(exitcode)
|
||||
Executable
+52
@@ -0,0 +1,52 @@
|
||||
#!/usr/bin/env python
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 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.
|
||||
|
||||
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='''\n %prog noderange
|
||||
\n ''')
|
||||
(options, args) = argparser.parse_args()
|
||||
if len(args) != 1:
|
||||
argparser.print_help()
|
||||
sys.exit(1)
|
||||
noderange = args[0]
|
||||
client.check_globbing(noderange)
|
||||
session = client.Command()
|
||||
exitcode = 0
|
||||
for r in session.delete('/nodegroups/{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)
|
||||
@@ -18,8 +18,13 @@
|
||||
import codecs
|
||||
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'):
|
||||
@@ -36,6 +41,7 @@ try:
|
||||
except IndexError:
|
||||
argparser.print_help()
|
||||
sys.exit(1)
|
||||
client.check_globbing(noderange)
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
@@ -17,7 +17,12 @@
|
||||
|
||||
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'))
|
||||
@@ -33,10 +38,13 @@ try:
|
||||
except IndexError:
|
||||
argparser.print_help()
|
||||
sys.exit(1)
|
||||
|
||||
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(
|
||||
|
||||
@@ -15,9 +15,18 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import codecs
|
||||
import json
|
||||
import optparse
|
||||
import os
|
||||
import re
|
||||
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'):
|
||||
@@ -25,6 +34,10 @@ if path.startswith('/opt'):
|
||||
|
||||
import confluent.client as client
|
||||
|
||||
sys.stdout = codecs.getwriter('utf8')(sys.stdout)
|
||||
|
||||
filters = []
|
||||
|
||||
|
||||
def pretty(text):
|
||||
if text == 'pcislot':
|
||||
@@ -34,14 +47,17 @@ def pretty(text):
|
||||
return text
|
||||
|
||||
def print_mem_info(node, prefix, meminfo):
|
||||
capacity = meminfo['capacity_mb'] / 1024
|
||||
memdescfmt = '{0}GB PC'
|
||||
if meminfo['memory_type'] == 'DDR3 SDRAM':
|
||||
memdescfmt += '3-{1} '
|
||||
elif meminfo['memory_type'] == 'DDR4 SDRAM':
|
||||
memdescfmt += '4-{1} '
|
||||
else:
|
||||
print('{0}: {1}: Unrecognized Memory'.format(node, prefix))
|
||||
return
|
||||
if meminfo['ecc']:
|
||||
memdescfmt += 'ECC '
|
||||
capacity = meminfo['capacity_mb'] / 1024
|
||||
memdescfmt += meminfo['module_type']
|
||||
memdesc = memdescfmt.format(capacity, meminfo['speed'])
|
||||
print('{0}: {1} description: {2}'.format(node, prefix, memdesc))
|
||||
@@ -70,17 +86,41 @@ def printerror(res, node=None):
|
||||
exitcode = 1
|
||||
|
||||
|
||||
argparser = optparse.OptionParser(usage="Usage: %prog <noderange>")
|
||||
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)
|
||||
client.check_globbing(noderange)
|
||||
if len(args) > 1:
|
||||
if args[1] == 'firm':
|
||||
os.execlp('nodefirmware', 'nodefirmware', noderange)
|
||||
else:
|
||||
url = '/noderange/{0}/inventory/hardware/all/system'
|
||||
for arg in args:
|
||||
for arg in arg.split(','):
|
||||
if arg == 'serial':
|
||||
filters.append(re.compile('serial number'))
|
||||
elif arg == 'model':
|
||||
filters.append(re.compile('^model'))
|
||||
filters.append(re.compile('product name'))
|
||||
elif arg == 'uuid':
|
||||
filters.append(re.compile('uuid'))
|
||||
elif arg == 'mac':
|
||||
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('/noderange/{0}/inventory/hardware/all/all'.format(
|
||||
noderange)):
|
||||
for res in session.read(url.format(noderange)):
|
||||
printerror(res)
|
||||
if 'databynode' not in res:
|
||||
continue
|
||||
@@ -90,8 +130,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']:
|
||||
print '{0}: {1}: Not Present'.format(node, prefix)
|
||||
if not filters:
|
||||
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)
|
||||
@@ -99,14 +150,34 @@ try:
|
||||
info.pop('chassis_extra', None)
|
||||
info.pop('product_extra', None)
|
||||
if 'memory_type' in info:
|
||||
print_mem_info(node, prefix, 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:
|
||||
if filters:
|
||||
for filter in filters:
|
||||
if filter.match(datum.lower()):
|
||||
break
|
||||
else:
|
||||
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)
|
||||
|
||||
@@ -15,12 +15,17 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
__author__ = 'jjohnson2'
|
||||
__author__ = 'jjohnson2,alin37'
|
||||
|
||||
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'):
|
||||
@@ -28,77 +33,37 @@ if path.startswith('/opt'):
|
||||
|
||||
import confluent.client as client
|
||||
|
||||
|
||||
def attrrequested(attr, attrlist, seenattributes):
|
||||
for candidate in attrlist:
|
||||
truename = candidate
|
||||
if candidate.startswith('hm'):
|
||||
candidate = candidate.replace('hm', 'hardwaremanagement', 1)
|
||||
if candidate == attr:
|
||||
seenattributes.add(truename)
|
||||
return True
|
||||
elif '.' not in candidate and attr.startswith(candidate + '.'):
|
||||
seenattributes.add(truename)
|
||||
return True
|
||||
return False
|
||||
argparser = optparse.OptionParser(
|
||||
usage="Usage: %prog [options] noderange [list of attributes]")
|
||||
argparser.add_option('-b', '--blame', action='store_true',
|
||||
def main():
|
||||
argparser = optparse.OptionParser(
|
||||
usage="Usage: %prog noderange\n"
|
||||
" or: %prog [options] noderange <nodeattribute>...")
|
||||
argparser.add_option('-b', '--blame', action='store_true',
|
||||
help='Show information about how attributes inherited')
|
||||
(options, args) = argparser.parse_args()
|
||||
try:
|
||||
noderange = args[0]
|
||||
nodelist = '/noderange/{0}/nodes/'.format(noderange)
|
||||
except IndexError:
|
||||
nodelist = '/nodes/'
|
||||
session = client.Command()
|
||||
exitcode = 0
|
||||
if len(args) > 1:
|
||||
seenattributes = set([])
|
||||
for res in session.read('/noderange/{0}/attributes/all'.format(noderange)):
|
||||
if 'error' in res:
|
||||
sys.stderr.write(res['error'] + '\n')
|
||||
exitcode = 1
|
||||
continue
|
||||
for node in res['databynode']:
|
||||
for attr in res['databynode'][node]:
|
||||
seenattributes.add(attr)
|
||||
currattr = res['databynode'][node][attr]
|
||||
if attrrequested(attr, args[1:], seenattributes):
|
||||
if 'value' in currattr:
|
||||
if currattr['value'] is not None:
|
||||
attrout = '{0}: {1}: {2}'.format(
|
||||
node, attr, currattr['value'])
|
||||
else:
|
||||
attrout = '{0}: {1}:'.format(node, attr)
|
||||
elif 'isset' in currattr:
|
||||
if currattr['isset']:
|
||||
attrout = '{0}: {1}: ********'.format(node, attr)
|
||||
else:
|
||||
attrout = '{0}: {1}:'.format(node, attr)
|
||||
if options.blame:
|
||||
blamedata = []
|
||||
if 'inheritedfrom' in currattr:
|
||||
blamedata.append('inherited from group {0}'.format(
|
||||
currattr['inheritedfrom']
|
||||
))
|
||||
if 'expression' in currattr:
|
||||
blamedata.append(
|
||||
'derived from expression "{0}"'.format(
|
||||
currattr['expression']))
|
||||
if blamedata:
|
||||
attrout += ' (' + ', '.join(blamedata) + ')'
|
||||
print attrout
|
||||
if not exitcode:
|
||||
for attr in args[1:]:
|
||||
if attr not in seenattributes:
|
||||
sys.stderr.write('Error: {0} not a valid attribute\n'.format(attr))
|
||||
(options, args) = argparser.parse_args()
|
||||
noderange=""
|
||||
nodelist=""
|
||||
try:
|
||||
noderange = args[0]
|
||||
nodelist = '/noderange/{0}/nodes/'.format(noderange)
|
||||
except IndexError:
|
||||
nodelist = '/nodes/'
|
||||
client.check_globbing(noderange)
|
||||
session = client.Command()
|
||||
exitcode = 0
|
||||
showtype='all'
|
||||
requestargs=args[1:]
|
||||
nodetype='noderange'
|
||||
if len(args) > 1:
|
||||
exitcode=client.printattributes(session,requestargs,showtype,nodetype,noderange,options)
|
||||
else:
|
||||
for res in session.read(nodelist):
|
||||
if 'error' in res:
|
||||
sys.stderr.write(res['error'] + '\n')
|
||||
exitcode = 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)
|
||||
else:
|
||||
print res['item']['href'].replace('/', '')
|
||||
|
||||
sys.exit(exitcode)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
#!/usr/bin/python
|
||||
# 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 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()
|
||||
@@ -17,8 +17,13 @@
|
||||
|
||||
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'):
|
||||
@@ -29,21 +34,42 @@ 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')
|
||||
(options, args) = argparser.parse_args()
|
||||
try:
|
||||
noderange = args[0]
|
||||
except IndexError:
|
||||
argparser.print_help()
|
||||
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()
|
||||
sys.exit(1)
|
||||
session = client.Command()
|
||||
exitcode = 0
|
||||
sys.exit(
|
||||
session.simple_noderange_command(noderange, '/power/state', setstate))
|
||||
session.add_precede_key('oldstate')
|
||||
|
||||
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))
|
||||
Executable
+52
@@ -0,0 +1,52 @@
|
||||
#!/usr/bin/env python
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 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.
|
||||
|
||||
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='''\n %prog noderange
|
||||
\n ''')
|
||||
(options, args) = argparser.parse_args()
|
||||
if len(args) != 1:
|
||||
argparser.print_help()
|
||||
sys.exit(1)
|
||||
noderange = args[0]
|
||||
client.check_globbing(noderange)
|
||||
session = client.Command()
|
||||
exitcode = 0
|
||||
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)
|
||||
Executable
+62
@@ -0,0 +1,62 @@
|
||||
#!/usr/bin/env python
|
||||
# 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.
|
||||
|
||||
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>")
|
||||
(options, args) = argparser.parse_args()
|
||||
try:
|
||||
noderange = args[0]
|
||||
except IndexError:
|
||||
argparser.print_help()
|
||||
sys.exit(1)
|
||||
client.check_globbing(noderange)
|
||||
session = client.Command()
|
||||
exitcode = 0
|
||||
|
||||
errorNodes = set([])
|
||||
|
||||
success = session.simple_noderange_command(noderange, 'power/reseat', 'reseat', key='reseat', errnodes=errorNodes) # = 0 if successful
|
||||
|
||||
# Determine which nodes were successful and print them
|
||||
|
||||
allNodes = set([])
|
||||
|
||||
for node in session.read('/noderange/{0}/nodes/'.format(noderange)):
|
||||
allNodes.add(node['item']['href'].replace("/", ""))
|
||||
|
||||
goodNodes = allNodes - errorNodes
|
||||
|
||||
for node in goodNodes:
|
||||
print node + ": Reseat successful"
|
||||
|
||||
|
||||
sys.exit(success)
|
||||
@@ -15,72 +15,115 @@
|
||||
# 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.sortutil as sortutil
|
||||
|
||||
|
||||
argparser = optparse.OptionParser(
|
||||
usage="Usage: %prog node commandexpression",
|
||||
epilog="Expressions are the same as in attributes, e.g. "
|
||||
"'ipmitool -H {hardwaremanagement.manager}' will be expanded.")
|
||||
argparser.disable_interspersed_args()
|
||||
(options, args) = argparser.parse_args()
|
||||
if len(args) < 2:
|
||||
argparser.print_help()
|
||||
sys.exit(1)
|
||||
c = client.Command()
|
||||
cmdstr = " ".join(args[1:])
|
||||
def run():
|
||||
argparser = optparse.OptionParser(
|
||||
usage="Usage: %prog noderange commandexpression",
|
||||
epilog="Expressions are the same as in attributes, e.g. "
|
||||
"'ipmitool -H {hardwaremanagement.manager}' will be expanded.")
|
||||
argparser.add_option('-f', '-c', '--count', type='int', default=168,
|
||||
help='Number of commands to run at a time')
|
||||
# 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:
|
||||
argparser.print_help()
|
||||
sys.exit(1)
|
||||
concurrentprocs = options.count
|
||||
client.check_globbing(args[0])
|
||||
c = client.Command()
|
||||
cmdstr = " ".join(args[1:])
|
||||
|
||||
nodeforpopen = {}
|
||||
popens = []
|
||||
for exp in c.create('/noderange/{0}/attributes/expression'.format(args[0]),
|
||||
{'expression': cmdstr}):
|
||||
ex = exp['databynode']
|
||||
for node in ex:
|
||||
cmd = ex[node]['value'].encode('utf-8')
|
||||
cmdv = shlex.split(cmd)
|
||||
nopen = subprocess.Popen(
|
||||
cmdv, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
popens.append(nopen)
|
||||
nodeforpopen[nopen] = node
|
||||
currprocs = 0
|
||||
all = set([])
|
||||
pipedesc = {}
|
||||
pendingexecs = deque()
|
||||
exitcode = 0
|
||||
|
||||
all = set([])
|
||||
pipedesc = {}
|
||||
exitcode = 0
|
||||
for pop in popens:
|
||||
node = nodeforpopen[pop]
|
||||
pipedesc[pop.stdout] = { 'node': node, 'popen': pop, 'type': 'stdout'}
|
||||
pipedesc[pop.stderr] = {'node': node, 'popen': pop, 'type': 'stderr'}
|
||||
all.add(pop.stdout)
|
||||
all.add(pop.stderr)
|
||||
rdy, _, _ = select.select(all, [], [], 10)
|
||||
while all and rdy:
|
||||
for r in rdy:
|
||||
data = r.readline()
|
||||
desc = pipedesc[r]
|
||||
if data:
|
||||
node = desc['node']
|
||||
if desc['type'] == 'stdout':
|
||||
sys.stdout.write('{0}: {1}'.format(node,data))
|
||||
for exp in c.create('/noderange/{0}/attributes/expression'.format(args[0]),
|
||||
{'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'].encode('utf-8')
|
||||
cmdv = shlex.split(cmd)
|
||||
if currprocs < concurrentprocs:
|
||||
currprocs += 1
|
||||
run_cmdv(node, cmdv, all, pipedesc)
|
||||
else:
|
||||
sys.stderr.write('{0}: {1}'.format(node, data))
|
||||
else:
|
||||
pop = desc['popen']
|
||||
ret = pop.poll()
|
||||
if ret is not None:
|
||||
exitcode = exitcode | ret
|
||||
all.discard(r)
|
||||
if all:
|
||||
rdy, _, _ = select.select(all, [], [], 10)
|
||||
sys.exit(exitcode)
|
||||
pendingexecs.append((node, cmdv))
|
||||
if not all or exitcode:
|
||||
sys.exit(exitcode)
|
||||
rdy, _, _ = select.select(all, [], [], 10)
|
||||
while all:
|
||||
pernodeout = {}
|
||||
for r in rdy:
|
||||
desc = pipedesc[r]
|
||||
node = desc['node']
|
||||
data = True
|
||||
while data and select.select([r], [], [], 0)[0]:
|
||||
data = r.readline()
|
||||
if data:
|
||||
if desc['type'] == 'stdout':
|
||||
if node not in pernodeout:
|
||||
pernodeout[node] = []
|
||||
pernodeout[node].append(data)
|
||||
else:
|
||||
sys.stderr.write('{0}: {1}'.format(node, data))
|
||||
sys.stderr.flush()
|
||||
else:
|
||||
pop = desc['popen']
|
||||
ret = pop.poll()
|
||||
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))
|
||||
sys.stdout.flush()
|
||||
if all:
|
||||
rdy, _, _ = select.select(all, [], [], 10)
|
||||
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()
|
||||
|
||||
@@ -19,9 +19,14 @@ import csv
|
||||
import datetime
|
||||
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'):
|
||||
@@ -31,6 +36,7 @@ import confluent.client as client
|
||||
|
||||
sensorcollections = {
|
||||
'all': 'sensors/hardware/all/all',
|
||||
'energy': 'sensors/hardware/energy/all',
|
||||
'temperature': 'sensors/hardware/temperature/all',
|
||||
'temp': 'sensors/hardware/temperature/all',
|
||||
'power': 'sensors/hardware/power/all',
|
||||
@@ -59,6 +65,7 @@ if options.numreadings:
|
||||
options.interval = 1
|
||||
try:
|
||||
noderange = args[0]
|
||||
client.check_globbing(noderange)
|
||||
except IndexError:
|
||||
argparser.print_help()
|
||||
sys.exit(1)
|
||||
@@ -73,7 +80,6 @@ for sensorgroup in args[1:]:
|
||||
sensors.append('sensors/hardware/all/' + sensor)
|
||||
if not sensors:
|
||||
sensors = ['sensors/hardware/all/all']
|
||||
|
||||
session = client.Command()
|
||||
exitcode = 0
|
||||
sensorheaders = {}
|
||||
@@ -120,6 +126,8 @@ def sensorpass(showout=True, appendtime=False):
|
||||
if showout:
|
||||
if sensedata['value'] is None:
|
||||
showval = ''
|
||||
elif isinstance(sensedata['value'], float):
|
||||
showval = u' {0:.5f} '.format(sensedata['value'])
|
||||
else:
|
||||
showval = u' {0} '.format(sensedata['value'])
|
||||
if sensedata['units'] not in (None, u''):
|
||||
@@ -141,6 +149,7 @@ def sensorpass(showout=True, appendtime=False):
|
||||
'%Y-%m-%dT%H:%M:%S')
|
||||
print(u'{0}: {1}:{2}'.format(
|
||||
node, sensedata['name'], showval).encode('utf-8'))
|
||||
sys.stdout.flush()
|
||||
return resultdata
|
||||
|
||||
|
||||
|
||||
@@ -17,8 +17,13 @@
|
||||
|
||||
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'):
|
||||
@@ -35,11 +40,15 @@ 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('-u', '--uefi', dest='uefi', action='store_true',
|
||||
default=True,
|
||||
help='Request UEFI style boot (rather than BIOS)')
|
||||
|
||||
(options, args) = argparser.parse_args()
|
||||
|
||||
try:
|
||||
noderange = args[0]
|
||||
client.check_globbing(noderange)
|
||||
except IndexError:
|
||||
argparser.print_help()
|
||||
sys.exit(1)
|
||||
|
||||
Executable
+130
@@ -0,0 +1,130 @@
|
||||
#!/usr/bin/env python
|
||||
# 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 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.sortutil as sortutil
|
||||
|
||||
|
||||
def run():
|
||||
|
||||
argparser = optparse.OptionParser(
|
||||
usage="Usage: %prog noderange commandexpression",
|
||||
epilog="Expressions are the same as in attributes, e.g. "
|
||||
"'ipmitool -H {hardwaremanagement.manager}' will be expanded.")
|
||||
argparser.add_option('-f', '-c', '--count', type='int', default=168,
|
||||
help='Number of commands to run at a time')
|
||||
# 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:
|
||||
argparser.print_help()
|
||||
sys.exit(1)
|
||||
client.check_globbing(args[0])
|
||||
concurrentprocs = options.count
|
||||
c = client.Command()
|
||||
cmdstr = " ".join(args[1:])
|
||||
|
||||
currprocs = 0
|
||||
all = set([])
|
||||
pipedesc = {}
|
||||
pendingexecs = deque()
|
||||
exitcode = 0
|
||||
|
||||
|
||||
for exp in c.create('/noderange/{0}/attributes/expression'.format(args[0]),
|
||||
{'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'].encode('utf-8')
|
||||
cmdv = ['ssh', node, 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)
|
||||
while all:
|
||||
pernodeout = {}
|
||||
for r in rdy:
|
||||
desc = pipedesc[r]
|
||||
node = desc['node']
|
||||
data = True
|
||||
while data and select.select([r], [], [], 0)[0]:
|
||||
data = r.readline()
|
||||
if data:
|
||||
if desc['type'] == 'stdout':
|
||||
if node not in pernodeout:
|
||||
pernodeout[node] = []
|
||||
pernodeout[node].append(data)
|
||||
else:
|
||||
sys.stderr.write('{0}: {1}'.format(node, data))
|
||||
sys.stderr.flush()
|
||||
else:
|
||||
pop = desc['popen']
|
||||
ret = pop.poll()
|
||||
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))
|
||||
sys.stdout.flush()
|
||||
if all:
|
||||
rdy, _, _ = select.select(all, [], [], 10)
|
||||
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()
|
||||
@@ -0,0 +1,145 @@
|
||||
#!/usr/bin/python
|
||||
# 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):
|
||||
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 = {}
|
||||
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\n'
|
||||
'\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 == '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)
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -1,7 +1,7 @@
|
||||
# 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.
|
||||
@@ -17,8 +17,10 @@
|
||||
|
||||
import anydbm as dbm
|
||||
import errno
|
||||
import fnmatch
|
||||
import hashlib
|
||||
import os
|
||||
import shlex
|
||||
import socket
|
||||
import ssl
|
||||
import sys
|
||||
@@ -26,6 +28,30 @@ import confluent.tlvdata as tlvdata
|
||||
|
||||
SO_PASSCRED = 16
|
||||
|
||||
_attraliases = {
|
||||
'bmc': 'hardwaremanagement.manager',
|
||||
'bmcuser': 'secret.hardwaremanagementuser',
|
||||
'bmcpass': 'secret.hardwaremanagementpassword',
|
||||
}
|
||||
|
||||
|
||||
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)
|
||||
sys.stdout.flush()
|
||||
|
||||
def _parseserver(string):
|
||||
if ']:' in string:
|
||||
@@ -42,9 +68,11 @@ def _parseserver(string):
|
||||
|
||||
|
||||
class Command(object):
|
||||
|
||||
def __init__(self, server=None):
|
||||
self._prevdict = None
|
||||
self._prevkeyname = None
|
||||
self.connection = None
|
||||
self._currnoderange = None
|
||||
if server is None:
|
||||
if 'CONFLUENT_HOST' in os.environ:
|
||||
self.serverloc = os.environ['CONFLUENT_HOST']
|
||||
@@ -74,8 +102,16 @@ class Command(object):
|
||||
if authdata['authpassed'] == 1:
|
||||
self.authenticated = True
|
||||
|
||||
def handle_results(self, ikey, rc, res):
|
||||
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:
|
||||
errnodes.add(self._currnoderange)
|
||||
sys.stderr.write('Error: {0}\n'.format(res['error']))
|
||||
if 'errorcode' in res:
|
||||
return res['errorcode']
|
||||
@@ -86,6 +122,8 @@ class Command(object):
|
||||
res = res['databynode']
|
||||
for node in res:
|
||||
if 'error' in res[node]:
|
||||
if errnodes is not None:
|
||||
errnodes.add(node)
|
||||
sys.stderr.write('{0}: Error: {1}\n'.format(
|
||||
node, res[node]['error']))
|
||||
if 'errorcode' in res[node]:
|
||||
@@ -93,12 +131,26 @@ class Command(object):
|
||||
else:
|
||||
rc |= 1
|
||||
elif ikey in res[node]:
|
||||
print('{0}: {1}'.format(node, res[node][ikey]['value']))
|
||||
if 'value' in res[node][ikey]:
|
||||
val = res[node][ikey]['value']
|
||||
elif 'isset' in res[node][ikey]:
|
||||
val = '********' if res[node][ikey] else ''
|
||||
else:
|
||||
val = repr(res[node][ikey])
|
||||
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, **kwargs):
|
||||
key=None, errnodes=None, **kwargs):
|
||||
try:
|
||||
self._currnoderange = noderange
|
||||
rc = 0
|
||||
if resource[0] == '/':
|
||||
resource = resource[1:]
|
||||
@@ -110,19 +162,42 @@ class Command(object):
|
||||
if input is None:
|
||||
for res in self.read('/noderange/{0}/{1}'.format(
|
||||
noderange, resource)):
|
||||
rc = self.handle_results(ikey, rc, res)
|
||||
rc = self.handle_results(ikey, rc, res, errnodes)
|
||||
else:
|
||||
kwargs[ikey] = input
|
||||
for res in self.update('/noderange/{0}/{1}'.format(
|
||||
noderange, resource), kwargs):
|
||||
rc = self.handle_results(ikey, rc, res, errnodes)
|
||||
self._currnoderange = None
|
||||
return rc
|
||||
except KeyboardInterrupt:
|
||||
cprint('')
|
||||
return 0
|
||||
|
||||
def simple_nodegroups_command(self, noderange, resource, input=None, key=None, **kwargs):
|
||||
try:
|
||||
rc = 0
|
||||
if resource[0] == '/':
|
||||
resource = resource[1:]
|
||||
# The implicit key is the resource basename
|
||||
if key is None:
|
||||
ikey = resource.rpartition('/')[-1]
|
||||
else:
|
||||
ikey = key
|
||||
if input is None:
|
||||
for res in self.read('/nodegroups/{0}/{1}'.format(
|
||||
noderange, resource)):
|
||||
rc = self.handle_results(ikey, rc, res)
|
||||
else:
|
||||
kwargs[ikey] = input
|
||||
for res in self.update('/nodegroups/{0}/{1}'.format(
|
||||
noderange, resource), kwargs):
|
||||
rc = self.handle_results(ikey, rc, res)
|
||||
return rc
|
||||
except KeyboardInterrupt:
|
||||
print('')
|
||||
cprint('')
|
||||
return 0
|
||||
|
||||
|
||||
|
||||
def read(self, path, parameters=None):
|
||||
if not self.authenticated:
|
||||
raise Exception('Unauthenticated')
|
||||
@@ -187,12 +262,11 @@ 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()
|
||||
hostid = '@'.join((port,server))
|
||||
hostid = '@'.join((port, server))
|
||||
khf = dbm.open(os.path.join(clientcfgdir, "knownhosts"), 'c', 384)
|
||||
if hostid in khf:
|
||||
if fingerprint == khf[hostid]:
|
||||
@@ -202,11 +276,10 @@ class Command(object):
|
||||
"MISMATCHED CERTIFICATE DATA, ACCEPT NEW? (y/n):")
|
||||
if replace not in ('y', 'Y'):
|
||||
raise Exception("BAD CERTIFICATE")
|
||||
print 'Adding new key for %s:%s' % (server, port)
|
||||
cprint('Adding new key for %s:%s' % (server, port))
|
||||
khf[hostid] = fingerprint
|
||||
|
||||
|
||||
|
||||
def send_request(operation, path, server, parameters=None):
|
||||
"""This function iterates over all the responses
|
||||
received from the server.
|
||||
@@ -223,8 +296,301 @@ def send_request(operation, path, server, parameters=None):
|
||||
tlvdata.send(server, payload)
|
||||
result = tlvdata.recv(server)
|
||||
while '_requestdone' not in result:
|
||||
yield result
|
||||
try:
|
||||
yield result
|
||||
except GeneratorExit:
|
||||
while '_requestdone' not in result:
|
||||
result = tlvdata.recv(server)
|
||||
raise
|
||||
result = tlvdata.recv(server)
|
||||
|
||||
|
||||
def attrrequested(attr, attrlist, seenattributes):
|
||||
for candidate in attrlist:
|
||||
truename = candidate
|
||||
if candidate.startswith('hm'):
|
||||
candidate = candidate.replace('hm', 'hardwaremanagement', 1)
|
||||
if candidate in _attraliases:
|
||||
candidate = _attraliases[candidate]
|
||||
if fnmatch.fnmatch(attr.lower(), candidate.lower()):
|
||||
seenattributes.add(truename)
|
||||
return True
|
||||
elif attr.lower().startswith(candidate.lower() + '.'):
|
||||
seenattributes.add(truename)
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def printattributes(session, requestargs, showtype, nodetype, noderange, options):
|
||||
path = '/{0}/{1}/attributes/{2}'.format(nodetype, noderange, showtype)
|
||||
return print_attrib_path(path, session, requestargs, options)
|
||||
|
||||
|
||||
def print_attrib_path(path, session, requestargs, options, rename=None):
|
||||
exitcode = 0
|
||||
seenattributes = set([])
|
||||
for res in session.read(path):
|
||||
if 'error' in res:
|
||||
sys.stderr.write(res['error'] + '\n')
|
||||
exitcode = 1
|
||||
continue
|
||||
for node in sorted(res['databynode']):
|
||||
for attr, val in sorted(
|
||||
res['databynode'][node].items(),
|
||||
key=lambda (k, v): v.get('sortid', k) if isinstance(v, dict) else k):
|
||||
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:
|
||||
printattr = rename.get(attr, attr)
|
||||
else:
|
||||
printattr = attr
|
||||
currattr = res['databynode'][node][attr]
|
||||
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, val)
|
||||
else:
|
||||
attrout = '{0}: {1}:'.format(node, printattr)
|
||||
elif 'isset' in currattr:
|
||||
if currattr['isset']:
|
||||
attrout = '{0}: {1}: ********'.format(node,
|
||||
printattr)
|
||||
else:
|
||||
attrout = '{0}: {1}:'.format(node, printattr)
|
||||
elif 'broken' in currattr:
|
||||
attrout = '{0}: {1}: *ERROR* BROKEN EXPRESSION: ' \
|
||||
'{2}'.format(node, printattr,
|
||||
currattr['broken'])
|
||||
elif isinstance(currattr, list) or isinstance(currattr, tuple):
|
||||
attrout = '{0}: {1}: {2}'.format(node, attr, ','.join(map(str, currattr)))
|
||||
elif isinstance(currattr, dict):
|
||||
dictout = []
|
||||
for k, v in currattr.items:
|
||||
dictout.append("{0}={1}".format(k, v))
|
||||
attrout = '{0}: {1}: {2}'.format(node, printattr, ','.join(map(str, dictout)))
|
||||
else:
|
||||
cprint("CODE ERROR" + repr(attr))
|
||||
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(
|
||||
currattr['inheritedfrom']
|
||||
))
|
||||
if 'expression' in currattr:
|
||||
blamedata.append(
|
||||
'derived from expression "{0}"'.format(
|
||||
currattr['expression']))
|
||||
if blamedata:
|
||||
attrout += ' (' + ', '.join(blamedata) + ')'
|
||||
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'])):
|
||||
cprint('{0}: {1}: {2} (Default: {3})'.format(
|
||||
node, printattr, currattr['value'],
|
||||
currattr['default']))
|
||||
else:
|
||||
|
||||
try:
|
||||
details = options.detail
|
||||
except AttributeError:
|
||||
details = False
|
||||
if details:
|
||||
if currattr.get('help', None):
|
||||
attrout += ' (Help: {0})'.format(
|
||||
currattr['help'].encode('utf-8'))
|
||||
if currattr.get('possible', None):
|
||||
attrout += ' (Choices: {0})'.format(
|
||||
','.join(currattr['possible']))
|
||||
cprint(attrout)
|
||||
if not exitcode:
|
||||
if requestargs:
|
||||
for attr in requestargs:
|
||||
if attr not in seenattributes:
|
||||
sys.stderr.write('Error: {0} not a valid attribute\n'.format(attr))
|
||||
exitcode = 1
|
||||
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([])
|
||||
for res in session.read('/{0}/{1}/attributes/{2}'.format(nodetype, noderange, showtype)):
|
||||
if 'error' in res:
|
||||
sys.stderr.write(res['error'] + '\n')
|
||||
exitcode = 1
|
||||
continue
|
||||
for attr in res:
|
||||
seenattributes.add(attr)
|
||||
currattr = res[attr]
|
||||
if (requestargs is None or requestargs == [] or attrrequested(attr, requestargs, seenattributes)):
|
||||
if 'value' in currattr:
|
||||
if currattr['value'] is not None:
|
||||
attrout = '{0}: {1}: {2}'.format(
|
||||
noderange, attr, currattr['value'])
|
||||
else:
|
||||
attrout = '{0}: {1}:'.format(noderange, attr)
|
||||
elif 'isset' in currattr:
|
||||
if currattr['isset']:
|
||||
attrout = '{0}: {1}: ********'.format(noderange, attr)
|
||||
else:
|
||||
attrout = '{0}: {1}:'.format(noderange, attr)
|
||||
elif 'broken' in currattr:
|
||||
attrout = '{0}: {1}: *ERROR* BROKEN EXPRESSION: ' \
|
||||
'{2}'.format(noderange, attr,
|
||||
currattr['broken'])
|
||||
elif 'expression' in currattr:
|
||||
attrout = '{0}: {1}: (will derive from expression {2})'.format(noderange, attr, currattr['expression'])
|
||||
elif isinstance(currattr, list) or isinstance(currattr, tuple):
|
||||
attrout = '{0}: {1}: {2}'.format(noderange, attr, ','.join(map(str, currattr)))
|
||||
elif isinstance(currattr, dict):
|
||||
dictout = []
|
||||
for k, v in currattr.items:
|
||||
dictout.append("{0}={1}".format(k, v))
|
||||
attrout = '{0}: {1}: {2}'.format(noderange, attr, ','.join(map(str, dictout)))
|
||||
else:
|
||||
cprint("CODE ERROR" + repr(attr))
|
||||
cprint(attrout)
|
||||
if not exitcode:
|
||||
if requestargs:
|
||||
for attr in requestargs:
|
||||
if attr not in seenattributes:
|
||||
sys.stderr.write('Error: {0} not a valid attribute\n'.format(attr))
|
||||
exitcode = 1
|
||||
return exitcode
|
||||
|
||||
def updateattrib(session, updateargs, nodetype, noderange, options, dictassign=None):
|
||||
# update attribute
|
||||
exitcode = 0
|
||||
if options.clear:
|
||||
targpath = '/{0}/{1}/attributes/all'.format(nodetype, noderange)
|
||||
keydata = {}
|
||||
for attrib in updateargs[1:]:
|
||||
keydata[attrib] = None
|
||||
for res in session.update(targpath, keydata):
|
||||
if 'error' in res:
|
||||
if 'errorcode' in res:
|
||||
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:
|
||||
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',
|
||||
value, key)
|
||||
except:
|
||||
sys.stderr.write('Error: {0} not a valid expression\n'.format(str(updateargs[1:])))
|
||||
exitcode = 1
|
||||
sys.exit(exitcode)
|
||||
return exitcode
|
||||
|
||||
|
||||
# So we try to prevent bad things from happening when globbing
|
||||
# We tried to head this off at the shell, but the various solutions would end
|
||||
# up breaking the shell in various ways (breaking pipe capability if using
|
||||
# DEBUG, breaking globbing if in pipe, etc)
|
||||
# Then we tried to parse the original commandline instead, however shlex isn't
|
||||
# going to parse full bourne language (e.g. knowing that '|' and '>' and
|
||||
# a world of other things would not be in our command line
|
||||
# so finally, just make sure the noderange appears verbatim in the command line
|
||||
# if we glob to something, then bash will change noderange and this should
|
||||
# detect it and save the user from tragedy
|
||||
def check_globbing(noderange):
|
||||
if not os.path.exists(noderange):
|
||||
return True
|
||||
rawargs = os.environ.get('CURRENT_CMDLINE', None)
|
||||
if rawargs:
|
||||
rawargs = shlex.split(rawargs)
|
||||
for arg in rawargs:
|
||||
if arg.startswith('$'):
|
||||
arg = arg[1:]
|
||||
if arg.endswith(';'):
|
||||
arg = arg[:-1]
|
||||
arg = os.environ.get(arg, '$' + arg)
|
||||
if arg.startswith(noderange):
|
||||
break
|
||||
else:
|
||||
sys.stderr.write(
|
||||
'Shell glob conflict detected, specified target "{0}" '
|
||||
'not in command line, but is a file. You can use "set -f" in '
|
||||
'bash or change directories such that there is no filename '
|
||||
'that would conflict.'
|
||||
'\n'.format(noderange))
|
||||
sys.exit(1)
|
||||
@@ -0,0 +1,85 @@
|
||||
# Copyright 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.
|
||||
|
||||
import fcntl
|
||||
import sys
|
||||
import struct
|
||||
import termios
|
||||
|
||||
def get_screenwidth():
|
||||
return struct.unpack('hh', fcntl.ioctl(sys.stdout, termios.TIOCGWINSZ,
|
||||
b'....'))[1]
|
||||
class ScreenPrinter(object):
|
||||
|
||||
def __init__(self, noderange, client, textlen=4):
|
||||
self.squeeze = sys.stdout.isatty()
|
||||
self.textlen = textlen
|
||||
self.noderange = noderange
|
||||
self.client = client
|
||||
self.nodeoutput = {}
|
||||
self.nodelist = []
|
||||
maxlen = 0
|
||||
for ans in self.client.read('/noderange/{0}/nodes/'.format(noderange)):
|
||||
if 'error' in ans:
|
||||
sys.stderr.write(ans['error'])
|
||||
continue
|
||||
nodename = ans['item']['href'][:-1]
|
||||
if len(nodename) > maxlen:
|
||||
maxlen = len(nodename)
|
||||
self.nodelist.append(nodename)
|
||||
self.nodeoutput[nodename] = ''
|
||||
self.nodenamelen = maxlen
|
||||
self.textlen = textlen
|
||||
self.fieldwidth = maxlen + textlen + 1 # 1 for column
|
||||
|
||||
def set_output(self, node, text):
|
||||
self.nodeoutput[node] = text
|
||||
if len(text) >= self.textlen:
|
||||
self.textlen = len(text) + 1
|
||||
self.fieldwidth = self.textlen + self.nodenamelen + 1
|
||||
self.drawscreen()
|
||||
|
||||
def drawscreen(self):
|
||||
if self.squeeze:
|
||||
currwidth = get_screenwidth()
|
||||
numfields = currwidth // self.fieldwidth
|
||||
fieldformat = '{{0:>{0}}}:{{1:{1}}}'.format(self.nodenamelen,
|
||||
self.textlen)
|
||||
sys.stdout.write('\x1b[2J\x1b[;H') # clear screen
|
||||
else:
|
||||
numfields = 1
|
||||
fieldformat = '{0}: {1}'
|
||||
currfields = 0
|
||||
for node in self.nodelist:
|
||||
if currfields >= numfields:
|
||||
sys.stdout.write('\n')
|
||||
currfields = 1
|
||||
else:
|
||||
currfields += 1
|
||||
sys.stdout.write(fieldformat.format(node, self.nodeoutput[node]))
|
||||
sys.stdout.write('\n')
|
||||
sys.stdout.flush()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
import confluent.client as client
|
||||
c = client.Command()
|
||||
p = ScreenPrinter('n1-n13', c)
|
||||
p.set_output('n3', 'Upload: 67%')
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2014 IBM Corporation
|
||||
# Copyright 2015-2016 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 re
|
||||
|
||||
numregex = re.compile('([0-9]+)')
|
||||
|
||||
|
||||
def naturalize_string(key):
|
||||
"""Analyzes string in a human way to enable natural sort
|
||||
|
||||
:param key: The node name to analyze
|
||||
:returns: A structure that can be consumed by 'sorted'
|
||||
"""
|
||||
return [int(text) if text.isdigit() else text.lower()
|
||||
for text in re.split(numregex, key)]
|
||||
|
||||
|
||||
def natural_sort(iterable):
|
||||
"""Return a sort using natural sort if possible
|
||||
|
||||
:param iterable:
|
||||
:return:
|
||||
"""
|
||||
try:
|
||||
return sorted(iterable, key=naturalize_string)
|
||||
except TypeError:
|
||||
# The natural sort attempt failed, fallback to ascii sort
|
||||
return sorted(iterable)
|
||||
@@ -0,0 +1,217 @@
|
||||
# Copyright 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.
|
||||
|
||||
import difflib
|
||||
import re
|
||||
import sys
|
||||
|
||||
try:
|
||||
range = xrange
|
||||
except NameError:
|
||||
pass
|
||||
|
||||
|
||||
|
||||
numregex = re.compile('([0-9]+)')
|
||||
def humanify_nodename(nodename):
|
||||
"""Analyzes nodename in a human way to enable natural sort
|
||||
|
||||
:param nodename: The node name to analyze
|
||||
:returns: A structure that can be consumed by 'sorted'
|
||||
"""
|
||||
return [int(text) if text.isdigit() else text.lower()
|
||||
for text in re.split(numregex, nodename)]
|
||||
|
||||
|
||||
def _colorize_line(orig, mask):
|
||||
highlighted = False
|
||||
newline = orig[0]
|
||||
for i in range(1, len(orig)):
|
||||
if i > len(mask) - 1:
|
||||
if highlighted:
|
||||
newline += '\x1b[0m'
|
||||
newline += orig[i:]
|
||||
break
|
||||
if highlighted and mask[i] == ' ':
|
||||
highlighted = False
|
||||
newline += '\x1b[0m'
|
||||
elif not highlighted and mask[i] != ' ':
|
||||
highlighted = True
|
||||
newline += '\x1b[31m'
|
||||
newline += orig[i]
|
||||
newline += '\x1b[0m'
|
||||
return newline
|
||||
|
||||
|
||||
def near_diff(idx, diffdata):
|
||||
maxidx = idx + 3
|
||||
if maxidx > len(diffdata) -1:
|
||||
maxidx = len(diffdata) - 1
|
||||
idx = idx - 3
|
||||
if idx < 0:
|
||||
idx = 0
|
||||
while idx <= maxidx:
|
||||
if len(diffdata[idx]) > 0 and diffdata[idx][0] in ('+', '-'):
|
||||
return True
|
||||
idx += 1
|
||||
return False
|
||||
|
||||
|
||||
def colordiff(first, second):
|
||||
diffdata = list(difflib.ndiff(first, second))
|
||||
quiet = True
|
||||
for i in range(len(diffdata)):
|
||||
if i < len(diffdata) - 1 and diffdata[i + 1].startswith('?'):
|
||||
if quiet:
|
||||
quiet = False
|
||||
yield '@@'
|
||||
yield _colorize_line(diffdata[i], diffdata[i + 1])
|
||||
elif not diffdata[i].startswith('?') and near_diff(i, diffdata):
|
||||
if quiet:
|
||||
quiet = False
|
||||
yield '@@'
|
||||
yield diffdata[i]
|
||||
elif not diffdata[i].startswith('?'):
|
||||
quiet = True
|
||||
|
||||
|
||||
|
||||
class GroupedData(object):
|
||||
'''A post processor to sort and compare per-node data
|
||||
|
||||
:param confluentconnection: If given, will attempt to use the connection to abbreviate noderanges
|
||||
'''
|
||||
|
||||
def __init__(self, confluentconnection=None):
|
||||
self.bynode = {}
|
||||
self.byoutput = {}
|
||||
self.header = {}
|
||||
self.client = confluentconnection
|
||||
|
||||
def generate_byoutput(self):
|
||||
self.byoutput = {}
|
||||
for n in self.bynode:
|
||||
output = '\n'.join(self.bynode[n])
|
||||
if output not in self.byoutput:
|
||||
self.byoutput[output] = set([n])
|
||||
else:
|
||||
self.byoutput[output].add(n)
|
||||
|
||||
def add_line(self, node, line):
|
||||
if node not in self.bynode:
|
||||
self.bynode[node] = [line]
|
||||
else:
|
||||
self.bynode[node].append(line)
|
||||
|
||||
def get_group_text(self, nodes):
|
||||
if self.client:
|
||||
headerkey = ','.join(sorted(nodes))
|
||||
if headerkey in self.header:
|
||||
return self.header[headerkey]
|
||||
noderange = ''
|
||||
for reply in self.client.create('/noderange//abbreviate',
|
||||
{'nodes': sorted(nodes)}):
|
||||
noderange = reply['noderange']
|
||||
self.header[headerkey] = noderange
|
||||
return noderange
|
||||
else:
|
||||
return ','.join(sorted(nodes, key=humanify_nodename))
|
||||
|
||||
def print_all(self, output=sys.stdout, skipmodal=False, reverse=False,
|
||||
count=False):
|
||||
self.generate_byoutput()
|
||||
modaloutput = None
|
||||
ismodal = True
|
||||
|
||||
if reverse:
|
||||
outdatalist = sorted(
|
||||
self.byoutput, key=lambda x: [len(self.byoutput[x]),
|
||||
humanify_nodename(
|
||||
self.get_group_text(
|
||||
self.byoutput[x]
|
||||
))])
|
||||
else:
|
||||
outdatalist = sorted(
|
||||
self.byoutput, key=lambda x: [0 - len(self.byoutput[x]),
|
||||
humanify_nodename(
|
||||
self.get_group_text(
|
||||
self.byoutput[x]
|
||||
))])
|
||||
if reverse and skipmodal:
|
||||
# if reversed, the last is biggest and should be skipped if modal
|
||||
outdatalist = outdatalist[:-1]
|
||||
for outdata in outdatalist:
|
||||
if not reverse and skipmodal:
|
||||
# If big first, this makes skipmodal skip first
|
||||
skipmodal = False
|
||||
continue
|
||||
currout = '====================================\n'
|
||||
currout += self.get_group_text(self.byoutput[outdata])
|
||||
currout += '\n====================================\n'
|
||||
if count:
|
||||
currout += 'Count: {0}'.format(len(list(
|
||||
self.byoutput[outdata])))
|
||||
currout += '\n====================================\n'
|
||||
currout += outdata
|
||||
currout += '\n\n'
|
||||
output.write(currout)
|
||||
output.flush()
|
||||
|
||||
def print_deviants(self, output=sys.stdout, skipmodal=False, reverse=False,
|
||||
count=False):
|
||||
self.generate_byoutput()
|
||||
modaloutput = None
|
||||
ismodal = True
|
||||
revoutput = []
|
||||
for outdata in sorted(
|
||||
self.byoutput, key=lambda x: [0 - len(self.byoutput[x]),
|
||||
humanify_nodename(
|
||||
self.get_group_text(
|
||||
self.byoutput[x]
|
||||
))]):
|
||||
if modaloutput is None:
|
||||
modaloutput = outdata
|
||||
if skipmodal:
|
||||
skipmodal = False
|
||||
ismodal = False
|
||||
continue
|
||||
currout = '====================================\n'
|
||||
currout += self.get_group_text(self.byoutput[outdata])
|
||||
currout += '\n====================================\n'
|
||||
if count:
|
||||
currout += 'Count: {0}'.format(len(list(
|
||||
self.byoutput[outdata])))
|
||||
currout += '\n====================================\n'
|
||||
if ismodal:
|
||||
ismodal = False
|
||||
currout += outdata
|
||||
else:
|
||||
currout += '\n'.join(colordiff(modaloutput.split('\n'),
|
||||
outdata.split('\n')))
|
||||
currout += '\n\n'
|
||||
if reverse:
|
||||
revoutput.append(currout)
|
||||
else:
|
||||
output.write(currout)
|
||||
for currout in reversed(revoutput):
|
||||
output.write(currout)
|
||||
output.flush()
|
||||
|
||||
if __name__ == '__main__':
|
||||
groupoutput = GroupedData()
|
||||
for line in sys.stdin.read().split('\n'):
|
||||
if not line:
|
||||
continue
|
||||
groupoutput.add_line(*line.split(': ', 1))
|
||||
groupoutput.print_deviants()
|
||||
@@ -20,6 +20,11 @@ from datetime import datetime
|
||||
import json
|
||||
import struct
|
||||
|
||||
try:
|
||||
unicode
|
||||
except NameError:
|
||||
unicode = str
|
||||
|
||||
def decodestr(value):
|
||||
ret = None
|
||||
try:
|
||||
@@ -38,17 +43,28 @@ def unicode_dictvalues(dictdata):
|
||||
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 xrange(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, str) or isinstance(data, unicode):
|
||||
# plain text, e.g. console data
|
||||
tl = len(data)
|
||||
if tl == 0:
|
||||
@@ -74,6 +90,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)
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
setenv PATH /opt/confluent/bin:$PATH
|
||||
@@ -1,2 +1,255 @@
|
||||
PATH=/opt/confluent/bin:$PATH
|
||||
export PATH
|
||||
MANPATH=/opt/confluent/share/man:$MANPATH
|
||||
export MANPATH
|
||||
# The aliases below are to signify that file globbing is unwelcome at the shell
|
||||
# this avoids a problem if a user does a noderange like 'n[21-33] and there is a file
|
||||
# in the directory like 'n3' that causes the parameter to change and target a totally
|
||||
# different node
|
||||
# Unfortunately in bourne shell, we cannot reliably ensure a prepended set-f
|
||||
# and an appended set +f are both run. alias seems to be the only mechanism
|
||||
# that can intervene before glob expansion, but it lacks power.
|
||||
# putting it into a function to append is all well and good, *except* that
|
||||
# if doing something like 'nodepower compute|grep' causes set -f to execute
|
||||
# in current shell, and the function to be in a subshell and leaves globbing
|
||||
# disabled in the parent shell. Instead, store the current command in a
|
||||
# variable and use that to check for misglobbed noderanges, which was the goal
|
||||
alias nodeattrib='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export CURRENT_CMDLINE; nodeattrib'
|
||||
alias nodebmcreset='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export CURRENT_CMDLINE; nodebmcreset'
|
||||
alias nodeboot='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export CURRENT_CMDLINE; nodeboot'
|
||||
alias nodeconfig='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export CURRENT_CMDLINE; nodeconfig'
|
||||
alias nodeconsole='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export CURRENT_CMDLINE; nodeconsole'
|
||||
alias nodedefine='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export CURRENT_CMDLINE; nodedefine'
|
||||
alias nodeeventlog='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export CURRENT_CMDLINE; nodeeventlog'
|
||||
alias nodefirmware='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export CURRENT_CMDLINE; nodefirmware'
|
||||
alias nodegroupattrib='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export CURRENT_CMDLINE; nodegroupattrib'
|
||||
alias nodegroupdefine='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export CURRENT_CMDLINE; nodegroupdefine'
|
||||
alias nodegroupremove='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export CURRENT_CMDLINE; nodegroupremove'
|
||||
alias nodehealth='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export CURRENT_CMDLINE; nodehealth'
|
||||
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 nodeshell='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export CURRENT_CMDLINE; nodeshell'
|
||||
|
||||
|
||||
_confluent_get_args()
|
||||
{
|
||||
CMPARGS=($COMP_LINE)
|
||||
NUMARGS=${#CMPARGS[@]}
|
||||
if [ "${COMP_WORDS[-1]}" == '' ]; then
|
||||
NUMARGS=$((NUMARGS+1))
|
||||
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[-1]}))
|
||||
fi
|
||||
if [ $NUMARGS -lt 3 ]; then
|
||||
_confluent_nr_completion
|
||||
return;
|
||||
fi
|
||||
}
|
||||
_confluent_nodeidentify_completion()
|
||||
{
|
||||
COMP_CANDIDATES=("on,off -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[-1]}))
|
||||
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[-1]}))
|
||||
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[-1]}))
|
||||
return
|
||||
fi
|
||||
if [ $NUMARGS -gt 3 ]; then
|
||||
compopt -o default
|
||||
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[-1]}))
|
||||
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_nn_completion()
|
||||
{
|
||||
_confluent_get_args
|
||||
if [ $NUMARGS -gt 2 ]; then
|
||||
return;
|
||||
fi
|
||||
INPUT=${COMP_WORDS[-1]}
|
||||
INPUT=${INPUT##*,-}
|
||||
INPUT=${INPUT##*,}
|
||||
INPUT=${INPUT##*@}
|
||||
PREFIX=""
|
||||
if [ "$INPUT" != "${COMP_WORDS[-1]}" ]; then
|
||||
PREFIX=${COMP_WORDS[-1]}
|
||||
PREFIX=$(echo $PREFIX | sed -e 's/,[^,@-]*$/,/' -e 's/,-[^,@]*$/,-/' -e 's/@[^,@]*/@/')
|
||||
fi
|
||||
|
||||
COMPREPLY=($(compgen -W "$(nodelist | sed -e s/^/$PREFIX/)" -- "${COMP_WORDS[-1]}"))
|
||||
}
|
||||
_confluent_nr_completion()
|
||||
{
|
||||
CMPARGS=($COMP_LINE)
|
||||
NUMARGS=${#CMPARGS[@]}
|
||||
if [ "${COMP_WORDS[-1]}" == '' ]; then
|
||||
NUMARGS=$((NUMARGS+1))
|
||||
fi
|
||||
_confluent_get_args
|
||||
if [ $NUMARGS -gt 2 ]; then
|
||||
return;
|
||||
fi
|
||||
INPUT=${COMP_WORDS[-1]}
|
||||
INPUT=${INPUT##*,-}
|
||||
INPUT=${INPUT##*,}
|
||||
INPUT=${INPUT##*@}
|
||||
PREFIX=""
|
||||
if [ "$INPUT" != "${COMP_WORDS[-1]}" ]; then
|
||||
PREFIX=${COMP_WORDS[-1]}
|
||||
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[-1]}"))
|
||||
COMPREPLY=($(compgen -W "$(confetty show /nodegroups|sed -e 's/\///' -e s/^/$PREFIX/;nodelist | sed -e s/^/$PREFIX/)" -- "${COMP_WORDS[-1]}"))
|
||||
}
|
||||
_confluent_ng_completion()
|
||||
{
|
||||
_confluent_get_args
|
||||
if [ $NUMARGS -gt 2 ]; then
|
||||
return;
|
||||
fi
|
||||
INPUT=${COMP_WORDS[-1]}
|
||||
INPUT=${INPUT##*,-}
|
||||
INPUT=${INPUT##*,}
|
||||
INPUT=${INPUT##*@}
|
||||
PREFIX=""
|
||||
if [ "$INPUT" != "${COMP_WORDS[-1]}" ]; then
|
||||
PREFIX=${COMP_WORDS[-1]}
|
||||
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[-1]}"))
|
||||
}
|
||||
complete -F _confluent_nr_completion nodeattrib
|
||||
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_nr_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
|
||||
|
||||
|
||||
Executable
+1
@@ -0,0 +1 @@
|
||||
for i in *.ronn; do echo -n `head -n 1 $i|awk '{print $1}'`; echo " $i"; done > index.txt
|
||||
@@ -0,0 +1,92 @@
|
||||
collate(1) -- Organize text input by node
|
||||
==============================
|
||||
|
||||
## SYNOPSIS
|
||||
|
||||
`<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.
|
||||
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
|
||||
|
||||
* `-a`, `--abbreviate`:
|
||||
Use confluent to attempt to shorten the noderange. This can help identify
|
||||
when output differs along a telling group boundary. For example, if
|
||||
output suggests a large number of nodes are unreachable, abbreviate
|
||||
showing 'rack1' as being unreachable may make more obvious a possible cause.
|
||||
|
||||
* `-d`, `--diff`:
|
||||
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.
|
||||
|
||||
* `-s`, `--skipcommon`:
|
||||
Suppress printing of the most common result text group. This is used to
|
||||
focus on stray output against a well known and expected result.
|
||||
|
||||
* `-c`, `--count`:
|
||||
Print a count of the number of nodes in an output group under the
|
||||
noderange.
|
||||
|
||||
* `-r`, `--reverse`:
|
||||
Rather than starting with most common to least common, start with
|
||||
the least common and print the most common last.
|
||||
|
||||
## EXAMPLES
|
||||
* Organizing power state of multiple nodes:
|
||||
`# nodepower n1-n12 | collate`
|
||||
`====================================`
|
||||
`n1,n2,n3,n4,n7,n8,n9,n10,n11,n12`
|
||||
`====================================`
|
||||
`on`
|
||||
` `
|
||||
`====================================`
|
||||
`n5,n6`
|
||||
`====================================`
|
||||
`off`
|
||||
|
||||
* Using diff to detect distinct UEFI configuration
|
||||
|
||||
`# pasu n1-n4 show Processors|collate -d -s`
|
||||
`====================================`
|
||||
`n3`
|
||||
`====================================`
|
||||
`@@`
|
||||
` Processors.ProcessorPerformanceStates=Enable`
|
||||
` Processors.C-States=Enable`
|
||||
` Processors.PackageACPIC-StateLimit=ACPI C3`
|
||||
`- Processors.C1EnhancedMode=Enable`
|
||||
`+ Processors.C1EnhancedMode=Disable`
|
||||
`- Processors.Hyper-Threading=Enable`
|
||||
`+ Processors.Hyper-Threading=Disable`
|
||||
` Processors.ExecuteDisableBit=Enable`
|
||||
` Processors.IntelVirtualizationTechnology=Enable`
|
||||
` `
|
||||
`====================================`
|
||||
`n1`
|
||||
`====================================`
|
||||
`@@`
|
||||
` Processors.ProcessorPerformanceStates=Enable`
|
||||
` Processors.C-States=Enable`
|
||||
` Processors.PackageACPIC-StateLimit=ACPI C3`
|
||||
`- Processors.C1EnhancedMode=Enable`
|
||||
`+ Processors.C1EnhancedMode=Disable`
|
||||
` Processors.Hyper-Threading=Enable`
|
||||
` Processors.ExecuteDisableBit=Enable`
|
||||
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
confetty(1) --- Interactive confluent client
|
||||
confetty(8) --- Interactive confluent client
|
||||
=================================================
|
||||
|
||||
## SYNOPSIS
|
||||
|
||||
`confetty`
|
||||
`confetty`
|
||||
`confetty <confetty command line>`
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
@@ -33,5 +34,3 @@ commands.
|
||||
Start a console session indicated by **ELEMENT** (e.g. /nodes/n1/console/session)
|
||||
* `rm` **ELEMENT**
|
||||
Request removal of an element. (e.g. rm events/hardware/log clears log from a node)
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
confluent(8) -- Start the confluent server
|
||||
=========================================================
|
||||
|
||||
## SYNOPSIS
|
||||
|
||||
`confluent`
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
**confluent** is the name of the server daemon. It is normally run
|
||||
through the init subsystem rather than executed directly. All confluent
|
||||
commands connect to confluent daemon. It provides the web interface, debug,
|
||||
and unix socket connectivity.
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
confluentdbutil(8) -- Backup or restore confluent database
|
||||
=========================================================
|
||||
|
||||
## SYNOPSIS
|
||||
|
||||
|
||||
`confluentdbutil [options] <dump|restore> <path>`
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
**confluentdbutil** is a utility to export/import the confluent attributes
|
||||
to/from json files. The path is a directory that holds the json version.
|
||||
In order to perform restore, the confluent service must not be running. It
|
||||
is required to indicate how to treat the usernames/passwords are treated in
|
||||
the json files (password protected, removed from the files, or unprotected).
|
||||
|
||||
## OPTIONS
|
||||
|
||||
* `-p`, `--password`:
|
||||
If specified, information such as usernames and passwords will be encrypted
|
||||
using the given password.
|
||||
* `-r`, `--redact`:
|
||||
Indicates to replace usernames and passwords with a dummy string rather
|
||||
than included.
|
||||
* `-u`, `--unprotected`:
|
||||
The keys.json file will include the encryption keys without any protection.
|
||||
@@ -0,0 +1,93 @@
|
||||
nodeattrib(8) -- List or change confluent nodes attributes
|
||||
=========================================================
|
||||
|
||||
## SYNOPSIS
|
||||
|
||||
`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
|
||||
|
||||
**nodeattrib** manages the attributes of confluent nodes. In
|
||||
the simplest form, it simply takes the given noderange(5) and lists the
|
||||
matching nodes, one line at a time.
|
||||
|
||||
If a list of node attribute names are given, the value of those are also
|
||||
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 value.
|
||||
This is different from setting the value to an empty string.
|
||||
|
||||
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.
|
||||
|
||||
## OPTIONS
|
||||
|
||||
* `-b`, `--blame`:
|
||||
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:
|
||||
`# nodeattrib n1-n2`
|
||||
`n1: console.method: ipmi`
|
||||
`n1: hardwaremanagement.manager: 172.30.3.1`
|
||||
`n2: console.method: ipmi`
|
||||
`n2: hardwaremanagement.manager: 172.30.3.2`
|
||||
|
||||
* Getting an attribute of nodes matching a noderange:
|
||||
`# nodeattrib n1,n2 hardwaremanagement.manager`
|
||||
`n1: hardwaremanagement.manager: 172.30.3.1`
|
||||
`n2: hardwaremanagement.manager: 172.30.3.2`
|
||||
|
||||
* Getting a group of attributes while determining what group defines them:
|
||||
`# nodeattrib n1,n2 hardwaremanagement --blame`
|
||||
`n1: hardwaremanagement.manager: 172.30.3.1`
|
||||
`n1: hardwaremanagement.method: ipmi (inherited from group everything)`
|
||||
`n1: hardwaremanagement.switch: r8e1`
|
||||
`n1: hardwaremanagement.switchport: 14`
|
||||
`n2: hardwaremanagement.manager: 172.30.3.2`
|
||||
`n2: hardwaremanagement.method: ipmi (inherited from group everything)`
|
||||
`n2: hardwaremanagement.switch: r8e1`
|
||||
`n2: hardwaremanagement.switchport: 2`
|
||||
|
||||
* Listing matching nodes of a simple noderange that are set:
|
||||
`# nodeattrib n1-n2 current`
|
||||
`n1: console.method: ipmi`
|
||||
`n1: hardwaremanagement.manager: 172.30.3.1`
|
||||
`n2: console.method: ipmi`
|
||||
`n2: hardwaremanagement.manager: 172.30.3.2`
|
||||
|
||||
* Change attribute on nodes of a simple noderange:
|
||||
`# nodeattrib n1-n2 console.method=serial`
|
||||
`n1: console.method: serial`
|
||||
`n1: hardwaremanagement.manager: 172.30.3.1`
|
||||
`n2: console.method: serial`
|
||||
`n2: hardwaremanagement.manager: 172.30.3.2`
|
||||
|
||||
* Clear attribute on nodes of a simple noderange, if you want to retain the variable set the attribute to "":
|
||||
`# nodeattrib n1-n2 -c console.method`
|
||||
`# nodeattrib n1-n2 console.method`
|
||||
`n1: console.method: `
|
||||
`n2: console.method: `
|
||||
|
||||
## SEE ALSO
|
||||
|
||||
nodegroupattrib(8), nodeattribexpressions(5)
|
||||
@@ -0,0 +1,64 @@
|
||||
nodeattribexpressions(5) -- Confluent attribute expression syntax
|
||||
=================================================================
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
In confluent, any attribute may either be a straightforward value, or an
|
||||
expression to generate the value.
|
||||
|
||||
An expression will contain some directives wrapped in `{}` characters. Within
|
||||
`{}` are a number of potential substitute values and operations.
|
||||
|
||||
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.
|
||||
Additionally, {n0} is special as representing the last number in a name, so in
|
||||
the b1o2r3u4 example, {n0} would be 4.
|
||||
|
||||
Frequently a value derives from a number in the node name, but must undergo a
|
||||
transform to be useful. As an example, if we have a scheme where nodes are
|
||||
numbered n1-n512, and they are arranged 1-42 in rack1, 43-84 in rack2, and so
|
||||
forth, it is convenient to perform arithmetic on the extracted number. Here is
|
||||
an example of codifying the above scheme, and setting the u to the remainder:
|
||||
|
||||
`location.rack=rack{(n1-1)/42+1}`
|
||||
`location.u={(n1-1)%42+1}`
|
||||
|
||||
Note how text may be mixed into expressions, only data within {} will receive
|
||||
special treatment. Here we also had to adjust by subtracting 1 and adding it
|
||||
back to make the math work as expected.
|
||||
|
||||
It is sometimes the case that the number must be formatted a different way,
|
||||
either specifying 0 padding or converting to hexadecimal. This can be done by a
|
||||
number of operators at the end to indicate formatting changes.
|
||||
|
||||
`{n1:02x} - Zero pad to two decimal places, and convert to hexadecimal, as mightbe used for generating MAC addresses`
|
||||
`{n1:x} - Hexadecimal without padding, as may be used in a generated IPv6 address`
|
||||
`{n1:X} - Uppercase hexadecimal`
|
||||
`{n1:02d} - Zero pad a normal numeric representation of the number.`
|
||||
|
||||
Another common element to pull into an expression is the node name in whole:
|
||||
|
||||
`hardwaremanagement.manager={node}-imm`
|
||||
|
||||
Additionally other attributes may be pulled in:
|
||||
|
||||
`hardwaremanagement.switchport={location.u}`
|
||||
|
||||
Multiple expressions are permissible within a single attribute:
|
||||
|
||||
`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
|
||||
expression. For example, submitting the following JSON:
|
||||
|
||||
`{ 'location.rack': '{n1}' }`
|
||||
|
||||
Will auto-detect {n1} as an expression and assign it normally. If wanting to
|
||||
set that value verbatim, it can either be escaped by doubling the {} or by
|
||||
explicitly declaring it as a value:
|
||||
|
||||
`{ 'location.rack': '{{n1}}' }`
|
||||
|
||||
`{ 'location.rack': { 'value': '{n1}' } }`
|
||||
@@ -0,0 +1,19 @@
|
||||
nodebmcreset(8) -- Reset management controller
|
||||
=========================================================
|
||||
|
||||
## SYNOPSIS
|
||||
|
||||
`nodebmcreset <noderange>`
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
`nodebmcreset` allows you to reset the management controller of the specified noderange
|
||||
|
||||
## EXAMPLES:
|
||||
|
||||
* Reset the management controller for nodes n1 through n4:
|
||||
`# nodebmcreset n1-n4`
|
||||
`n1: BMC Reset Successful`
|
||||
`n2: BMC Reset Successful`
|
||||
`n3: BMC Reset Successful`
|
||||
`n4: BMC Reset Successful`
|
||||
@@ -0,0 +1,66 @@
|
||||
nodeboot(8) -- Reboot a confluent node to a specific device
|
||||
=========================================================
|
||||
|
||||
## SYNOPSIS
|
||||
|
||||
`nodeboot <noderange>`
|
||||
`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. 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:
|
||||
`# nodeboot n3-n4`
|
||||
`n3: default`
|
||||
`n4: default`
|
||||
`n3: on->reset`
|
||||
`n4: on->reset`
|
||||
|
||||
* Booting n1 and n2 to setup menu:
|
||||
`# nodeboot n1-n2 setup`
|
||||
`n2: setup`
|
||||
`n1: setup`
|
||||
`n2: on->reset`
|
||||
`n1: on->reset`
|
||||
|
||||
* Booting n3 and n4 to network:
|
||||
`# nodeboot n3-n4 net`
|
||||
`n3: network`
|
||||
`n4: network`
|
||||
`n4: on->reset`
|
||||
`n3: off->on`
|
||||
@@ -0,0 +1,55 @@
|
||||
nodeconfig(8) -- Show or change node configuration
|
||||
==================================================
|
||||
|
||||
## SYNOPSIS
|
||||
|
||||
`nodeconfig <noderange> [options] [<configuration>..]`
|
||||
`nodeconfig <noderange> [options] [<configuration=value>..]`
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
**nodeconfig** manages the configuration of nodes managed by confluent.
|
||||
Rather than manipulating the confluent database, this actually modifies the
|
||||
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.
|
||||
|
||||
|
||||
## EXAMPLES
|
||||
* Showing the current IP configuration of noderange BMC/IMM/XCC:
|
||||
`# nodeconfig s3,s4 bmc`
|
||||
`s3: bmc.ipv4_address: 172.30.254.193/16`
|
||||
`s3: bmc.ipv4_method: DHCP`
|
||||
`s3: bmc.ipv4_gateway: 172.30.0.6`
|
||||
`s4: bmc.ipv4_address: 172.30.254.192/16`
|
||||
`s4: bmc.ipv4_method: DHCP`
|
||||
`s4: bmc.ipv4_gateway: 172.30.0.6`
|
||||
|
||||
* Changing nodes `s3` and `s4` to have the ip addressess 10.1.2.3 and 10.1.2.4 with a 16 bit subnet mask:
|
||||
`# nodeconfig s3,s4 bmc.ipv4_address=10.1.2.{n1}/16`
|
||||
|
||||
## SEE ALSO
|
||||
|
||||
nodeattribexpressions(5)
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
nodeconsole(1) -- Open a console to a confluent node
|
||||
nodeconsole(8) -- Open a console to a confluent node
|
||||
=====================================================
|
||||
|
||||
## SYNOPSIS
|
||||
`nodeconsole` `node`
|
||||
`nodeconsole <node>`
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
**nodeconsole** opens an interactive console session to a given node. This is the
|
||||
text or serial console of a system. Exiting is done by hitting `Ctrl-e`, then `c`,
|
||||
then `.`. Note that console output by default is additionally logged to
|
||||
then `.`. Note that console output by default is additionally logged to
|
||||
`/var/log/confluent/consoles/`**NODENAME**.
|
||||
|
||||
When the console connection to the target is broken, then confluent on backend
|
||||
will initiate an automatic retry interval that is randomized between 2 and 4 minutes.
|
||||
The reopen escape sequence below requests an immediate retry, as does connecting
|
||||
a new session.
|
||||
|
||||
## ESCAPE SEQUENCE COMMANDS
|
||||
|
||||
While connected to a console, a number of commands may be performed through escape
|
||||
@@ -20,11 +25,33 @@ keystroke will be interpreted as a command. The following commands are availabl
|
||||
* `.`:
|
||||
Exit the session and return to the command prompt
|
||||
* `b`:
|
||||
[send Break]
|
||||
Send a break to the remote console when possible (some console plugins may not support this)
|
||||
* `o`:
|
||||
[reOpen]
|
||||
Request confluent to disconnect and reconnect to console. For example if there is suspicion
|
||||
that the console has gone inoperable, but would work if reconnected.
|
||||
* `po`:
|
||||
[Power Off]
|
||||
Power off server immediately, without waiting for OS to shutdown
|
||||
* `ps`:
|
||||
[Power Shutdown]
|
||||
Request OS shut down gracefully, and then power off
|
||||
* `pb<ent>`:
|
||||
[Power Boot]
|
||||
Cause system to immediately boot, resetting or turning on as appropriate.
|
||||
Hitting enter is required to execute the reboot rather than another pb sequence
|
||||
* `pbs`:
|
||||
[Power Boot Setup]
|
||||
Request immediate boot ultimately landing in interactive firmware setup
|
||||
* `pbn`:
|
||||
[Power Boot Network]
|
||||
Request immediate boot to network
|
||||
* `r`:
|
||||
[send Resize]
|
||||
This queries the current terminal and sends stty commands to advertise the user termineal
|
||||
size to the remote console
|
||||
* `?`:
|
||||
Get a list of supported commands
|
||||
* `<enter>`:
|
||||
Abandon entering an escape sequence command
|
||||
* `<ent>`:
|
||||
Hit enter to skip entering a command at the escape prompt.
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
nodedefine(8) -- Define new confluent nodes
|
||||
===================================================================
|
||||
|
||||
## SYNOPSIS
|
||||
|
||||
`nodedefine <noderange> [nodeattribute1=value1> <nodeattribute2=value2> ...]`
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
`nodedefine` allows the definition of new nodes for the confluent management
|
||||
system. It has the same syntax as `nodeattrib(8)`, and the commands differ in
|
||||
that `nodeattrib(8)` will error if a node does not exist.
|
||||
|
||||
## EXAMPLES
|
||||
|
||||
* Define two racks of nodes, named r{rack}u{u}:
|
||||
`# nodedefine r1u1-r2u4`
|
||||
`r1u4: created`
|
||||
`r1u1: created`
|
||||
`r1u2: created`
|
||||
`r1u3: created`
|
||||
`r2u4: created`
|
||||
`r2u3: created`
|
||||
`r2u2: created`
|
||||
`r2u1: created`
|
||||
|
||||
## SEE ALSO
|
||||
|
||||
noderange(5), nodeattribexpressions(8)
|
||||
@@ -0,0 +1,130 @@
|
||||
nodediscover(8) -- List or manage confluent node discovery
|
||||
=========================================================
|
||||
|
||||
## SYNOPSIS
|
||||
|
||||
`nodediscover rescan`
|
||||
`nodediscover [options] list`
|
||||
`nodediscover [options] assign`
|
||||
`nodediscover [options] rescan`
|
||||
`nodediscover [options] clear`
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
**nodediscover** provides streamlined access to the confluent discovery data
|
||||
and assignment. Nodes are detected through either an active scan (as occurs
|
||||
at service startup and on request by nodediscover rescan) or through passive
|
||||
detection (as a target comes online, it may attempt to register with the
|
||||
network).
|
||||
|
||||
**nodediscover list** provides the currently known data in tabular format. The
|
||||
data may be filtered by various parameters, as denoted in the options below.
|
||||
|
||||
**nodediscover assign** performs manual discovery, assigning an entry to a node
|
||||
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
|
||||
columns followed by the data. The available columns are:
|
||||
|
||||
* node: The name desired for the node in confluent
|
||||
* groups: A comma delimited list of groups to put the node into (using normal CSV escape rules for the commas)
|
||||
* mac: The mac address of the node
|
||||
* serial: The serial number of the node
|
||||
* uuid: The uuid of the node
|
||||
* bmc: The name or ip address that should be assigned to the BMC, regardless of current address
|
||||
* bmc_gateway: IP address of gateway, if desired
|
||||
* bmcuser: The desired username for the BMC to have as administrator
|
||||
* bmcpass: The desired password for the BMC to require
|
||||
|
||||
Note that node is the only mandatory field. To identify the systems, one of
|
||||
mac, serial, or uuid should be specified, it is pointless to provide more than
|
||||
one of these columns. Other attributes if not provided may be defined through
|
||||
nodeattrib or group inherited. It is possible to define nodes without ever
|
||||
providing a BMC ip, in which case IPv6 will be used automatically if possible.
|
||||
|
||||
One example of a valid CSV file would be:
|
||||
node,serial,bmc,bmcuser,bmcpass
|
||||
n1,06DPMDF,172.30.204.1,admin,Passw0rd12
|
||||
n2,J30002HG,172.30.204.2,admin,Passw0rd12
|
||||
|
||||
Which would use the serial number to assign the name and other three values to
|
||||
the nodes.
|
||||
|
||||
## OPTIONS
|
||||
|
||||
* `-m MODEL`, `--model=MODEL`:
|
||||
Operate with nodes matching the specified model number
|
||||
* `-s SERIAL`, `--serial=SERIAL`:
|
||||
Operate against the system matching the specified
|
||||
serial number
|
||||
* `-u UUID`, `--uuid=UUID`:
|
||||
Operate against the system matching the specified UUID
|
||||
* `-n NODE`, `--node=NODE`:
|
||||
Operate with the given nodename
|
||||
* `-e MAC`, `--ethaddr=MAC`:
|
||||
Operate against the system with the specified MAC
|
||||
address
|
||||
* `-t TYPE`, `--type=TYPE`:
|
||||
Operate against the system of the specified type
|
||||
* `-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
|
||||
|
||||
* Listing all detected Lenovo IMMv2 systems on a local network:
|
||||
`# nodediscover list -t lenovo-imm2`
|
||||
` Node| Model| Serial| UUID| Mac Address| Type| Current IP Addresses`
|
||||
`---------------|---------------|---------------|------------------------------------|-----------------|------------|------------------------------------------------`
|
||||
` r2| 5463AC1| 06DPMDF|5f7133b8-c8cb-11e4-99a9-40f2e9b91018|40:f2:e9:b9:10:1d| lenovo-imm2| 172.30.204.1,fe80::42f2:e9ff:feb9:101d%eth1`
|
||||
` | 7906AC1| 06PBX15|e98d483d-2759-11e1-8ffd-5cf3fc11249c|5c:f3:fc:11:24:9f| lenovo-imm2| 172.30.3.12,fe80::5ef3:fcff:fe11:249f%eth1`
|
||||
` n1| 8737AC1| 23XXH41|14dd3ba6-5c38-11e1-931a-5cf3fc6e4680|5c:f3:fc:6e:13:e1| lenovo-imm2| 172.30.3.1,fe80::5ef3:fcff:fe6e:13e1%eth1`
|
||||
` n7| 8737AC1| 23XXH32|79d2ce28-5cd5-11e1-8c86-5cf3fc6e46b0|5c:f3:fc:6e:13:f9| lenovo-imm2| 172.30.3.7,fe80::5ef3:fcff:fe6e:13f9%eth1`
|
||||
` n8| 8737AC1| 23XXH49|551a8438-5cd5-11e1-8d6c-5cf3fc6e4708|5c:f3:fc:6e:14:25| lenovo-imm2| 172.30.3.8,fe80::5ef3:fcff:fe6e:1425%eth1`
|
||||
` n3| 8737AC1| 23XXH30|1dd7f7b3-5da5-11e1-baf0-5cf3fc6e4738|5c:f3:fc:6e:14:3d| lenovo-imm2| 172.30.3.3,fe80::5ef3:fcff:fe6e:143d%eth1`
|
||||
` n4| 8737AC1| 23XXH35|45b81dae-5d9b-11e1-8337-5cf3fc6e4858|5c:f3:fc:6e:14:cd| lenovo-imm2| 172.30.3.4,fe80::5ef3:fcff:fe6e:14cd%eth1`
|
||||
` n11| 8737AC1| 23XXH12|31d90128-5c37-11e1-bdb7-5cf3fc6e4920|5c:f3:fc:6e:15:31| lenovo-imm2| 172.30.3.11,fe80::5ef3:fcff:fe6e:1531%eth1`
|
||||
` n13| 8737AC1| 23XXH44|e23a138a-5cd3-11e1-8f3d-5cf3fc6e4950|5c:f3:fc:6e:15:49| lenovo-imm2| 172.30.3.13,fe80::5ef3:fcff:fe6e:1549%eth1`
|
||||
` | 8737AC1| 23XXH29|5cd1216b-5c37-11e1-ba0c-5cf3fc6e49c8|5c:f3:fc:6e:15:85| lenovo-imm2| 172.30.3.9,fe80::5ef3:fcff:fe6e:1585%eth1`
|
||||
` | 8737AC1| 23ZYT44|f4bf48ca-71f0-11e1-b274-5cf3fc6e4f10|5c:f3:fc:6e:18:29| lenovo-imm2| 172.30.3.10,fe80::5ef3:fcff:fe6e:1829%eth1`
|
||||
` hpcedr| 7915AC1| 06DRHL5|a64e3014-d7e3-11e1-8d21-6cae8b1dff32|6c:ae:8b:1d:ff:36| lenovo-imm2| 172.30.254.250,fe80::6eae:8bff:fe1d:ff36%eth1`
|
||||
` | 8737AC1| 06YRWC3|3af85a51-7efd-11e3-8599-000af7482e00|6c:ae:8b:32:cb:c5| lenovo-imm2| 172.30.3.6,fe80::6eae:8bff:fe32:cbc5%eth1`
|
||||
` | 8737AC1| 06YRWB7|b230f62e-7efd-11e3-9773-000af7482980|6c:ae:8b:32:cd:01| lenovo-imm2| 172.30.3.5,fe80::6eae:8bff:fe32:cd01%eth1`
|
||||
` | 8737AC1| 06YRWC7|09586005-7efe-11e3-9f03-000af7482df0|6c:ae:8b:32:cd:a5| lenovo-imm2| 172.30.3.2,fe80::6eae:8bff:fe32:cda5%eth1`
|
||||
|
||||
* Manually assign a single node according to serial number:
|
||||
`[root@odin ~]# nodediscover assign -s 06PBX15 -n n12`
|
||||
`Assigned: n12`
|
||||
|
||||
* Bulk execute discovery based on spreadsheet:
|
||||
`[root@odin ~]# nodediscover assign -i import.csv`
|
||||
`Defined r2`
|
||||
`Discovered r2`
|
||||
`Defined c1`
|
||||
`Discovered c1`
|
||||
@@ -0,0 +1,33 @@
|
||||
nodeeventlog(8) -- Pull eventlog from confluent nodes
|
||||
============================================================
|
||||
|
||||
## SYNOPSIS
|
||||
|
||||
`nodeeventlog <noderange>`
|
||||
`nodeeventlog <noderange> [clear]`
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
`nodeeventlog` pulls and optionally clears the event log from the requested
|
||||
noderange.
|
||||
|
||||
## EXAMPLES
|
||||
* Pull the event log from n2 and n3:
|
||||
`# nodeeventlog n2,n3`
|
||||
`n2: 05/03/2017 11:44:25 Event Log Disabled - SEL Fullness - Log clear`
|
||||
`n2: 05/03/2017 11:44:56 System Firmware - Progress - Unspecified`
|
||||
`n3: 05/03/2017 11:44:39 Event Log Disabled - SEL Fullness - Log clear`
|
||||
`n3: 05/03/2017 11:45:00 System Firmware - Progress - Unspecified`
|
||||
`n3: 05/03/2017 11:47:22 System Firmware - Progress - Starting OS boot`
|
||||
|
||||
* Pull and clear the event log from n2 and n3:
|
||||
`# nodeeventlog n2,n3 clear`
|
||||
`n2: 05/03/2017 11:44:25 Event Log Disabled - SEL Fullness - Log clear`
|
||||
`n2: 05/03/2017 11:44:56 System Firmware - Progress - Unspecified`
|
||||
`n2: 05/03/2017 11:48:29 System Firmware - Progress - Starting OS boot`
|
||||
`n3: 05/03/2017 11:44:39 Event Log Disabled - SEL Fullness - Log clear`
|
||||
`n3: 05/03/2017 11:45:00 System Firmware - Progress - Unspecified`
|
||||
`n3: 05/03/2017 11:47:22 System Firmware - Progress - Starting OS boot`
|
||||
`# nodeeventlog n2,n3`
|
||||
`n2: 05/03/2017 11:48:48 Event Log Disabled - SEL Fullness - Log clear`
|
||||
`n3: 05/03/2017 11:48:52 Event Log Disabled - SEL Fullness - Log clear`
|
||||
@@ -0,0 +1,43 @@
|
||||
nodefirmware(8) -- Report firmware information on confluent nodes
|
||||
=================================================================
|
||||
|
||||
## SYNOPSIS
|
||||
|
||||
`nodefirmware <noderange>`
|
||||
`nodefirmware <noderange> list|<components>|core`
|
||||
`nodefirmware <noderange> update [--backup] <filename>`
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
`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.
|
||||
|
||||
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.
|
||||
* `complete`: The firmware update process has completed and activation has proceeded. Note that while the activation process has commenced, the component may still be in the process of rebooting when nodefirmware exits.
|
||||
|
||||
## EXAMPLES
|
||||
|
||||
* Pull firmware from a node:
|
||||
`# nodefirmware r1`
|
||||
`r1: IMM: 3.70 (TCOO26H 2016-11-29T05:09:51)`
|
||||
`r1: IMM Backup: 1.71 (TCOO10D 2015-04-17T00:00:00)`
|
||||
`r1: IMM Trusted Image: TCOO26H`
|
||||
`r1: UEFI: 2.31 (TCE128I 2016-12-13T00:00:00)`
|
||||
`r1: UEFI Backup: 2.20 (TCE126O)`
|
||||
`r1: FPGA: 3.2.0`
|
||||
`r1: Broadcom NetXtreme Gigabit Ethernet Controller Bootcode: 1.38`
|
||||
`r1: Broadcom NetXtreme Gigabit Ethernet Controller MBA: 16.8.0`
|
||||
`r1: Broadcom NetXtreme Gigabit Ethernet Controller Firmware Package: 0.0.0a`
|
||||
`r1: ServeRAID M1215 MegaRAID Controller Firmware: 24.12.0-0038 (2016-10-20T00:00:00)`
|
||||
`r1: ServeRAID M1215 Disk 28 MBF2600RC: SB2C`
|
||||
`r1: ServeRAID M1215 Disk 29 MBF2600RC: SB2C`
|
||||
`r1: ServeRAID M5210 Disk 0 MBF2600RC: SB2C`
|
||||
`r1: ServeRAID M5210 Disk 1 MBF2600RC: SB2C`
|
||||
`r1: ServeRAID M5210 Disk 2 MBF2600RC: SB2C`
|
||||
@@ -0,0 +1,43 @@
|
||||
nodegroupattrib(8) -- List or change confluent nodegroup attributes
|
||||
===================================================================
|
||||
|
||||
## SYNOPSIS
|
||||
|
||||
`nodegroupattrib <group> [ current | all ]`
|
||||
`nodegroupattrib <group> [<nodeattribute>...]`
|
||||
`nodegroupattrib <group> [<nodeattribute1=value1> <nodeattribute2=value2> ...]`
|
||||
`nodegroupattrib <group> [-c] [<nodeattribute1> <nodeattribute2=value2> ...]`
|
||||
`nodeattrib -p <noderange> <nodeattribute1> <nodeattribute2> ...`
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
`nodegroupattrip` queries the confluent server to get information about nodes.
|
||||
In the simplest form, it simply takes the given group and lists the attributes of that group.
|
||||
|
||||
Contrasted with nodeattrib(8), settings managed by nodegroupattrib will be added
|
||||
and removed from a node as it is added or removed from a group. If an attribute
|
||||
is set using nodeattrib(8) against a noderange(5) that happens to be a group name,
|
||||
nodeattrib(8) individually sets attributes directly on each individual node that is
|
||||
currently a member of that group. Removing group membership or adding a new
|
||||
node after using the nodeattrib(8) command will not have attributes change automatically.
|
||||
It's easiest to see by using the `nodeattrib <noderange> -b` to understand how
|
||||
the attributes are set on the node versus a group to which a node belongs.
|
||||
|
||||
## OPTIONS
|
||||
|
||||
* `-c`, `--clear`:
|
||||
Clear specified nodeattributes.
|
||||
|
||||
## EXAMPLES
|
||||
|
||||
* Show attributes of a group called `demogrp`:
|
||||
`# nodegroupattrib demogrp`
|
||||
`demogrp: hardwaremanagement.manager: (will derive from expression 10.30.{n0/255}.{n0%255})`
|
||||
`demogrp: nodes: n12,n13,n10,n11,n9,n1,n2,n3,n4`
|
||||
|
||||
* Set location.u to be the remainder of first number in node name when divided by 42:
|
||||
`# nodegroupattrib demogrp location.u={n1%42}`
|
||||
|
||||
## SEE ALSO
|
||||
|
||||
nodeattrib(8), nodeattribexpressions(5)
|
||||
@@ -0,0 +1,24 @@
|
||||
nodegroupdefine(8) -- Define new confluent node group
|
||||
===================================================================
|
||||
|
||||
## SYNOPSIS
|
||||
|
||||
`nodegroupdefine <groupname> [nodeattribute1=value1> <nodeattribute2=value2> ...]`
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
`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.
|
||||
|
||||
## EXAMPLES
|
||||
|
||||
* Create a group called `compute`:
|
||||
`# nodegroupdefine compute`
|
||||
`compute: created`
|
||||
|
||||
|
||||
## SEE ALSO
|
||||
|
||||
nodeattribexpressions(8), nodegroupattrib(8), nodegroupremove(8)
|
||||
@@ -0,0 +1,18 @@
|
||||
nodegroupremove(8) -- Remove a nodegroup from the confluent database
|
||||
====================================================================
|
||||
|
||||
## SYNOPSIS
|
||||
|
||||
`nodegroupremove <noderange>`
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
`nodegroupremove` simply removes the given single nodegroup from the confluent database.
|
||||
|
||||
|
||||
## EXAMPLES
|
||||
|
||||
* Remove group called testgroup
|
||||
`# nodegroupremove testgroup`
|
||||
`testgroup: deleted`
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
nodehealth(8) -- Show health summary of confluent nodes
|
||||
========================================================
|
||||
|
||||
## SYNOPSIS
|
||||
|
||||
`nodehealth <noderange>`
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
`nodehealth` reports the current health assessment of a confluent node. It
|
||||
will report either `ok`, `warning`, `critical`, or `failed`, along with
|
||||
a string explaining the reason for any result other than `ok`.
|
||||
|
||||
## EXAMPLES
|
||||
|
||||
* Pull health summary of 5 nodes:
|
||||
`# nodehealth n1-n4,r1`
|
||||
`n1: critical (Mezz Exp 2 Fault:Critical)`
|
||||
`n3: ok`
|
||||
`n2: ok`
|
||||
`r1: ok`
|
||||
`n4: ok`
|
||||
@@ -0,0 +1,31 @@
|
||||
nodeidentify(8) -- Control the identify LED of confluent nodes
|
||||
=========================================================
|
||||
|
||||
## SYNOPSIS
|
||||
|
||||
`nodidentify <noderange> [on|off]`
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
`nodeidentify` allows you to turn on or off the location LED of conflueunt nodes,
|
||||
making it easier to determine the physical location of the nodes. The following
|
||||
options are supported:
|
||||
|
||||
* `on`: Turn on the identify LED
|
||||
* `off`: Turn off the identify LED
|
||||
|
||||
## EXAMPLES:
|
||||
|
||||
* Turn on the identify LED on nodes n1 through n4:
|
||||
`# nodeidentify n1-n4 on`
|
||||
`n1: on`
|
||||
`n2: on`
|
||||
`n3: on`
|
||||
`n4: on`
|
||||
|
||||
* Turn off the identify LED on nodes n1 thorugh n4:
|
||||
`# nodeidentify n1-n4 off`
|
||||
`n1: off`
|
||||
`n2: off`
|
||||
`n4: off`
|
||||
`n3: off`
|
||||
@@ -0,0 +1,121 @@
|
||||
nodeinventory(8) -- Get hardware inventory of confluent node
|
||||
===============================================================
|
||||
|
||||
## SYNOPSIS
|
||||
|
||||
`nodeinventory <noderange> [serial|model|uuid|mac]`
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
`nodeinventory` pulls information about hardware of a node. This includes
|
||||
information such as adapters, serial numbers, processors, and memory modules,
|
||||
as supported by the platforms hardware management implementation. It accepts
|
||||
arguments such as serial or model or others as listed above to filter
|
||||
output to specific data.
|
||||
|
||||
## EXAMPLES
|
||||
|
||||
* Pulling inventory of a node named r1:
|
||||
`# nodeinventory r1`
|
||||
`r1: System MAC Address 1: 40:f2:e9:af:45:a0`
|
||||
`r1: System MAC Address 2: 40:f2:e9:af:45:a1`
|
||||
`r1: System MAC Address 3: 40:f2:e9:af:45:a2`
|
||||
`r1: System MAC Address 4: 40:f2:e9:af:45:a3`
|
||||
`r1: System Board manufacturer: IBM`
|
||||
`r1: System Product name: System x3650 M5`
|
||||
`r1: System Device ID: 32`
|
||||
`r1: System Revision: 9`
|
||||
`r1: System Product ID: 323`
|
||||
`r1: System Board model: 00KG915`
|
||||
`r1: System Device Revision: 0`
|
||||
`r1: System Serial Number: E2K4831`
|
||||
`r1: System Board manufacture date: 2014-10-20T12:00`
|
||||
`r1: System Board serial number: Y010UF4AL0B5`
|
||||
`r1: System Manufacturer: IBM`
|
||||
`r1: System FRU Number: 00FK639`
|
||||
`r1: System Board product name: System Board`
|
||||
`r1: System Model: 5462AC1`
|
||||
`r1: System UUID: 1B29CE46-765E-31A3-A3B9-B5FB934F15AB`
|
||||
`r1: System Hardware Version: 0x0000`
|
||||
`r1: System Manufacturer ID: 20301`
|
||||
`r1: System Chassis serial number: E2K4831`
|
||||
`r1: System Asset Number: `
|
||||
`r1: System Chassis type: Other`
|
||||
`r1: Power Supply 1 Board model: 94Y8136`
|
||||
`r1: Power Supply 1 Board manufacturer: EMER`
|
||||
`r1: Power Supply 1 FRU Number: 94Y8137`
|
||||
`r1: Power Supply 1 Board product name: IBM Designed Device`
|
||||
`r1: Power Supply 1 Board manufacture date: 2014-11-08T00:00`
|
||||
`r1: Power Supply 1 Board serial number: K13814B88ED`
|
||||
`r1: Power Supply 1 Revision: 49`
|
||||
`r1: Power Supply 2: Not Present`
|
||||
`r1: DASD Backplane 1 Board model: 00JY139`
|
||||
`r1: DASD Backplane 1 Board manufacturer: WIST`
|
||||
`r1: DASD Backplane 1 FRU Number: 00FJ756`
|
||||
`r1: DASD Backplane 1 Board product name: IBM Designed Device`
|
||||
`r1: DASD Backplane 1 Board manufacture date: 2014-08-28T00:00`
|
||||
`r1: DASD Backplane 1 Board serial number: Y011UF48W02U`
|
||||
`r1: DASD Backplane 1 Revision: 0`
|
||||
`r1: DASD Backplane 2: Not Present`
|
||||
`r1: DASD Backplane 3: Not Present`
|
||||
`r1: DASD Backplane 4: Not Present`
|
||||
`r1: DASD Backplane 5 Board model: 00YJ530`
|
||||
`r1: DASD Backplane 5 Board manufacturer: WIST`
|
||||
`r1: DASD Backplane 5 FRU Number: 00AL953`
|
||||
`r1: DASD Backplane 5 Board product name: IBM Designed Device`
|
||||
`r1: DASD Backplane 5 Board manufacture date: 2016-02-04T00:00`
|
||||
`r1: DASD Backplane 5 Board serial number: Y010UF624024`
|
||||
`r1: DASD Backplane 5 Revision: 0`
|
||||
`r1: DASD Backplane 6: Not Present`
|
||||
`r1: CPU 1 Hardware Version: Intel(R) Xeon(R) CPU E5-2640 v3 @ 2.60GHz`
|
||||
`r1: CPU 1 Asset Number: Unknown`
|
||||
`r1: CPU 1 Manufacturer: Intel(R) Corporation`
|
||||
`r1: CPU 2: Not Present`
|
||||
`r1: ML2 Card: Not Present`
|
||||
`r1: DIMM 1: Not Present`
|
||||
`r1: DIMM 2: Not Present`
|
||||
`r1: DIMM 3: Not Present`
|
||||
`r1: DIMM 4: Not Present`
|
||||
`r1: DIMM 5: Not Present`
|
||||
`r1: DIMM 6: Not Present`
|
||||
`r1: DIMM 7: Not Present`
|
||||
`r1: DIMM 8: Not Present`
|
||||
`r1: DIMM 9: Not Present`
|
||||
`r1: DIMM 10: Not Present`
|
||||
`r1: DIMM 11: Not Present`
|
||||
`r1: DIMM 12: Not Present`
|
||||
`r1: DIMM 13: Not Present`
|
||||
`r1: DIMM 14: Not Present`
|
||||
`r1: DIMM 15: Not Present`
|
||||
`r1: DIMM 16: Not Present`
|
||||
`r1: DIMM 17: Not Present`
|
||||
`r1: DIMM 18: Not Present`
|
||||
`r1: DIMM 19: Not Present`
|
||||
`r1: DIMM 20: Not Present`
|
||||
`r1: DIMM 21: Not Present`
|
||||
`r1: DIMM 22: Not Present`
|
||||
`r1: DIMM 23: Not Present`
|
||||
`r1: DIMM 24: Not Present`
|
||||
`r1: X8 PCI 1: Not Present`
|
||||
`r1: X8 PCI 2: Not Present`
|
||||
`r1: X8 PCI 6: Not Present`
|
||||
`r1: X8 PCI 7: Not Present`
|
||||
`r1: Broadcom NetXtreme Gigabit Ethernet Controller MAC Address 1: 40:f2:e9:af:45:a0`
|
||||
`r1: Broadcom NetXtreme Gigabit Ethernet Controller MAC Address 2: 40:f2:e9:af:45:a1`
|
||||
`r1: Broadcom NetXtreme Gigabit Ethernet Controller MAC Address 3: 40:f2:e9:af:45:a2`
|
||||
`r1: Broadcom NetXtreme Gigabit Ethernet Controller MAC Address 4: 40:f2:e9:af:45:a3`
|
||||
`r1: Broadcom NetXtreme Gigabit Ethernet Controller PCI slot: 1b:00`
|
||||
`r1: Broadcom NetXtreme Gigabit Ethernet Controller location: Onboard`
|
||||
|
||||
* Reporting just the mac addresses of a node named r2:
|
||||
`# nodeinventory r2 mac`
|
||||
`r2: System MAC Address 1: 40:f2:e9:b9:10:18`
|
||||
`r2: System MAC Address 2: 40:f2:e9:b9:10:19`
|
||||
`r2: System MAC Address 3: 40:f2:e9:b9:10:1a`
|
||||
`r2: System MAC Address 4: 40:f2:e9:b9:10:1b`
|
||||
|
||||
* Getting the model number and serial number of a node named s2:
|
||||
`# nodeinventory stark2 serial model`
|
||||
`s2: System Product name: LENOVO THINKSYSTEM SD530 SERVER`
|
||||
`s2: System Serial Number: DEV0000002`
|
||||
`s2: System Model: 7X2104Z028`
|
||||
@@ -1,10 +1,10 @@
|
||||
nodelist(1) -- List confluent nodes and their attributes
|
||||
nodelist(8) -- List confluent nodes and their attributes
|
||||
=========================================================
|
||||
|
||||
## SYNOPSIS
|
||||
|
||||
`nodelist` `noderange`
|
||||
`nodelist` `noderange` [-b] [<nodeattribute>...]
|
||||
`nodelist <noderange>`
|
||||
`nodelist <noderange> [-b] <nodeattribute>...`
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
@@ -45,4 +45,3 @@ information on node attributes in nodeattributes(5) man page.
|
||||
`n2: hardwaremanagement.method: ipmi (inherited from group everything)`
|
||||
`n2: hardwaremanagement.switch: r8e1`
|
||||
`n2: hardwaremanagement.switchport: 2`
|
||||
|
||||
|
||||
@@ -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)`
|
||||
@@ -0,0 +1,48 @@
|
||||
nodepower(8) -- Check or change power state of confluent nodes
|
||||
=========================================================
|
||||
|
||||
## SYNOPSIS
|
||||
|
||||
`nodepower <noderange>`
|
||||
`nodepower <noderange> [-p] [on|off|boot|shutdown|reset|status]`
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
**nodepower** with only a noderange will retrieve current power state of nodes
|
||||
through confluent. When given an additional argument, it will request a change
|
||||
to the power state of the nodes. The following arguments are recognized:
|
||||
|
||||
* `on`: Turn on the specified noderange. Nothing will happen to nodes of
|
||||
the noderange that are already on.
|
||||
* `off`: Immediately turn off the specified noderange, without waiting for OS
|
||||
to shutdown. Nothing will happen to nodes of the noderange that are already on.
|
||||
* `boot`: Immediately boot a system. This will power on nodes of the noderange
|
||||
that are off, and reset nodes of the noderange that are on. The previous state
|
||||
will be reflected in the output.
|
||||
* `shutdown`: Request the OS gracefully shut down. Nothing will happen for
|
||||
nodes that are off, and nodes will not shutdown if the OS fails to gracefully
|
||||
respond.
|
||||
* `reset`: Request immediate reset of nodes of the noderange. Nodes that are
|
||||
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`
|
||||
`n1: on`
|
||||
`n2: on`
|
||||
`n3: on`
|
||||
`n4: off`
|
||||
|
||||
|
||||
* Forcing a reboot of nodes n1-n4:
|
||||
`# nodepower n1-n4 boot`
|
||||
`n3: on->reset`
|
||||
`n1: on->reset`
|
||||
`n2: on->reset`
|
||||
`n4: off->on`
|
||||
@@ -0,0 +1,57 @@
|
||||
noderange(5) -- Indicate a target set of nodes in confluent
|
||||
=========================================================
|
||||
|
||||
## SYNOPSIS
|
||||
|
||||
`<noderange>`
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
<noderange> is a syntax that can be used in most Confluent commands to conveniently specify a list of nodes. The result is that the command will be applied to a range of nodes, often in parallel.
|
||||
|
||||
## EXAMPLES:
|
||||
|
||||
The simplest noderange is a single node name:
|
||||
`n1`
|
||||
|
||||
Nodes and groups may be used interchangeably. The following may be used in any context where n1 would be accepted as a noderange:
|
||||
`rack1`
|
||||
|
||||
Commonly, there is a desire to target a range of elements. There are a few identically behaving syntaxes for the purpose.
|
||||
`n1:n20`
|
||||
`n1-n20`
|
||||
`n[1-20]`
|
||||
|
||||
Note that numbers may be zero padded or not, it will automatically detect the padding amount and adjust members of the range accordingly. Ranges also can understand multiple numeric values changing:
|
||||
`r[1-3]u[01-10]`
|
||||
`r1u01-r3u10`
|
||||
|
||||
Ranges can also be applied to group names, and all above syntaxes are compatible:
|
||||
`rack1-rack10`
|
||||
`rack[1-10]`
|
||||
|
||||
Also, regular expressions may be used to indicate nodes with names matching certain patterns:
|
||||
`~r1u..`
|
||||
|
||||
The other major noderange primitive is indicating nodes by some attribute value:
|
||||
`location.rack=7`
|
||||
|
||||
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:
|
||||
`rack1,-n2`
|
||||
`compute,-rack1`
|
||||
`compute,-location.row=12`
|
||||
|
||||
To indicate nodes that match multiple selections at once (set intersection), the @ symbol may be used:
|
||||
`compute@rack1`
|
||||
`location.rack=10@compute`
|
||||
|
||||
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:
|
||||
`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.
|
||||
@@ -0,0 +1,25 @@
|
||||
noderemove(8) -- Remove nodes from the confluent management service
|
||||
===================================================================
|
||||
|
||||
## SYNOPSIS
|
||||
|
||||
`noderemove <noderange>`
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
`noderemove` simply removes the given noderange from the confluent database.
|
||||
|
||||
|
||||
## EXAMPLES
|
||||
|
||||
* Remove two racks each with 4 nodes:
|
||||
`# noderemove r1u1-r2u4`
|
||||
`r1u4: deleted`
|
||||
`r1u1: deleted`
|
||||
`r1u2: deleted`
|
||||
`r1u3: deleted`
|
||||
`r2u4: deleted`
|
||||
`r2u3: deleted`
|
||||
`r2u2: deleted`
|
||||
`r2u1: deleted`
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
nodereseat(8) -- Request a reseat of a node
|
||||
============================================
|
||||
|
||||
## SYNOPSIS
|
||||
|
||||
`nodereseat <noderange>`
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
`nodereseat` requests the enclosure manager of the current node to reseat that
|
||||
node's slot. This should be equivalent to removing the system entirely from
|
||||
the chassis and putting it back in, but without actually having to do so.
|
||||
|
||||
## EXAMPLES
|
||||
|
||||
* Reseating the node `s1`:
|
||||
`# nodereseat s1`
|
||||
`s1: Reseat successful`
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
noderun(8) -- Run arbitrary commands per node in a noderange
|
||||
=============================================================
|
||||
|
||||
## SYNOPSIS
|
||||
|
||||
`noderun <noderange> <command string>`
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
`noderun` will take a given command and execute it in parallel once per node
|
||||
in the specified noderange. Attribute expressions as documented in
|
||||
nodeattribexpressions(5) are expanded prior to execution of the command. For
|
||||
noderun, the commands are locally executed. To execute commands on the nodes
|
||||
themselves, see nodeshell(8).
|
||||
|
||||
## EXAMPLES
|
||||
|
||||
* Run ping against nodes n1 through n4:
|
||||
`# noderun n1-n4 ping -c 1 {nodename}`
|
||||
`n3: PING n3 (172.30.2.3) 56(84) bytes of data.`
|
||||
`n3: 64 bytes from n3 (172.30.2.3): icmp_seq=1 ttl=64 time=0.387 ms`
|
||||
`n3: `
|
||||
`n3: --- n3 ping statistics ---`
|
||||
`n3: 1 packets transmitted, 1 received, 0% packet loss, time 0ms`
|
||||
`n3: rtt min/avg/max/mdev = 0.387/0.387/0.387/0.000 ms`
|
||||
`n4: PING n4 (172.30.2.4) 56(84) bytes of data.`
|
||||
`n4: 64 bytes from n4 (172.30.2.4): icmp_seq=1 ttl=64 time=0.325 ms`
|
||||
`n4: `
|
||||
`n4: --- n4 ping statistics ---`
|
||||
`n4: 1 packets transmitted, 1 received, 0% packet loss, time 0ms`
|
||||
`n4: rtt min/avg/max/mdev = 0.325/0.325/0.325/0.000 ms`
|
||||
`n2: PING n2 (172.30.2.2) 56(84) bytes of data.`
|
||||
`n2: From odin (172.30.0.6) icmp_seq=1 Destination Host Unreachable`
|
||||
`n2: `
|
||||
`n2: --- n2 ping statistics ---`
|
||||
`n2: 1 packets transmitted, 0 received, +1 errors, 100% packet loss, time 3000ms`
|
||||
`n2: `
|
||||
`n1: PING n1 (172.30.2.1) 56(84) bytes of data.`
|
||||
`n1: `
|
||||
`n1: --- n1 ping statistics ---`
|
||||
`n1: 1 packets transmitted, 0 received, 100% packet loss, time 10000ms`
|
||||
`n1: `
|
||||
|
||||
* Run an ipmitool raw command against the management controllers of n1 through n4:
|
||||
`# noderun n1-n4 ipmitool -I lanplus -U USERID -E -H {hardwaremanagement.manager} raw 0 1`
|
||||
`n3: 01 10 00`
|
||||
`n1: 01 10 00`
|
||||
`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)
|
||||
@@ -1,9 +1,9 @@
|
||||
nodesensors(1) --- Retrieve telemetry for sensors of confluent nodes
|
||||
nodesensors(8) --- Retrieve telemetry for sensors of confluent nodes
|
||||
====================================================================
|
||||
|
||||
## SYNOPSIS
|
||||
|
||||
`nodesensors` `noderange` [-c] [-i <interval>] [-n <samplecount>] [<sensor name or category>...]
|
||||
`nodesensors <noderange> [-c] [-i <interval>] [-n <samplecount>] [<sensor name or category>...]`
|
||||
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
nodesetboot(8) -- Check or set next boot device for noderange
|
||||
====================================================
|
||||
|
||||
## SYNOPSIS
|
||||
|
||||
`nodesetboot <noderange>`
|
||||
`nodesetboot [options] <noderange> [default|cd|network|setup|hd]`
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
Requests that the next boot occur from the specified device. Unless otherwise
|
||||
specified, this is a one time boot option, and does not change the normal boot
|
||||
behavior of the system. This is useful for taking a system that normally boots
|
||||
to the hard drive and startking a network install, or to go into the firmware
|
||||
setup menu without having to hit a keystroke at the correct time on the console.
|
||||
|
||||
Generally, it's a bit more convenient and direct to use the nodeboot(8) command,
|
||||
which will follow up the boot device with an immediate power directive to take
|
||||
effect. The `nodesetboot` command is still useful, particularly if you want
|
||||
to use `nodesetboot <noderange> setup` and then initiate a reboot from within
|
||||
the operating system with ssh or similar rather than using the remote hardware
|
||||
control.
|
||||
|
||||
## 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).
|
||||
|
||||
* `-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
|
||||
|
||||
* `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
|
||||
|
||||
* Set next boot to setup for four nodes:
|
||||
`# nodesetboot n1-n4 setup`
|
||||
`n1: setup`
|
||||
`n3: setup`
|
||||
`n2: setup`
|
||||
`n4: setup`
|
||||
|
||||
* Check boot override settings on four nodes:
|
||||
`# nodesetboot n1-n4`
|
||||
`n1: setup`
|
||||
`n2: setup`
|
||||
`n3: setup`
|
||||
`n4: setup`
|
||||
|
||||
## SEE ALSO
|
||||
|
||||
nodeboot(8)
|
||||
@@ -0,0 +1,37 @@
|
||||
nodeshell(8) -- Execute command on many nodes in a noderange through ssh
|
||||
=========================================================================
|
||||
|
||||
## SYNOPSIS
|
||||
|
||||
`nodeshell <noderange> [options] <command to execute on each node>`
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
Allows execution of a command on many nodes in parallel. Like noderun(8), it
|
||||
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:
|
||||
`# nodeshell n1-n4 echo hi`
|
||||
`n1: hi`
|
||||
`n2: hi`
|
||||
`n3: hi`
|
||||
`n4: hi`
|
||||
|
||||
* Setting a new static ip address temporarily on secondary interface of four nodes:
|
||||
`# 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
|
||||
|
||||
noderun(8)
|
||||
@@ -0,0 +1,25 @@
|
||||
nodesupport(8) -- Utilities for interacting with vendor support
|
||||
=================================================================
|
||||
|
||||
## SYNOPSIS
|
||||
|
||||
`nodesupport <noderange> servicedata <directory or filename>`
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
`nodesupport` provides capabilities associated with interactiong 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)
|
||||
|
||||
## 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`
|
||||
Executable
+10
@@ -0,0 +1,10 @@
|
||||
#!/bin/sh
|
||||
cd `dirname $0`/doc/man
|
||||
mkdir -p ../../man/man1
|
||||
mkdir -p ../../man/man5
|
||||
mkdir -p ../../man/man8
|
||||
ronn -r *.ronn
|
||||
mv *.1 ../../man/man1/
|
||||
mv *.5 ../../man/man5/
|
||||
mv *.8 ../../man/man8/
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
# 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))
|
||||
@@ -1,4 +1,14 @@
|
||||
from setuptools import setup
|
||||
import os
|
||||
|
||||
data_files = [('/etc/profile.d', ['confluent_env.sh', 'confluent_env.csh'])]
|
||||
try:
|
||||
scriptlist = ['bin/{0}'.format(d) for d in os.listdir('bin/')]
|
||||
data_files.append(('/opt/confluent/share/man/man1', ['man/man1/' + x for x in os.listdir('man/man1')]))
|
||||
data_files.append(('/opt/confluent/share/man/man5', ['man/man5/' + x for x in os.listdir('man/man5')]))
|
||||
data_files.append(('/opt/confluent/share/man/man8', ['man/man8/' + x for x in os.listdir('man/man8')]))
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
setup(
|
||||
name='confluent_client',
|
||||
@@ -7,9 +17,6 @@ setup(
|
||||
author_email='jjohnson2@lenovo.com',
|
||||
url='http://xcat.sf.net/',
|
||||
packages=['confluent'],
|
||||
scripts=['bin/confetty', 'bin/nodeconsole', 'bin/nodeeventlog',
|
||||
'bin/nodefirmware', 'bin/nodehealth', 'bin/nodeidentify',
|
||||
'bin/nodeinventory', 'bin/nodelist', 'bin/nodepower',
|
||||
'bin/nodesensors', 'bin/nodesetboot', 'bin/noderun'],
|
||||
data_files=[('/etc/profile.d', ['confluent_env.sh'])],
|
||||
scripts=scriptlist,
|
||||
data_files=data_files,
|
||||
)
|
||||
|
||||
@@ -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,2 +1,3 @@
|
||||
include sysvinit/*
|
||||
include systemd/*
|
||||
include sysctl/*
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
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(0077)
|
||||
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 = raw_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))))
|
||||
|
||||
|
||||
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')
|
||||
ic.add_argument('name', help='Name of server to invite to join the '
|
||||
'collective')
|
||||
jc = sp.add_parser('join', help='Join a collective')
|
||||
jc.add_argument('server', help='A server currently in the collective')
|
||||
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()
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Executable
+93
@@ -0,0 +1,93 @@
|
||||
#!/usr/bin/env python
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 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.
|
||||
|
||||
|
||||
import getpass
|
||||
import optparse
|
||||
import sys
|
||||
import os
|
||||
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.config.configmanager as cfm
|
||||
import confluent.config.conf as conf
|
||||
import confluent.main as main
|
||||
|
||||
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',
|
||||
help='Specify that no password should be used to protect'
|
||||
' 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()
|
||||
sys.exit(1)
|
||||
dumpdir = args[1]
|
||||
|
||||
|
||||
if args[0] == 'restore':
|
||||
pid = main.is_running()
|
||||
if pid is not None:
|
||||
print("Confluent is running, must shut down to restore db")
|
||||
sys.exit(1)
|
||||
try:
|
||||
cfm.restore_db_from_directory(dumpdir, options.password)
|
||||
except Exception as e:
|
||||
print(str(e))
|
||||
sys.exit(1)
|
||||
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(077)
|
||||
main._initsecurity(conf.get_config())
|
||||
if not os.path.exists(dumpdir):
|
||||
os.makedirs(dumpdir)
|
||||
cfm.dump_db_to_directory(dumpdir, options.password, options.redact,
|
||||
options.skipkeys)
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
@@ -1,5 +1,8 @@
|
||||
#!/bin/sh
|
||||
cd `dirname $0`
|
||||
if [ -x ./makeman ]; then
|
||||
./makeman
|
||||
fi
|
||||
./makesetup
|
||||
VERSION=`cat VERSION`
|
||||
PKGNAME=$(basename $(pwd))
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
import confluent.config.configmanager as configmanager
|
||||
import eventlet
|
||||
import eventlet.tpool
|
||||
import Crypto.Protocol.KDF as KDF
|
||||
import Cryptodome.Protocol.KDF as KDF
|
||||
import hashlib
|
||||
import hmac
|
||||
import multiprocessing
|
||||
@@ -37,6 +37,7 @@ _passcache = {}
|
||||
_passchecking = {}
|
||||
|
||||
authworkers = None
|
||||
authcleaner = None
|
||||
|
||||
|
||||
class Credentials(object):
|
||||
@@ -195,6 +196,13 @@ def check_user_passphrase(name, passphrase, element=None, tenant=False):
|
||||
#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
|
||||
@@ -211,19 +219,16 @@ def _apply_pbkdf(passphrase, salt):
|
||||
lambda p, s: hmac.new(p, s, hashlib.sha256).digest())
|
||||
|
||||
|
||||
def _clean_authworkers():
|
||||
global authworkers
|
||||
global authcleaner
|
||||
authworkers = None
|
||||
authcleaner = None
|
||||
|
||||
|
||||
def _do_pbkdf(passphrase, salt):
|
||||
# we must get it over to the authworkers pool or else get blocked in
|
||||
# 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])
|
||||
|
||||
|
||||
def init_auth():
|
||||
# have a some auth workers available. Keep them distinct from
|
||||
# the general populace of workers to avoid unauthorized users
|
||||
# starving out productive work
|
||||
global authworkers
|
||||
# for now we'll just have one auth worker and see if there is any
|
||||
# demand for more. I personally doubt it.
|
||||
authworkers = multiprocessing.Pool(processes=1)
|
||||
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,590 @@
|
||||
# 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
|
||||
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:
|
||||
tlvdata.recv(remote) # the banner
|
||||
tlvdata.recv(remote) # authpassed... 0..
|
||||
if name is None:
|
||||
name = get_myname()
|
||||
tlvdata.send(remote, {'collective': {'operation': 'connect',
|
||||
'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 = ''
|
||||
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)
|
||||
return True
|
||||
|
||||
|
||||
def follow_leader(remote):
|
||||
global currentleader
|
||||
cleanexit = False
|
||||
try:
|
||||
cfm.follow_channel(remote)
|
||||
except greenlet.GreenletExit:
|
||||
cleanexit = True
|
||||
finally:
|
||||
if cleanexit:
|
||||
log.log({'info': 'Previous following cleanly closed',
|
||||
'subsystem': 'collective'})
|
||||
return
|
||||
log.log({'info': 'Current leader has disappeared, restarting '
|
||||
'collective membership', '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 'show' == operation:
|
||||
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
|
||||
tlvdata.send(connection, {'collective': collinfo})
|
||||
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('@', 1)
|
||||
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 follower.isAlive()):
|
||||
# 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']
|
||||
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):
|
||||
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)
|
||||
|
||||
|
||||
@@ -16,50 +16,69 @@
|
||||
# limitations under the License.
|
||||
|
||||
|
||||
#This defines the attributes of variou classes of things
|
||||
#This defines the attributes of various classes of things
|
||||
|
||||
# 'nic', meant to be a nested structure under node
|
||||
nic = {
|
||||
'name': {
|
||||
'description': 'Name in ip/ifconfig as desired by administrator',
|
||||
},
|
||||
'port': {
|
||||
'description': 'Port that this nic connects to',
|
||||
},
|
||||
'switch': {
|
||||
'description': 'Switch that this nic connects to',
|
||||
},
|
||||
'customhardwareaddress': {
|
||||
'description': 'Mac address to push to nic',
|
||||
},
|
||||
'dnssuffix': {
|
||||
'description': ('String to place after nodename, but before'
|
||||
'Network.Domain to derive FQDN for this NIC'),
|
||||
},
|
||||
'hardwareaddress': {
|
||||
'description': 'Active mac address on this nic (factory or custom)'
|
||||
},
|
||||
'ipaddresses': {
|
||||
'description': 'Set of IPv4 and IPv6 addresses in CIDR format'
|
||||
},
|
||||
'pvid': {
|
||||
'description': 'PVID of port on switch this nic connects to',
|
||||
},
|
||||
'mtu': {
|
||||
'description': 'Requested MTU to configure on this interface',
|
||||
},
|
||||
'vlans': {
|
||||
'description': 'Tagged VLANs to apply to nic/switch',
|
||||
},
|
||||
'dhcpv4enabled': {
|
||||
'description': ('Whether DHCP should be attempted to acquire IPv4'
|
||||
'address on this interface'),
|
||||
},
|
||||
'dhcpv6enabled': {
|
||||
'description': ('Whether DHCP should be attempted to acquire IPv6'
|
||||
'address on this interface'),
|
||||
},
|
||||
}
|
||||
# changing mind on design, flattening to a single attribute, a *touch* less
|
||||
# flexible at the top end, but much easier on the low end
|
||||
# now net.<name>.attribute scheme
|
||||
# similarly, leaning toward comma delimited ip addresses, since 99.99% of the
|
||||
# time each nic will have one ip address
|
||||
# vlan specification will need to be thought about a tad, each ip could be on
|
||||
# a distinct vlan, but could have a vlan without an ip for sake of putting
|
||||
# to a bridge. Current thought is
|
||||
# vlans attribute would be comma delimited referring to the same index
|
||||
# as addresses, with either 'native' or a number for vlan id
|
||||
# the 'joinbridge' attribute would have some syntax like @<vlanid> to indicate
|
||||
# joining only a vlan of the nic to the bridge
|
||||
# 'joinbond' attribute would not support vlans.
|
||||
|
||||
#nic = {
|
||||
# 'name': {
|
||||
# 'description': 'Name in ip/ifconfig as desired by administrator',
|
||||
# },
|
||||
# 'biosdevname': {
|
||||
# 'description': '"biosdevname" scheme to identify the adapter. If not'
|
||||
# 'mac address match is preferred, then biosdevname, then'
|
||||
# 'name.',
|
||||
# },
|
||||
# 'port': {
|
||||
# 'description': 'Port that this nic connects to',
|
||||
# },
|
||||
# 'switch': {
|
||||
# 'description': 'Switch that this nic connects to',
|
||||
# },
|
||||
# 'customhardwareaddress': {
|
||||
# 'description': 'Mac address to push to nic',
|
||||
# },
|
||||
# 'dnssuffix': {
|
||||
# 'description': ('String to place after nodename, but before'
|
||||
# 'Network.Domain to derive FQDN for this NIC'),
|
||||
# },
|
||||
# 'hardwareaddress': {
|
||||
# 'description': 'Active mac address on this nic (factory or custom)'
|
||||
# },
|
||||
# 'ipaddresses': {
|
||||
# 'description': 'Set of IPv4 and IPv6 addresses in CIDR format'
|
||||
# },
|
||||
# 'pvid': {
|
||||
# 'description': 'PVID of port on switch this nic connects to',
|
||||
# },
|
||||
# 'mtu': {
|
||||
# 'description': 'Requested MTU to configure on this interface',
|
||||
# },
|
||||
# 'vlans': {
|
||||
# 'description': 'Tagged VLANs to apply to nic/switch',
|
||||
# },
|
||||
# 'dhcpv4enabled': {
|
||||
# 'description': ('Whether DHCP should be attempted to acquire IPv4'
|
||||
# 'address on this interface'),
|
||||
# },
|
||||
# 'dhcpv6enabled': {
|
||||
# 'description': ('Whether DHCP should be attempted to acquire IPv6'
|
||||
# 'address on this interface'),
|
||||
# },
|
||||
#}
|
||||
|
||||
user = {
|
||||
'password': {
|
||||
@@ -71,7 +90,6 @@ user = {
|
||||
node = {
|
||||
'groups': {
|
||||
'type': list,
|
||||
'default': 'all',
|
||||
'description': ('List of static groups for which this node is '
|
||||
'considered a member'),
|
||||
},
|
||||
@@ -81,6 +99,86 @@ node = {
|
||||
#'id': {
|
||||
# 'description': ('Numeric identifier for node')
|
||||
#},
|
||||
# autonode is the feature of generating nodes based on connectivity to
|
||||
# current node. In recursive autonode, for now we just allow endpoint to
|
||||
# either be a server directly *or* a server enclosure. This precludes
|
||||
# for the moment a concept of nested arbitrarily deep, but for now do this.
|
||||
# hypothetically, one could imagine supporting an array and 'popping'
|
||||
# names until reaching end. Not worth implementing at this point. If
|
||||
# a traditional switch is added, it needs some care and feeding anyway.
|
||||
# If a more exciting scheme presents itself, well we won't have to
|
||||
# # own discovering switches anyway.
|
||||
# 'autonode.servername': {
|
||||
# 'description': ('Template for creating nodenames for automatic '
|
||||
# 'creation of nodes detected as children of '
|
||||
# 'this node. For example, a node in a server '
|
||||
# 'enclosure bay or a server connected to a switch or '
|
||||
# 'an enclosure manager connected to a switch. Certain '
|
||||
# 'special template parameters are available and can '
|
||||
# 'be used alongside usual config template directives. '
|
||||
# '"discovered.nodenumber" will be replaced with the '
|
||||
# 'bay or port number where the child node is connected.'
|
||||
# ),
|
||||
# },
|
||||
# 'autonode.servergroups': {
|
||||
# 'type': list,
|
||||
# 'description': ('A list of groups to which discovered nodes will '
|
||||
# 'belong to. As in autonode.servername, "discovered." '
|
||||
# 'variable names will be substituted in special context')
|
||||
# },
|
||||
# 'autonode.enclosurename': {
|
||||
# 'description': ('Template for creating nodenames when the discovered '
|
||||
# 'node is an enclosure that will in turn generate nodes.'
|
||||
# )
|
||||
# },
|
||||
# 'autonode.enclosuregroups': {
|
||||
# 'type': list,
|
||||
# 'description': ('A list of groups to which a discovered node will be'
|
||||
# 'placed, presuming that node is an enclosure.')
|
||||
# },
|
||||
#For now, we consider this eventuality if needed. For now emphasize paradigm
|
||||
# of group membership and see how far that goes.
|
||||
# 'autonode.copyattribs': {
|
||||
# 'type': list,
|
||||
# 'description': ('A list of attributes to copy from the node generator '
|
||||
# 'to the generated node. Expressions will be copied '
|
||||
# 'over without evaluation, so will be evaluated '
|
||||
# 'in the context of the generated node, rather than the'
|
||||
# 'parent node. By default, an enclosure will copy over'
|
||||
# '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" '
|
||||
'would disable password expiration and login failures '
|
||||
'triggering a lockout.'
|
||||
},
|
||||
'discovery.policy': {
|
||||
'description': 'Policy to use for auto-configuration of discovered '
|
||||
'and identified nodes. Valid values are "manual", '
|
||||
'"permissive", or "open". "manual" means nodes are '
|
||||
'detected, but not autoconfigured until a user '
|
||||
'approves. "permissive" indicates to allow discovery, '
|
||||
'so long as the node has no existing public key. '
|
||||
'"open" allows discovery even if a known public key '
|
||||
'is already stored',
|
||||
},
|
||||
'info.note': {
|
||||
'description': 'A field used for administrators to make arbitrary '
|
||||
'notations about nodes. This is meant entirely for '
|
||||
'human use and not programmatic use, so it can be '
|
||||
'freeform text data without concern for issues in how '
|
||||
'the server will process it.',
|
||||
},
|
||||
'location.room': {
|
||||
'description': 'Room description for the node',
|
||||
},
|
||||
@@ -195,23 +293,17 @@ node = {
|
||||
'description': 'The method used to perform operations such as power '
|
||||
'control, get sensor data, get inventory, and so on. '
|
||||
},
|
||||
'hardwaremanagement.switch': {
|
||||
'description': 'The switch to which the hardware manager is connected.'
|
||||
' Only relevant if using switch based discovery via the'
|
||||
' hardware manager (Lenovo IMMs and CMMs). Not '
|
||||
'applicable to Lenovo Flex nodes.'
|
||||
'enclosure.bay': {
|
||||
'description': 'The bay in the enclosure, if any',
|
||||
# 'appliesto': ['system'],
|
||||
},
|
||||
'hardwaremanagement.switchport': {
|
||||
'description': 'The port of the switch that the hardware manager is '
|
||||
'connected. See documentation of '
|
||||
'hardwaremanagement.switch for more detail.'
|
||||
'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'],
|
||||
},
|
||||
'enclosure.bay': {
|
||||
'description': 'The bay in the enclosure, if any',
|
||||
# 'appliesto': ['system'],
|
||||
},
|
||||
|
||||
@@ -220,12 +312,57 @@ node = {
|
||||
#IBM Flex)''',
|
||||
# 'appliesto': ['system'],
|
||||
# },
|
||||
# 'id.serial': {
|
||||
# 'description': 'The manufacturer serial number of node',
|
||||
# },
|
||||
# 'id.uuid': {
|
||||
# 'description': 'The UUID of the node as presented in DMI',
|
||||
# },
|
||||
'id.model': {
|
||||
'description': 'The model number of a node. In scenarios where there '
|
||||
'is both a name and a model number, it is generally '
|
||||
'expected that this would be the generally more '
|
||||
'specific model number.'
|
||||
},
|
||||
'id.serial': {
|
||||
'description': 'The manufacturer serial number of node',
|
||||
},
|
||||
'id.uuid': {
|
||||
'description': 'The UUID of the node as presented in DMI.',
|
||||
},
|
||||
# For single interface mac collection, net.bootable suffices
|
||||
# For multiple interface mac collection, we perhaps add an address field and use subnet affinity as a clue
|
||||
# to disambiguate multiple addresses for external provisioning
|
||||
# For internal provisioning, the UUID matters rather than the MAC, though subnet affinity may
|
||||
# still be a factor to select the appropriate static config to send down. In such a case bonding
|
||||
# is hopefully more likely as that's a bit easier.
|
||||
# Start with the first case and only document that, the other thoughts can be future items if they turn up
|
||||
'net.bootable': {
|
||||
'type': bool,
|
||||
'description': 'Whether or not the indicated network interface is to be used for booting. This is used by '
|
||||
'the discovery process to decide where to place the mac address of a detected PXE nic.',
|
||||
},
|
||||
'net.ipv4_gateway': {
|
||||
'description': 'The IPv4 gateway to use if applicable. As is the '
|
||||
'case for other net attributes, net.eth0.ipv4_gateway '
|
||||
'and similar is accepted.'
|
||||
},
|
||||
'net.hwaddr': {
|
||||
'description': 'The hardware address, aka MAC address of the interface indicated, generally populated by the '
|
||||
'PXE discovery mechanism'
|
||||
},
|
||||
# 'net.pxe': { 'description': 'Whether pxe will be used on this interface'
|
||||
# TODO(jjohnson2): Above being 'true' will control whether mac addresses
|
||||
# are stored in this nics attribute on pxe-client discovery, since
|
||||
# pxe discovery is ambiguous for BMC and system on same subnet,
|
||||
# or even both on the same port and same subnet
|
||||
'net.switch': {
|
||||
'description': 'An ethernet switch the node is connected to. Note '
|
||||
'that net.* attributes may be indexed by interface. '
|
||||
'For example instead of using net.switch, it is '
|
||||
'possible to use net.eth0.switch and net.eth1.switch '
|
||||
'or net.0.switch and net.1.switch to define multiple '
|
||||
'sets of net connectivity associated with each other.'
|
||||
},
|
||||
'net.switchport': {
|
||||
'description': 'The port on the switch that corresponds to this node. '
|
||||
'See information on net.switch for more on the '
|
||||
'flexibility of net.* attributes.'
|
||||
},
|
||||
# 'id.modelnumber': {
|
||||
# 'description': 'The manufacturer dictated model number for the node',
|
||||
# },
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,7 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2014 IBM Corporation
|
||||
# Copyright 2017 Lenovo
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -20,15 +21,21 @@
|
||||
|
||||
# we track nodes that are actively being logged, watched, or have attached
|
||||
# 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
|
||||
import traceback
|
||||
@@ -37,28 +44,127 @@ _handled_consoles = {}
|
||||
|
||||
_tracelog = None
|
||||
|
||||
try:
|
||||
range = xrange
|
||||
except NameError:
|
||||
pass
|
||||
|
||||
pytecolors2ansi = {
|
||||
'black': 0,
|
||||
'red': 1,
|
||||
'green': 2,
|
||||
'brown': 3,
|
||||
'blue': 4,
|
||||
'magenta': 5,
|
||||
'cyan': 6,
|
||||
'white': 7,
|
||||
'default': 9,
|
||||
}
|
||||
# might be able to use IBMPC map from pyte charsets,
|
||||
# in that case, would have to mask out certain things (like ESC)
|
||||
# in the same way that Screen's draw method would do
|
||||
# for now at least get some of the arrows in there (note ESC is one
|
||||
# of those arrows... so skip it...
|
||||
ansichars = dict(zip((0x18, 0x19), u'\u2191\u2193'))
|
||||
|
||||
|
||||
def _utf8_normalize(data, shiftin, decoder):
|
||||
# first we give the stateful decoder a crack at the byte stream,
|
||||
# we may come up empty in the event of a partial multibyte
|
||||
try:
|
||||
data = decoder.decode(data)
|
||||
except UnicodeDecodeError:
|
||||
# first order of business is to reset the state of
|
||||
# the decoder to a clean one, so we can switch back to utf-8
|
||||
# when things change, for example going from an F1 setup menu stuck
|
||||
# in the old days to a modern platform using utf-8
|
||||
decoder.setstate(codecs.getincrementaldecoder('utf-8')().getstate())
|
||||
# Ok, so we have something that is not valid UTF-8,
|
||||
# our next stop is to try CP437. We don't try incremental
|
||||
# decode, since cp437 is single byte
|
||||
# replace is silly here, since there does not exist invalid c437,
|
||||
# but just in case
|
||||
data = data.decode('cp437', 'replace')
|
||||
# Finally, the low part of ascii is valid utf-8, but we are going to be
|
||||
# more interested in the cp437 versions (since this is console *output*
|
||||
# not input
|
||||
if shiftin is None:
|
||||
data = data.translate(ansichars)
|
||||
return data.encode('utf-8')
|
||||
|
||||
|
||||
def pytechars2line(chars, maxlen=None):
|
||||
line = '\x1b[m' # start at default params
|
||||
lb = False # last bold
|
||||
li = False # last italic
|
||||
lu = False # last underline
|
||||
ls = False # last strikethrough
|
||||
lr = False # last reverse
|
||||
lfg = 'default' # last fg color
|
||||
lbg = 'default' # last bg color
|
||||
hasdata = False
|
||||
len = 1
|
||||
for charidx in range(maxlen):
|
||||
char = chars[charidx]
|
||||
csi = []
|
||||
if char.fg != lfg:
|
||||
csi.append(30 + pytecolors2ansi[char.fg])
|
||||
lfg = char.fg
|
||||
if char.bg != lbg:
|
||||
csi.append(40 + pytecolors2ansi[char.bg])
|
||||
lbg = char.bg
|
||||
if char.bold != lb:
|
||||
lb = char.bold
|
||||
csi.append(1 if lb else 22)
|
||||
if char.italics != li:
|
||||
li = char.italics
|
||||
csi.append(3 if li else 23)
|
||||
if char.underscore != lu:
|
||||
lu = char.underscore
|
||||
csi.append(4 if lu else 24)
|
||||
if char.strikethrough != ls:
|
||||
ls = char.strikethrough
|
||||
csi.append(9 if ls else 29)
|
||||
if char.reverse != lr:
|
||||
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():
|
||||
hasdata = True
|
||||
line += char.data.encode('utf-8')
|
||||
if maxlen and len >= maxlen:
|
||||
break
|
||||
len += 1
|
||||
return line, hasdata
|
||||
|
||||
|
||||
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):
|
||||
self.clearpending = False
|
||||
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'
|
||||
self._isalive = True
|
||||
self.buffer = bytearray()
|
||||
self.buffer = pyte.Screen(100, 31)
|
||||
self.termstream = pyte.ByteStream()
|
||||
self.termstream.attach(self.buffer)
|
||||
self.livesessions = set([])
|
||||
self.utf8decoder = codecs.getincrementaldecoder('utf-8')()
|
||||
if self._logtobuffer:
|
||||
self.logger = log.Logger(node, console=True,
|
||||
tenant=configmanager.tenant)
|
||||
(text, termstate, timestamp) = self.logger.read_recent_text(8192)
|
||||
else:
|
||||
(text, termstate, timestamp) = ('', 0, False)
|
||||
(text, termstate, timestamp) = (b'', 0, False)
|
||||
# when reading from log file, we will use wall clock
|
||||
# it should usually match walltime.
|
||||
self.lasttime = 0
|
||||
@@ -70,7 +176,7 @@ class ConsoleHandler(object):
|
||||
# wall clock has gone backwards, use current time as best
|
||||
# guess
|
||||
self.lasttime = util.monotonic_time()
|
||||
self.buffer += text
|
||||
self.clearbuffer()
|
||||
self.appmodedetected = False
|
||||
self.shiftin = None
|
||||
self.reconnect = None
|
||||
@@ -91,19 +197,57 @@ 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):
|
||||
try:
|
||||
self.termstream.feed(data)
|
||||
except StopIteration: # corrupt parser state, start over
|
||||
self.termstream = pyte.ByteStream()
|
||||
self.termstream.attach(self.buffer)
|
||||
except Exception:
|
||||
_tracelog.log(traceback.format_exc(), ltype=log.DataTypes.event,
|
||||
event=log.Events.stacktrace)
|
||||
|
||||
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]:
|
||||
self._isondemand = False
|
||||
elif (attrvalue[self.node]['console.logging']['value'] not in (
|
||||
'full', '')):
|
||||
self._isondemand = True
|
||||
elif (attrvalue[self.node]['console.logging']['value']) == 'none':
|
||||
self._dologging = False
|
||||
else:
|
||||
if (attrvalue[self.node]['console.logging']['value'] not in (
|
||||
'full', '', 'buffer')):
|
||||
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
|
||||
@@ -115,6 +259,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
|
||||
@@ -138,7 +286,7 @@ class ConsoleHandler(object):
|
||||
return
|
||||
else:
|
||||
self._ondemand()
|
||||
if logvalue == 'none':
|
||||
if logvalue in ('none', 'memory'):
|
||||
self._dologging = False
|
||||
if not self._isondemand or self.livesessions:
|
||||
eventlet.spawn(self._connect)
|
||||
@@ -157,10 +305,22 @@ class ConsoleHandler(object):
|
||||
else:
|
||||
self._console.ping()
|
||||
|
||||
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]')
|
||||
self.clearpending = True
|
||||
|
||||
def _detach(self):
|
||||
for ses in list(self.livesessions):
|
||||
ses.detach()
|
||||
|
||||
def _disconnect(self):
|
||||
if self.connectionthread:
|
||||
self.connectionthread.kill()
|
||||
self.connectionthread = None
|
||||
# clear the terminal buffer when disconnected
|
||||
self.clearbuffer()
|
||||
if self._console:
|
||||
self.log(
|
||||
logdata='console disconnected', ltype=log.DataTypes.event,
|
||||
@@ -176,6 +336,8 @@ class ConsoleHandler(object):
|
||||
self._disconnect()
|
||||
|
||||
def _connect(self):
|
||||
if not self._is_local:
|
||||
return
|
||||
if self.connectionthread:
|
||||
self.connectionthread.kill()
|
||||
self.connectionthread = None
|
||||
@@ -191,15 +353,19 @@ class ConsoleHandler(object):
|
||||
self.reconnect.cancel()
|
||||
self.reconnect = None
|
||||
try:
|
||||
self._console = plugin.handle_path(
|
||||
self._console = list(plugin.handle_path(
|
||||
self._plugin_path.format(self.node),
|
||||
"create", self.cfgmgr)
|
||||
except exc.NotImplementedException:
|
||||
"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)
|
||||
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,
|
||||
@@ -219,31 +385,34 @@ class ConsoleHandler(object):
|
||||
try:
|
||||
self._console.connect(self.get_console_output)
|
||||
except exc.TargetEndpointBadCredentials:
|
||||
self.clearbuffer()
|
||||
self.error = 'badcredentials'
|
||||
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
|
||||
except exc.TargetEndpointUnreachable:
|
||||
self.clearbuffer()
|
||||
self.error = 'unreachable'
|
||||
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
|
||||
except Exception:
|
||||
self.clearbuffer()
|
||||
_tracelog.log(traceback.format_exc(), ltype=log.DataTypes.event,
|
||||
event=log.Events.stacktrace)
|
||||
self.error = 'unknown'
|
||||
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
|
||||
@@ -251,6 +420,7 @@ 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)
|
||||
@@ -265,6 +435,8 @@ class ConsoleHandler(object):
|
||||
self._send_rcpts({'connectstate': self.connectstate})
|
||||
if self._isalive:
|
||||
self._connect()
|
||||
else:
|
||||
self.clearbuffer()
|
||||
|
||||
def close(self):
|
||||
self._isalive = False
|
||||
@@ -277,12 +449,9 @@ class ConsoleHandler(object):
|
||||
if self.connectionthread:
|
||||
self.connectionthread.kill()
|
||||
self.connectionthread = None
|
||||
|
||||
def flushbuffer(self):
|
||||
# Logging is handled in a different stream
|
||||
# this buffer is now just for having screen redraw on
|
||||
# connect
|
||||
self.buffer = bytearray(self.buffer[-8192:])
|
||||
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
|
||||
@@ -352,21 +521,21 @@ class ConsoleHandler(object):
|
||||
eventdata |= 1
|
||||
if self.shiftin is not None:
|
||||
eventdata |= 2
|
||||
self.log(data, eventdata=eventdata)
|
||||
self.lasttime = util.monotonic_time()
|
||||
if isinstance(data, bytearray) or isinstance(data, bytes):
|
||||
self.buffer += data
|
||||
else:
|
||||
self.buffer += data.encode('utf-8')
|
||||
# TODO: analyze buffer for registered events, examples:
|
||||
# panics
|
||||
# certificate signing request
|
||||
if len(self.buffer) > 16384:
|
||||
self.flushbuffer()
|
||||
self._send_rcpts(data)
|
||||
if self.clearpending:
|
||||
self.clearpending = False
|
||||
self.feedbuffer(b'\x1bc')
|
||||
self._send_rcpts(b'\x1bc')
|
||||
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 self.livesessions:
|
||||
for rcpt in list(self.livesessions):
|
||||
try:
|
||||
rcpt.data_handler(data)
|
||||
except: # No matter the reason, advance to next recipient
|
||||
@@ -385,7 +554,26 @@ class ConsoleHandler(object):
|
||||
'connectstate': self.connectstate,
|
||||
'clientcount': len(self.livesessions),
|
||||
}
|
||||
retdata = ''
|
||||
retdata = b'\x1b[H\x1b[J' # clear screen
|
||||
pendingbl = b'' # pending blank lines
|
||||
maxlen = 0
|
||||
for line in self.buffer.display:
|
||||
line = line.rstrip()
|
||||
if len(line) > maxlen:
|
||||
maxlen = len(line)
|
||||
for line in range(self.buffer.lines):
|
||||
nline, notblank = pytechars2line(self.buffer.buffer[line], maxlen)
|
||||
if notblank:
|
||||
if pendingbl:
|
||||
retdata += pendingbl
|
||||
pendingbl = b''
|
||||
retdata += nline + '\r\n'
|
||||
else:
|
||||
pendingbl += nline + '\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)
|
||||
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
|
||||
@@ -393,27 +581,16 @@ class ConsoleHandler(object):
|
||||
retdata += '\x1b[?1h'
|
||||
else:
|
||||
retdata += '\x1b[?1l'
|
||||
# an alternative would be to emulate a VT100 to know what the
|
||||
# whole screen would look like
|
||||
# this is one scheme to clear screen, move cursor then clear
|
||||
bufidx = self.buffer.rfind('\x1b[H\x1b[J')
|
||||
if bufidx >= 0:
|
||||
return retdata + str(self.buffer[bufidx:]), connstate
|
||||
# another scheme is the 2J scheme
|
||||
bufidx = self.buffer.rfind('\x1b[2J')
|
||||
if bufidx >= 0:
|
||||
# there was some sort of clear screen event
|
||||
# somewhere in the buffer, replay from that point
|
||||
# in hopes that it reproduces the screen
|
||||
return retdata + str(self.buffer[bufidx:]), connstate
|
||||
else:
|
||||
# we have no indication of last erase, play back last kibibyte
|
||||
# to give some sense of context anyway
|
||||
return retdata + str(self.buffer[-1024:]), connstate
|
||||
return retdata, connstate
|
||||
|
||||
def write(self, data):
|
||||
if self.connectstate == 'connected':
|
||||
self._console.write(data)
|
||||
try:
|
||||
self._console.write(data)
|
||||
except Exception:
|
||||
_tracelog.log(traceback.format_exc(), ltype=log.DataTypes.event,
|
||||
event=log.Events.stacktrace)
|
||||
self._got_disconnected()
|
||||
|
||||
|
||||
def disconnect_node(node, configmanager):
|
||||
@@ -431,7 +608,12 @@ def _nodechange(added, deleting, 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:
|
||||
@@ -446,12 +628,118 @@ 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):
|
||||
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)
|
||||
consk = (node, configmanager.tenant)
|
||||
if consk not in _handled_consoles:
|
||||
_handled_consoles[consk] = ConsoleHandler(node, configmanager)
|
||||
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):
|
||||
self.skipreplay = True
|
||||
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,
|
||||
#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'})
|
||||
|
||||
|
||||
# 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,
|
||||
@@ -473,10 +761,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):
|
||||
self.registered = False
|
||||
self.tenant = configmanager.tenant
|
||||
if not configmanager.is_node(node):
|
||||
@@ -484,6 +771,8 @@ class ConsoleSession(object):
|
||||
self.username = username
|
||||
self.node = node
|
||||
self.configmanager = configmanager
|
||||
self.direct = direct # true if client is directly connected versus
|
||||
# relay
|
||||
self.connect_session()
|
||||
self.registered = True
|
||||
self._evt = None
|
||||
@@ -512,7 +801,7 @@ class ConsoleSession(object):
|
||||
between console and shell.
|
||||
"""
|
||||
self.conshdl = connect_node(self.node, self.configmanager,
|
||||
self.username)
|
||||
self.username, self.direct)
|
||||
def send_break(self):
|
||||
"""Send break to remote system
|
||||
"""
|
||||
@@ -543,6 +832,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 Lenovo
|
||||
# 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.
|
||||
@@ -33,21 +33,43 @@
|
||||
# functions. Console is special and just get's passed through
|
||||
# see API.txt
|
||||
|
||||
import confluent
|
||||
import confluent.alerts as alerts
|
||||
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
|
||||
import confluent.messages as msg
|
||||
import confluent.networking.macmap as macmap
|
||||
import confluent.noderange as noderange
|
||||
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
|
||||
except ImportError:
|
||||
import pickle
|
||||
import socket
|
||||
import struct
|
||||
import sys
|
||||
|
||||
pluginmap = {}
|
||||
dispatch_plugins = (b'ipmi', u'ipmi')
|
||||
|
||||
|
||||
def seek_element(currplace, currkey):
|
||||
@@ -100,7 +122,8 @@ def load_plugins():
|
||||
sys.path.pop(1)
|
||||
|
||||
|
||||
rootcollections = ['noderange/', 'nodes/', 'nodegroups/', 'users/', 'events/']
|
||||
rootcollections = ['discovery/', 'events/', 'networking/',
|
||||
'noderange/', 'nodes/', 'nodegroups/', 'users/', 'version']
|
||||
|
||||
|
||||
class PluginRoute(object):
|
||||
@@ -150,6 +173,10 @@ def _init_core():
|
||||
'pluginattrs': ['hardwaremanagement.method'],
|
||||
'default': 'ipmi',
|
||||
}),
|
||||
'hostname': PluginRoute({
|
||||
'pluginattrs': ['hardwaremanagement.method'],
|
||||
'default': 'ipmi',
|
||||
}),
|
||||
'identifier': PluginRoute({
|
||||
'pluginattrs': ['hardwaremanagement.method'],
|
||||
'default': 'ipmi',
|
||||
@@ -168,7 +195,21 @@ def _init_core():
|
||||
'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({
|
||||
@@ -181,6 +222,11 @@ def _init_core():
|
||||
'handler': 'ssh',
|
||||
}),
|
||||
},
|
||||
'_enclosure': {
|
||||
'reseat_bay': PluginRoute(
|
||||
{'pluginattrs': ['hardwaremanagement.method'],
|
||||
'default': 'ipmi'}),
|
||||
},
|
||||
'shell': {
|
||||
# another special case similar to console
|
||||
'sessions': PluginCollection({
|
||||
@@ -195,6 +241,10 @@ def _init_core():
|
||||
'default': 'ipmi',
|
||||
}),
|
||||
},
|
||||
'description': PluginRoute({
|
||||
'pluginattrs': ['hardwaremanagement.method'],
|
||||
'default': 'ipmi',
|
||||
}),
|
||||
'events': {
|
||||
'hardware': {
|
||||
'log': PluginRoute({
|
||||
@@ -207,6 +257,10 @@ def _init_core():
|
||||
}),
|
||||
},
|
||||
},
|
||||
#'forward': {
|
||||
# # Another dummy value, currently only for the gui
|
||||
# 'web': None,
|
||||
#},
|
||||
'health': {
|
||||
'hardware': PluginRoute({
|
||||
'pluginattrs': ['hardwaremanagement.method'],
|
||||
@@ -229,13 +283,39 @@ def _init_core():
|
||||
'pluginattrs': ['hardwaremanagement.method'],
|
||||
'default': 'ipmi',
|
||||
}),
|
||||
'updates': {
|
||||
'active': PluginCollection({
|
||||
'pluginattrs': ['hardwaremanagement.method'],
|
||||
'default': 'ipmi',
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
'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'],
|
||||
'default': 'ipmi',
|
||||
}),
|
||||
'reseat': PluginRoute({'handler': 'enclosure'}),
|
||||
},
|
||||
'sensors': {
|
||||
'hardware': {
|
||||
@@ -243,6 +323,10 @@ def _init_core():
|
||||
'pluginattrs': ['hardwaremanagement.method'],
|
||||
'default': 'ipmi',
|
||||
}),
|
||||
'energy': PluginCollection({
|
||||
'pluginattrs': ['hardwaremanagement.method'],
|
||||
'default': 'ipmi',
|
||||
}),
|
||||
'temperature': PluginCollection({
|
||||
'pluginattrs': ['hardwaremanagement.method'],
|
||||
'default': 'ipmi',
|
||||
@@ -262,6 +346,12 @@ def _init_core():
|
||||
},
|
||||
|
||||
},
|
||||
'support': {
|
||||
'servicedata': PluginCollection({
|
||||
'pluginattrs': ['hardwaremanagement.method'],
|
||||
'default': 'ipmi',
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
nodegroupresources = {
|
||||
@@ -322,10 +412,12 @@ def iterate_collections(iterable, forcecollection=True):
|
||||
|
||||
|
||||
def iterate_resources(fancydict):
|
||||
for resource in fancydict.iterkeys():
|
||||
for resource in fancydict:
|
||||
if resource.startswith("_"):
|
||||
continue
|
||||
if not isinstance(fancydict[resource], PluginRoute): # a resource
|
||||
if resource == 'abbreviate':
|
||||
pass
|
||||
elif not isinstance(fancydict[resource], PluginRoute): # a resource
|
||||
resource += '/'
|
||||
yield msg.ChildCollection(resource)
|
||||
|
||||
@@ -344,11 +436,14 @@ def delete_nodegroup_collection(collectionpath, configmanager):
|
||||
raise Exception("Not implemented")
|
||||
|
||||
|
||||
def delete_node_collection(collectionpath, configmanager):
|
||||
def delete_node_collection(collectionpath, configmanager, isnoderange):
|
||||
if len(collectionpath) == 2: # just node
|
||||
node = collectionpath[-1]
|
||||
configmanager.del_nodes([node])
|
||||
yield msg.DeletedResource(node)
|
||||
nodes = [collectionpath[-1]]
|
||||
if isnoderange:
|
||||
nodes = noderange.NodeRange(nodes[0], configmanager).nodes
|
||||
configmanager.del_nodes(nodes)
|
||||
for node in nodes:
|
||||
yield msg.DeletedResource(node)
|
||||
else:
|
||||
raise Exception("Not implemented")
|
||||
|
||||
@@ -356,7 +451,8 @@ def delete_node_collection(collectionpath, configmanager):
|
||||
def enumerate_nodegroup_collection(collectionpath, configmanager):
|
||||
nodegroup = collectionpath[1]
|
||||
if not configmanager.is_nodegroup(nodegroup):
|
||||
raise exc.NotFoundException("Invalid element requested")
|
||||
raise exc.NotFoundException(
|
||||
'Invalid nodegroup: {0} not found'.format(nodegroup))
|
||||
del collectionpath[0:2]
|
||||
collection = nested_lookup(nodegroupresources, collectionpath)
|
||||
return iterate_resources(collection)
|
||||
@@ -376,6 +472,7 @@ def enumerate_node_collection(collectionpath, configmanager):
|
||||
collection = nested_lookup(noderesources, collectionpath[2:])
|
||||
if len(collectionpath) == 2 and collectionpath[0] == 'noderange':
|
||||
collection['nodes'] = {}
|
||||
collection['abbreviate'] = {}
|
||||
if not isinstance(collection, dict):
|
||||
raise exc.NotFoundException("Invalid element requested")
|
||||
return iterate_resources(collection)
|
||||
@@ -392,6 +489,7 @@ def create_group(inputdata, configmanager):
|
||||
configmanager.add_group_attributes(attribmap)
|
||||
except ValueError as e:
|
||||
raise exc.InvalidArgumentException(str(e))
|
||||
yield msg.CreatedResource(groupname)
|
||||
|
||||
|
||||
def create_node(inputdata, configmanager):
|
||||
@@ -405,6 +503,25 @@ def create_node(inputdata, configmanager):
|
||||
configmanager.add_node_attributes(attribmap)
|
||||
except ValueError as e:
|
||||
raise exc.InvalidArgumentException(str(e))
|
||||
yield msg.CreatedResource(nodename)
|
||||
|
||||
|
||||
def create_noderange(inputdata, configmanager):
|
||||
try:
|
||||
noder = inputdata['name']
|
||||
del inputdata['name']
|
||||
attribmap = {}
|
||||
for node in noderange.NodeRange(noder).nodes:
|
||||
attribmap[node] = inputdata
|
||||
except KeyError:
|
||||
raise exc.InvalidArgumentException('name not specified')
|
||||
try:
|
||||
configmanager.add_node_attributes(attribmap)
|
||||
except ValueError as e:
|
||||
raise exc.InvalidArgumentException(str(e))
|
||||
for node in attribmap:
|
||||
yield msg.CreatedResource(node)
|
||||
|
||||
|
||||
|
||||
def enumerate_collections(collections):
|
||||
@@ -419,7 +536,7 @@ def handle_nodegroup_request(configmanager, inputdata,
|
||||
if len(pathcomponents) < 2:
|
||||
if operation == "create":
|
||||
inputdata = msg.InputAttributes(pathcomponents, inputdata)
|
||||
create_group(inputdata.attribs, configmanager)
|
||||
return create_group(inputdata.attribs, configmanager)
|
||||
allgroups = list(configmanager.get_groups())
|
||||
try:
|
||||
allgroups.sort(key=noderange.humanify_nodename)
|
||||
@@ -458,6 +575,92 @@ def handle_nodegroup_request(configmanager, inputdata,
|
||||
raise Exception("unknown case encountered")
|
||||
|
||||
|
||||
class BadPlugin(object):
|
||||
def __init__(self, node, plugin):
|
||||
self.node = node
|
||||
self.plugin = plugin
|
||||
|
||||
def error(self, *args, **kwargs):
|
||||
yield msg.ConfluentNodeError(
|
||||
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')
|
||||
if 'nodes' not in inputdata:
|
||||
raise exc.InvalidArgumentException('Must be given list of nodes under key named nodes')
|
||||
if isinstance(inputdata['nodes'], str) or isinstance(inputdata['nodes'], unicode):
|
||||
inputdata['nodes'] = inputdata['nodes'].split(',')
|
||||
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
|
||||
dispatch = pickle.loads(dispatch)
|
||||
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 is not None:
|
||||
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)
|
||||
except Exception as res:
|
||||
_forward_rsp(connection, res)
|
||||
connection.sendall('\x00\x00\x00\x00\x00\x00\x00\x00')
|
||||
|
||||
|
||||
def _forward_rsp(connection, res):
|
||||
r = pickle.dumps(res)
|
||||
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):
|
||||
iscollection = False
|
||||
@@ -470,6 +673,8 @@ def handle_node_request(configmanager, inputdata, operation,
|
||||
# facility anyway
|
||||
isnoderange = False
|
||||
pathcomponents = pathcomponents[2:]
|
||||
elif len(pathcomponents) == 3 and pathcomponents[2] == 'abbreviate':
|
||||
return abbreviate_noderange(configmanager, inputdata, operation)
|
||||
else:
|
||||
isnoderange = True
|
||||
else:
|
||||
@@ -478,7 +683,8 @@ def handle_node_request(configmanager, inputdata, operation,
|
||||
nodeorrange = pathcomponents[1]
|
||||
if not isnoderange and not configmanager.is_node(nodeorrange):
|
||||
raise exc.NotFoundException("Invalid Node")
|
||||
if isnoderange:
|
||||
if isnoderange and not (len(pathcomponents) == 3 and
|
||||
pathcomponents[2] == 'abbreviate'):
|
||||
try:
|
||||
nodes = noderange.NodeRange(nodeorrange, configmanager).nodes
|
||||
except Exception as e:
|
||||
@@ -489,11 +695,14 @@ def handle_node_request(configmanager, inputdata, operation,
|
||||
# this is enumerating a list of nodes or just empty noderange
|
||||
if isnoderange and operation == "retrieve":
|
||||
return iterate_collections([])
|
||||
elif isnoderange and operation == "create":
|
||||
inputdata = msg.InputAttributes(pathcomponents, inputdata)
|
||||
return create_noderange(inputdata.attribs, configmanager)
|
||||
elif isnoderange or operation == "delete":
|
||||
raise exc.InvalidArgumentException()
|
||||
if operation == "create":
|
||||
inputdata = msg.InputAttributes(pathcomponents, inputdata)
|
||||
create_node(inputdata.attribs, configmanager)
|
||||
return create_node(inputdata.attribs, configmanager)
|
||||
allnodes = list(configmanager.list_nodes())
|
||||
try:
|
||||
allnodes.sort(key=noderange.humanify_nodename)
|
||||
@@ -524,16 +733,18 @@ def handle_node_request(configmanager, inputdata, operation,
|
||||
raise exc.InvalidArgumentException('Custom interface required for resource')
|
||||
if iscollection:
|
||||
if operation == "delete":
|
||||
return delete_node_collection(pathcomponents, configmanager)
|
||||
return delete_node_collection(pathcomponents, configmanager,
|
||||
isnoderange)
|
||||
elif operation == "retrieve":
|
||||
return enumerate_node_collection(pathcomponents, configmanager)
|
||||
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)
|
||||
pathcomponents, operation, inputdata, nodes, isnoderange,
|
||||
configmanager)
|
||||
if 'handler' in plugroute: # fixed handler definition, easy enough
|
||||
if isinstance(plugroute['handler'], str):
|
||||
hfunc = getattr(pluginmap[plugroute['handler']], operation)
|
||||
@@ -546,39 +757,67 @@ 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']
|
||||
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 is not None:
|
||||
hfunc = getattr(pluginmap[plugpath], operation)
|
||||
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]
|
||||
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()
|
||||
|
||||
@@ -588,6 +827,125 @@ 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(90)
|
||||
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")
|
||||
tlvdata.recv(remote)
|
||||
tlvdata.recv(remote)
|
||||
myname = collective.get_myname()
|
||||
dreq = pickle.dumps({'name': myname, 'nodes': list(nodes),
|
||||
'path': element,'tenant': configmanager.tenant,
|
||||
'operation': operation, 'inputdata': inputdata})
|
||||
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
|
||||
rsp = pickle.loads(rsp)
|
||||
if isinstance(rsp, Exception):
|
||||
raise rsp
|
||||
yield rsp
|
||||
|
||||
|
||||
def handle_discovery(pathcomponents, operation, configmanager, inputdata):
|
||||
if pathcomponents[0] == 'detected':
|
||||
pass
|
||||
|
||||
def handle_discovery(pathcomponents, operation, configmanager, inputdata):
|
||||
if pathcomponents[0] == 'detected':
|
||||
pass
|
||||
|
||||
def handle_path(path, operation, configmanager, inputdata=None, autostrip=True):
|
||||
"""Given a full path request, return an object.
|
||||
|
||||
@@ -612,6 +970,14 @@ def handle_path(path, operation, configmanager, inputdata=None, autostrip=True):
|
||||
# single node request of some sort
|
||||
return handle_node_request(configmanager, inputdata,
|
||||
operation, pathcomponents, autostrip)
|
||||
elif pathcomponents[0] == 'discovery':
|
||||
return disco.handle_api_request(
|
||||
configmanager, inputdata, operation, pathcomponents)
|
||||
elif pathcomponents[0] == 'networking':
|
||||
return macmap.handle_api_request(
|
||||
configmanager, inputdata, operation, pathcomponents)
|
||||
elif pathcomponents[0] == 'version':
|
||||
return (msg.Attributes(kv={'version': confluent.__version__}),)
|
||||
elif pathcomponents[0] == 'users':
|
||||
# TODO: when non-administrator accounts exist,
|
||||
# they must only be allowed to see their own user
|
||||
@@ -620,7 +986,8 @@ def handle_path(path, operation, configmanager, inputdata=None, autostrip=True):
|
||||
except IndexError: # it's just users/
|
||||
if operation == 'create':
|
||||
inputdata = msg.get_input_message(
|
||||
pathcomponents, operation, inputdata)
|
||||
pathcomponents, operation, inputdata,
|
||||
configmanager=configmanager)
|
||||
create_user(inputdata.attribs, configmanager)
|
||||
return iterate_collections(configmanager.list_users(),
|
||||
forcecollection=False)
|
||||
@@ -632,7 +999,8 @@ def handle_path(path, operation, configmanager, inputdata=None, autostrip=True):
|
||||
return delete_user(user, configmanager)
|
||||
elif operation == 'update':
|
||||
inputdata = msg.get_input_message(
|
||||
pathcomponents, operation, inputdata)
|
||||
pathcomponents, operation, inputdata,
|
||||
configmanager=configmanager)
|
||||
update_user(user, inputdata.attribs, configmanager)
|
||||
return show_user(user, configmanager)
|
||||
elif pathcomponents[0] == 'events':
|
||||
@@ -646,5 +1014,8 @@ def handle_path(path, operation, configmanager, inputdata=None, autostrip=True):
|
||||
raise exc.NotFoundException()
|
||||
if operation == 'update':
|
||||
return alerts.decode_alert(inputdata, configmanager)
|
||||
elif pathcomponents[0] == 'discovery':
|
||||
return handle_discovery(pathcomponents[1:], operation, configmanager,
|
||||
inputdata)
|
||||
else:
|
||||
raise exc.NotFoundException()
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,161 @@
|
||||
# Copyright 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.
|
||||
|
||||
import confluent.discovery.handlers.generic as generic
|
||||
import confluent.exceptions as exc
|
||||
import confluent.netutil as netutil
|
||||
import eventlet.support.greendns
|
||||
|
||||
# Provide foundation for general IPMI device configuration
|
||||
|
||||
import pyghmi.exceptions as pygexc
|
||||
ipmicommand = eventlet.import_patched('pyghmi.ipmi.command')
|
||||
ipmicommand.session.select = eventlet.green.select
|
||||
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):
|
||||
|
||||
def _get_ipmicmd(self, user=DEFAULT_USER, password=DEFAULT_PASS):
|
||||
return ipmicommand.Command(self.ipaddr, user, password)
|
||||
|
||||
def __init__(self, info, configmanager):
|
||||
super(NodeHandler, self).__init__(info, configmanager)
|
||||
|
||||
def probe(self):
|
||||
return
|
||||
# TODO(jjohnson2): probe serial number and uuid
|
||||
|
||||
def config(self, nodename, reset=False):
|
||||
self._bmcconfig(nodename, reset)
|
||||
|
||||
def _bmcconfig(self, nodename, reset=False, customconfig=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.
|
||||
try:
|
||||
ic = self._get_ipmicmd()
|
||||
passwd = 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:
|
||||
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:
|
||||
havecustomcreds = True
|
||||
else:
|
||||
passwd = DEFAULT_PASS
|
||||
if havecustomcreds:
|
||||
ic = self._get_ipmicmd(user, passwd)
|
||||
else:
|
||||
raise
|
||||
if customconfig:
|
||||
customconfig(ic)
|
||||
currusers = ic.get_users()
|
||||
lanchan = ic.get_network_channel()
|
||||
userdata = ic.xraw_command(netfn=6, command=0x44, data=(lanchan,
|
||||
1))
|
||||
userdata = bytearray(userdata['data'])
|
||||
maxusers = userdata[0] & 0b111111
|
||||
enabledusers = userdata[1] & 0b111111
|
||||
lockedusers = userdata[2] & 0b111111
|
||||
cfg = self.configmanager
|
||||
cd = cfg.get_node_attributes(
|
||||
nodename, ['secret.hardwaremanagementuser',
|
||||
'secret.hardwaremanagementpassword',
|
||||
'hardwaremanagement.manager'], True)
|
||||
cd = cd.get(nodename, {})
|
||||
if ('secret.hardwaremanagementuser' not in cd or
|
||||
'secret.hardwaremanagementpassword' not in cd):
|
||||
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)
|
||||
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')
|
||||
# Now to zap others
|
||||
for uid in currusers:
|
||||
if uid != newuserslot:
|
||||
if uid <= lockedusers: # we cannot delete, settle for disable
|
||||
ic.disable_user(uid, 'disable')
|
||||
else:
|
||||
# lead with the most critical thing, removing user access
|
||||
ic.set_user_access(uid, channel=None, callback=False,
|
||||
link_auth=False, ipmi_msg=False,
|
||||
privilege_level='no_access')
|
||||
# next, try to disable the password
|
||||
ic.set_user_password(uid, mode='disable', password=None)
|
||||
# ok, now we can be less paranoid
|
||||
try:
|
||||
ic.user_delete(uid)
|
||||
except pygexc.IpmiException as ie:
|
||||
if ie.ipmicode != 0xd5: # some response to the 0xff
|
||||
# name...
|
||||
# the user will remain, but that is life
|
||||
raise
|
||||
if reset:
|
||||
ic.reset_bmc()
|
||||
return ic
|
||||
@@ -0,0 +1,96 @@
|
||||
# Copyright 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.
|
||||
|
||||
import errno
|
||||
import eventlet
|
||||
webclient = eventlet.import_patched('pyghmi.util.webclient')
|
||||
|
||||
class NodeHandler(object):
|
||||
https_supported = True
|
||||
is_enclosure = False
|
||||
devname = ''
|
||||
maxmacs = 2 # reasonable default, allowing for common scenario of
|
||||
# shared nic in theory, but blocking enclosure managers
|
||||
# and uplink ports
|
||||
|
||||
def __init__(self, info, configmanager):
|
||||
self._certfailreason = None
|
||||
self._fp = None
|
||||
self.info = info
|
||||
self.configmanager = configmanager
|
||||
targsa = [None]
|
||||
# first let us prefer LLA if possible, since that's most stable
|
||||
for sa in info['addresses']:
|
||||
if sa[0].startswith('fe80'):
|
||||
targsa = sa
|
||||
break
|
||||
else:
|
||||
if info.get('addresses', False):
|
||||
targsa = info['addresses'][0]
|
||||
self.ipaddr = targsa[0]
|
||||
|
||||
def scan(self):
|
||||
# Do completely passive things to enhance data.
|
||||
# Probe is permitted to for example attempt a login
|
||||
# scan *only* does what it can without a login attempt
|
||||
return
|
||||
|
||||
def probe(self):
|
||||
# Use appropriate direct strategy to gather data such as
|
||||
# serial number and uuid to flesh out data as needed
|
||||
return
|
||||
|
||||
def preconfig(self):
|
||||
return
|
||||
|
||||
def discoverable_by_switch(self, macs):
|
||||
# Given the number of macs sharing the port, is this handler
|
||||
# appropriate?
|
||||
return macs <= self.maxmacs
|
||||
|
||||
def _savecert(self, certificate):
|
||||
self._fp = certificate
|
||||
return True
|
||||
|
||||
@property
|
||||
def cert_fail_reason(self):
|
||||
if self._certfailreason == 1:
|
||||
return 'refused'
|
||||
elif self._certfailreason == 2:
|
||||
return 'unreachable'
|
||||
|
||||
@property
|
||||
def https_cert(self):
|
||||
if self._fp:
|
||||
return self._fp
|
||||
if ':' in self.ipaddr:
|
||||
ip = '[{0}]'.format(self.ipaddr)
|
||||
else:
|
||||
ip = self.ipaddr
|
||||
wc = webclient.SecureHTTPConnection(ip, verifycallback=self._savecert)
|
||||
try:
|
||||
wc.connect()
|
||||
except IOError as ie:
|
||||
if ie.errno == errno.ECONNREFUSED:
|
||||
self._certfailreason = 1
|
||||
return None
|
||||
elif ie.errno == errno.EHOSTUNREACH:
|
||||
self._certfailreason = 2
|
||||
return None
|
||||
self._certfailreason = 2
|
||||
return None
|
||||
except Exception:
|
||||
self._certfailreason = 2
|
||||
return None
|
||||
return self._fp
|
||||
@@ -0,0 +1,85 @@
|
||||
# Copyright 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.
|
||||
|
||||
import confluent.discovery.handlers.bmc as bmchandler
|
||||
import pyghmi.exceptions as pygexc
|
||||
import pyghmi.ipmi.private.util as pygutil
|
||||
import string
|
||||
import struct
|
||||
|
||||
class NodeHandler(bmchandler.NodeHandler):
|
||||
devname = 'IMM'
|
||||
|
||||
def scan(self):
|
||||
slpattrs = self.info.get('attributes', {})
|
||||
self.isdense = False
|
||||
try:
|
||||
ff = slpattrs.get('enclosure-form-factor', [''])[0]
|
||||
except IndexError:
|
||||
return
|
||||
wronguuid = slpattrs.get('node-uuid', [''])[0]
|
||||
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 = uuidprefix[:8] + '-' + uuidprefix[8:12] + '-' + \
|
||||
uuidprefix[12:16]
|
||||
self.info['uuid'] = uuidprefix + '-' + '-'.join(
|
||||
wronguuid.split('-')[3:])
|
||||
self.info['uuid'] = string.lower(self.info['uuid'])
|
||||
if ff not in ('dense-computing', 'BC2'):
|
||||
# do not probe unless it's a dense platform
|
||||
return
|
||||
self.isdense = True
|
||||
slot = int(slpattrs.get('slot', ['0'])[0])
|
||||
if slot != 0:
|
||||
self.info['enclosure.bay'] = slot
|
||||
|
||||
def probe(self):
|
||||
if self.info.get('enclosure.bay', 0) == 0:
|
||||
self.scan()
|
||||
if self.info.get('enclosure.bay', 0) != 0:
|
||||
# scan has already populated info
|
||||
return
|
||||
ff = self.info.get('attributes', {}).get('enclosure-form-factor', '')
|
||||
if ff != 'dense-computing':
|
||||
return
|
||||
try:
|
||||
# we are a dense platform, but the SLP data did not give us slot
|
||||
# attempt to probe using IPMI
|
||||
ipmicmd = self._get_ipmicmd()
|
||||
guiddata = ipmicmd.xraw_command(netfn=6, command=8)
|
||||
self.info['uuid'] = pygutil.decode_wireformat_uuid(
|
||||
guiddata['data']).lower()
|
||||
ipmicmd.oem_init()
|
||||
bayid = ipmicmd._oem.immhandler.get_property(
|
||||
'/v2/cmm/sp/7')
|
||||
if not bayid:
|
||||
return
|
||||
self.info['enclosure.bay'] = int(bayid)
|
||||
smmid = ipmicmd._oem.immhandler.get_property(
|
||||
'/v2/ibmc/smm/chassis/uuid')
|
||||
if not smmid:
|
||||
return
|
||||
smmid = smmid.lower().replace(' ', '')
|
||||
smmid = '{0}-{1}-{2}-{3}-{4}'.format(smmid[:8], smmid[8:12],
|
||||
smmid[12:16], smmid[16:20],
|
||||
smmid[20:])
|
||||
self.info['enclosure.uuid'] = smmid
|
||||
self.info['enclosure.type'] = 'smm'
|
||||
except pygexc.IpmiException as ie:
|
||||
print(repr(ie))
|
||||
raise
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
# Copyright 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.
|
||||
|
||||
# This contains functionality for passive detection and, one day, active
|
||||
# response to pxe
|
||||
|
||||
|
||||
import confluent.discovery.handlers.generic as generic
|
||||
|
||||
class NodeHandler(generic.NodeHandler):
|
||||
https_supported = False
|
||||
is_enclosure = False
|
||||
devname = 'PXE'
|
||||
|
||||
def __init__(self, info, configmanager):
|
||||
self.ipaddr = ''
|
||||
self.cfm = configmanager
|
||||
|
||||
@property
|
||||
def cert_fail_reason(self):
|
||||
return 'unsupported'
|
||||
|
||||
@property
|
||||
def https_cert(self):
|
||||
return None
|
||||
|
||||
def config(self, nodename):
|
||||
return
|
||||
@@ -0,0 +1,56 @@
|
||||
# Copyright 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.
|
||||
|
||||
import confluent.discovery.handlers.bmc as bmchandler
|
||||
import struct
|
||||
|
||||
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(
|
||||
'hex')
|
||||
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
|
||||
|
||||
def scan(self):
|
||||
# the UUID is in a weird order, fix it up to match
|
||||
# ipmi return and property value
|
||||
uuid = self.info.get('attributes', {}).get('uuid', None)
|
||||
if uuid:
|
||||
uuid = fixuuid(uuid[0])
|
||||
self.info['uuid'] = uuid
|
||||
|
||||
def config(self, nodename):
|
||||
# SMM for now has to reset to assure configuration applies
|
||||
super(NodeHandler, self).config(nodename)
|
||||
|
||||
# notes for smm:
|
||||
# POST to:
|
||||
# https://172.30.254.160/data/changepwd
|
||||
# oripwd=PASSW0RD&newpwd=Passw0rd!4321
|
||||
# got response:
|
||||
# <?xml version="1.0" encoding="UTF-8"?><root><statusCode>0-ChangePwd</statusCode><fowardUrl>login.html</fowardUrl><status>ok</status></root>
|
||||
# requires relogin
|
||||
# https://172.30.254.160/index.html
|
||||
# post to:
|
||||
# https://172.30.254.160/data/login
|
||||
# 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
|
||||
@@ -0,0 +1,103 @@
|
||||
# Copyright 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.
|
||||
|
||||
import confluent.discovery.handlers.imm as immhandler
|
||||
import confluent.util as util
|
||||
import pyghmi.exceptions as pygexc
|
||||
import pyghmi.ipmi.oem.lenovo.imm as imm
|
||||
|
||||
|
||||
|
||||
|
||||
class NodeHandler(immhandler.NodeHandler):
|
||||
devname = 'XCC'
|
||||
|
||||
def preconfig(self):
|
||||
ff = self.info.get('attributes', {}).get('enclosure-form-factor', '')
|
||||
if ff not in ('dense-computing', [u'dense-computing']):
|
||||
return
|
||||
# attempt to enable SMM
|
||||
#it's normal to get a 'not supported' (193) for systems without an SMM
|
||||
ipmicmd = None
|
||||
try:
|
||||
ipmicmd = self._get_ipmicmd()
|
||||
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
|
||||
raise
|
||||
#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 set_password_policy(self, ic):
|
||||
ruleset = {'USER_GlobalMinPassChgInt': '0'}
|
||||
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'):
|
||||
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
|
||||
ic.register_key_handler(self.validate_cert)
|
||||
ic.oem_init()
|
||||
ic._oem.immhandler.wc.grab_json_response('/api/dataset', ruleset)
|
||||
|
||||
def config(self, nodename, reset=False):
|
||||
# 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.
|
||||
dpp = self.configmanager.get_node_attributes(
|
||||
nodename, 'discovery.passwordrules')
|
||||
self.ruleset = dpp.get(nodename, {}).get(
|
||||
'discovery.passwordrules', {}).get('value', '')
|
||||
ic = self._bmcconfig(nodename, customconfig=self.set_password_policy)
|
||||
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..
|
||||
enclosureuuid = ic._oem.immhandler.get_property(
|
||||
'/v2/ibmc/smm/chassis/uuid')
|
||||
enclosureuuid = ic._oem.immhandler.get_property(
|
||||
'/v2/ibmc/smm/chassis/uuid')
|
||||
if enclosureuuid:
|
||||
enclosureuuid = imm.fixup_uuid(enclosureuuid).lower()
|
||||
em = self.configmanager.get_node_attributes(nodename,
|
||||
'enclosure.manager')
|
||||
em = em.get(nodename, {}).get('enclosure.manager', {}).get(
|
||||
'value', None)
|
||||
# ok, set the uuid of the manager...
|
||||
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
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 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.
|
||||
|
||||
# We can listen to port 69 with SO_REUSEADDR to snoop port 69 *even* if dhcp
|
||||
# is running (because the other dhcp servers do it already)
|
||||
|
||||
# Goal is to detect and act on a DHCPDISCOVER, without actually having to do
|
||||
# any offer
|
||||
|
||||
# option 97 = UUID (wireformat)
|
||||
|
||||
import eventlet.green.socket as socket
|
||||
import struct
|
||||
|
||||
pxearchs = {
|
||||
'\x00\x00': 'bios-x86',
|
||||
'\x00\x07': 'uefi-x64',
|
||||
'\x00\x09': 'uefi-x64',
|
||||
'\x00\x0b': 'uefi-aarch64',
|
||||
}
|
||||
|
||||
|
||||
def decode_uuid(rawguid):
|
||||
lebytes = struct.unpack_from('<IHH', buffer(rawguid[:8]))
|
||||
bebytes = struct.unpack_from('>HHI', buffer(rawguid[8:]))
|
||||
return '{0:08X}-{1:04X}-{2:04X}-{3:04X}-{4:04X}{5:08X}'.format(
|
||||
lebytes[0], lebytes[1], lebytes[2], bebytes[0], bebytes[1], bebytes[2]).lower()
|
||||
|
||||
|
||||
def find_info_in_options(rq, optidx):
|
||||
uuid = None
|
||||
arch = None
|
||||
try:
|
||||
while uuid is None or arch is None:
|
||||
if rq[optidx] == 53: # DHCP message type
|
||||
# we want only length 1 and only discover (type 1)
|
||||
if rq[optidx + 1] != 1 or rq[optidx + 2] != 1:
|
||||
return uuid, arch
|
||||
optidx += 3
|
||||
elif rq[optidx] == 97:
|
||||
if rq[optidx + 1] != 17:
|
||||
# 16 bytes of uuid and one reserved byte
|
||||
return uuid, arch
|
||||
if rq[optidx + 2] != 0: # the reserved byte should be zero,
|
||||
# anything else would be a new spec that we don't know yet
|
||||
return uuid, arch
|
||||
uuid = decode_uuid(rq[optidx + 3:optidx + 19])
|
||||
optidx += 19
|
||||
elif rq[optidx] == 93:
|
||||
if rq[optidx + 1] != 2:
|
||||
return uuid, arch
|
||||
archraw = bytes(rq[optidx + 2:optidx + 4])
|
||||
if archraw in pxearchs:
|
||||
arch = pxearchs[archraw]
|
||||
optidx += 4
|
||||
else:
|
||||
optidx += rq[optidx + 1] + 2
|
||||
except IndexError:
|
||||
return uuid, arch
|
||||
return uuid, arch
|
||||
|
||||
def snoop(handler):
|
||||
#TODO(jjohnson2): ipv6 socket and multicast for DHCPv6, should that be
|
||||
#prominent
|
||||
#TODO(jjohnson2): IP_PKTINFO, recvmsg to get the destination ip, per
|
||||
#proxydhcp.c from xCAT
|
||||
net4 = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
net4.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
net4.bind(('', 67))
|
||||
while True:
|
||||
# Just need some delay, picked a prime number so that overlap with other
|
||||
# timers might be reduced, though it really is probably nothing
|
||||
(rq, peer) = net4.recvfrom(9000)
|
||||
# if we have a small packet, just skip, it can't possible hold enough
|
||||
# data and avoids some downstream IndexErrors that would be messy
|
||||
# with try/except
|
||||
if len(rq) < 64:
|
||||
continue
|
||||
rq = bytearray(rq)
|
||||
if rq[0] == 1: # Boot request
|
||||
addrlen = rq[2]
|
||||
if addrlen > 16 or addrlen == 0:
|
||||
continue
|
||||
netaddr = rq[28:28+addrlen]
|
||||
netaddr = ':'.join(['{0:02x}'.format(x) for x in netaddr])
|
||||
optidx = 0
|
||||
try:
|
||||
optidx = rq.index('\x63\x82\x53\x63') + 4
|
||||
except ValueError:
|
||||
continue
|
||||
uuid, arch = find_info_in_options(rq, optidx)
|
||||
if uuid is None:
|
||||
continue
|
||||
# We will fill out service to have something to byte into,
|
||||
# but the nature of the beast is that we do not have peers,
|
||||
# so that will not be present for a pxe snoop
|
||||
handler({'hwaddr': netaddr, 'uuid': uuid, 'architecture': arch,
|
||||
'services': ('pxe-client',)})
|
||||
|
||||
if __name__ == '__main__':
|
||||
def testsnoop(info):
|
||||
print(repr(info))
|
||||
snoop(testsnoop)
|
||||
|
||||
|
||||
@@ -0,0 +1,550 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 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.
|
||||
|
||||
import confluent.neighutil as neighutil
|
||||
import confluent.util as util
|
||||
import confluent.log as log
|
||||
import os
|
||||
import random
|
||||
import eventlet.green.select as select
|
||||
import eventlet.green.socket as socket
|
||||
import struct
|
||||
import traceback
|
||||
|
||||
_slp_services = set([
|
||||
'service:management-hardware.IBM:integrated-management-module2',
|
||||
'service:lenovo-smm',
|
||||
'service:management-hardware.Lenovo:lenovo-xclarity-controller',
|
||||
'service:management-hardware.IBM:chassis-management-module',
|
||||
'service:management-hardware.Lenovo:chassis-management-module',
|
||||
'service:io-device.Lenovo:management-module',
|
||||
])
|
||||
|
||||
# SLP has a lot of ambition that was unfulfilled in practice.
|
||||
# So we have a static footer here to always use 'DEFAULT' scope, no LDAP
|
||||
# predicates, and no authentication for service requests
|
||||
srvreqfooter = b'\x00\x07DEFAULT\x00\x00\x00\x00'
|
||||
# An empty instance of the attribute list extension
|
||||
# which is defined in RFC 3059, used to indicate support for that capability
|
||||
attrlistext = b'\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||
|
||||
try:
|
||||
IPPROTO_IPV6 = socket.IPPROTO_IPV6
|
||||
except AttributeError:
|
||||
IPPROTO_IPV6 = 41 # Assume Windows value if socket is missing it
|
||||
|
||||
|
||||
|
||||
def _parse_slp_header(packet):
|
||||
packet = bytearray(packet)
|
||||
if len(packet) < 16 or packet[0] != 2:
|
||||
# discard packets that are obviously useless
|
||||
return None
|
||||
parsed = {
|
||||
'function': packet[1],
|
||||
}
|
||||
(offset, parsed['xid'], langlen) = struct.unpack('!IHH',
|
||||
bytes(b'\x00' + packet[7:14]))
|
||||
parsed['lang'] = packet[14:14 + langlen].decode('utf-8')
|
||||
parsed['payload'] = packet[14 + langlen:]
|
||||
if offset:
|
||||
parsed['offset'] = 14 + langlen
|
||||
parsed['extoffset'] = offset
|
||||
return parsed
|
||||
|
||||
|
||||
def _pop_url(payload):
|
||||
urllen = struct.unpack('!H', bytes(payload[3:5]))[0]
|
||||
url = bytes(payload[5:5+urllen]).decode('utf-8')
|
||||
if payload[5+urllen] != 0:
|
||||
raise Exception('Auth blocks unsupported')
|
||||
payload = payload[5+urllen+1:]
|
||||
return url, payload
|
||||
|
||||
|
||||
def _parse_SrvRply(parsed):
|
||||
""" Modify passed dictionary to have parsed data
|
||||
|
||||
|
||||
:param parsed:
|
||||
:return:
|
||||
"""
|
||||
payload = parsed['payload']
|
||||
ecode, ucount = struct.unpack('!HH', bytes(payload[0:4]))
|
||||
if ecode:
|
||||
parsed['errorcode'] = ecode
|
||||
payload = payload[4:]
|
||||
parsed['urls'] = []
|
||||
while ucount:
|
||||
ucount -= 1
|
||||
url, payload = _pop_url(payload)
|
||||
parsed['urls'].append(url)
|
||||
|
||||
|
||||
def _parse_slp_packet(packet, peer, rsps, xidmap):
|
||||
parsed = _parse_slp_header(packet)
|
||||
if not parsed:
|
||||
return
|
||||
addr = peer[0]
|
||||
if '%' in addr:
|
||||
addr = addr[:addr.index('%')]
|
||||
mac = None
|
||||
if addr in neighutil.neightable:
|
||||
identifier = neighutil.neightable[addr]
|
||||
mac = identifier
|
||||
else:
|
||||
identifier = addr
|
||||
if (identifier, parsed['xid']) in rsps:
|
||||
# avoid obviously duplicate entries
|
||||
parsed = rsps[(identifier, parsed['xid'])]
|
||||
else:
|
||||
rsps[(identifier, parsed['xid'])] = parsed
|
||||
if mac and 'hwaddr' not in parsed:
|
||||
parsed['hwaddr'] = mac
|
||||
if parsed['xid'] in xidmap:
|
||||
parsed['services'] = [xidmap[parsed['xid']]]
|
||||
if 'addresses' in parsed:
|
||||
if peer not in parsed['addresses']:
|
||||
parsed['addresses'].append(peer)
|
||||
else:
|
||||
parsed['addresses'] = [peer]
|
||||
if parsed['function'] == 2: # A service reply
|
||||
_parse_SrvRply(parsed)
|
||||
|
||||
|
||||
def _v6mcasthash(srvtype):
|
||||
# The hash algorithm described by RFC 3111
|
||||
nums = bytearray(srvtype.encode('utf-8'))
|
||||
hashval = 0
|
||||
for i in nums:
|
||||
hashval *= 33
|
||||
hashval += i
|
||||
hashval &= 0xffff # only need to track the lowest 16 bits
|
||||
hashval &= 0x3ff
|
||||
hashval |= 0x1000
|
||||
return '{0:x}'.format(hashval)
|
||||
|
||||
|
||||
def _generate_slp_header(payload, multicast, functionid, xid, extoffset=0):
|
||||
if multicast:
|
||||
flags = 0x2000
|
||||
else:
|
||||
flags = 0
|
||||
packetlen = len(payload) + 16 # we have a fixed 16 byte header supported
|
||||
if extoffset: # if we have an offset, add 16 to account for this function
|
||||
# generating a 16 byte header
|
||||
extoffset += 16
|
||||
if packetlen > 1400:
|
||||
# For now, we aren't intending to support large SLP transmits
|
||||
# raise an exception to help identify if such a requirement emerges
|
||||
raise Exception("TODO: Transmit overflow packets")
|
||||
# We always do SLP v2, and only v2
|
||||
header = bytearray([2, functionid])
|
||||
# SLP uses 24 bit packed integers, so in such places we pack 32 then
|
||||
# discard the high byte
|
||||
header.extend(struct.pack('!IH', packetlen, flags)[1:])
|
||||
# '2' below refers to the length of the language tag
|
||||
header.extend(struct.pack('!IHH', extoffset, xid, 2)[1:])
|
||||
# we only do english (in SLP world, it's not like non-english appears...)
|
||||
header.extend(b'en')
|
||||
return header
|
||||
|
||||
def _generate_attr_request(service, xid):
|
||||
service = service.encode('utf-8')
|
||||
payload = bytearray(struct.pack('!HH', 0, len(service)) + service)
|
||||
payload.extend(srvreqfooter)
|
||||
header = _generate_slp_header(payload, False, functionid=6, xid=xid)
|
||||
return header + payload
|
||||
|
||||
|
||||
|
||||
def _generate_request_payload(srvtype, multicast, xid, prlist=''):
|
||||
prlist = prlist.encode('utf-8')
|
||||
payload = bytearray(struct.pack('!H', len(prlist)) + prlist)
|
||||
srvtype = srvtype.encode('utf-8')
|
||||
payload.extend(struct.pack('!H', len(srvtype)) + srvtype)
|
||||
payload.extend(srvreqfooter)
|
||||
extoffset = len(payload)
|
||||
payload.extend(attrlistext)
|
||||
header = _generate_slp_header(payload, multicast, functionid=1, xid=xid,
|
||||
extoffset=extoffset)
|
||||
return header + payload
|
||||
|
||||
|
||||
def _find_srvtype(net, net4, srvtype, addresses, xid):
|
||||
"""Internal function to find a single service type
|
||||
|
||||
Helper to do singleton requests to srvtype
|
||||
|
||||
:param net: Socket active
|
||||
:param srvtype: Service type to do now
|
||||
:param addresses: Pass through of addresses argument from find_targets
|
||||
:return:
|
||||
"""
|
||||
if addresses is None:
|
||||
data = _generate_request_payload(srvtype, True, xid)
|
||||
net4.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
|
||||
v6addrs = []
|
||||
v6hash = _v6mcasthash(srvtype)
|
||||
# do 'interface local' and 'link local'
|
||||
# it shouldn't make sense, but some configurations work with interface
|
||||
# local that do not work with link local
|
||||
v6addrs.append(('ff01::1:' + v6hash, 427, 0, 0))
|
||||
v6addrs.append(('ff02::1:' + v6hash, 427, 0, 0))
|
||||
for idx in util.list_interface_indexes():
|
||||
# IPv6 multicast is by index, so lead with that
|
||||
net.setsockopt(IPPROTO_IPV6, socket.IPV6_MULTICAST_IF, idx)
|
||||
for sa in v6addrs:
|
||||
try:
|
||||
net.sendto(data, sa)
|
||||
except socket.error:
|
||||
# if we hit an interface without ipv6 multicast,
|
||||
# this can cause an error, skip such an interface
|
||||
# case in point, 'lo'
|
||||
pass
|
||||
for i4 in util.list_ips():
|
||||
if 'broadcast' not in i4:
|
||||
continue
|
||||
addr = i4['addr']
|
||||
bcast = i4['broadcast']
|
||||
net4.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_IF,
|
||||
socket.inet_aton(addr))
|
||||
try:
|
||||
net4.sendto(data, ('239.255.255.253', 427))
|
||||
except socket.error as se:
|
||||
# On occasion, multicasting may be disabled
|
||||
# tolerate this scenario and move on
|
||||
if se.errno != 101:
|
||||
raise
|
||||
net4.sendto(data, (bcast, 427))
|
||||
|
||||
|
||||
def _grab_rsps(socks, rsps, interval, xidmap):
|
||||
r, _, _ = select.select(socks, (), (), interval)
|
||||
while r:
|
||||
for s in r:
|
||||
(rsp, peer) = s.recvfrom(9000)
|
||||
neighutil.refresh_neigh()
|
||||
_parse_slp_packet(rsp, peer, rsps, xidmap)
|
||||
r, _, _ = select.select(socks, (), (), interval)
|
||||
|
||||
|
||||
|
||||
def _parse_attrlist(attrstr):
|
||||
attribs = {}
|
||||
while attrstr:
|
||||
if attrstr[0] == '(':
|
||||
if ')' not in attrstr:
|
||||
attribs['INCOMPLETE'] = True
|
||||
return attribs
|
||||
currattr = attrstr[1:attrstr.index(')')]
|
||||
if '=' not in currattr: # Not allegedly kosher, but still..
|
||||
currattr = currattr.decode('utf-8')
|
||||
attribs[currattr] = None
|
||||
else:
|
||||
attrname, attrval = currattr.split('=')
|
||||
attrname = attrname.decode('utf-8')
|
||||
attribs[attrname] = []
|
||||
for val in attrval.split(','):
|
||||
try:
|
||||
val = val.decode('utf-8')
|
||||
except UnicodeDecodeError:
|
||||
val = '*DECODEERROR*'
|
||||
if val[:3] == '\\FF': # we should make this bytes
|
||||
finalval = bytearray([])
|
||||
for bnum in attrval[3:].split('\\'):
|
||||
if bnum == '':
|
||||
continue
|
||||
finalval.append(int(bnum, 16))
|
||||
val = finalval
|
||||
if 'uuid' in attrname and len(val) == 16:
|
||||
lebytes = struct.unpack_from(
|
||||
'<IHH', buffer(val[:8]))
|
||||
bebytes = struct.unpack_from(
|
||||
'>HHI', buffer(val[8:]))
|
||||
val = '{0:08X}-{1:04X}-{2:04X}-{3:04X}-' \
|
||||
'{4:04X}{5:08X}'.format(
|
||||
lebytes[0], lebytes[1], lebytes[2], bebytes[0],
|
||||
bebytes[1], bebytes[2]
|
||||
).lower()
|
||||
attribs[attrname].append(val)
|
||||
attrstr = attrstr[attrstr.index(')'):]
|
||||
elif attrstr[0] == ',':
|
||||
attrstr = attrstr[1:]
|
||||
elif ',' in attrstr:
|
||||
currattr = attrstr[:attrstr.index(',')]
|
||||
attribs[currattr] = None
|
||||
attrstr = attrstr[attrstr.index(','):]
|
||||
else:
|
||||
currattr = attrstr
|
||||
attribs[currattr] = None
|
||||
attrstr = None
|
||||
return attribs
|
||||
|
||||
|
||||
def _parse_attrs(data, parsed):
|
||||
headinfo = _parse_slp_header(data)
|
||||
if headinfo['function'] != 7 or headinfo['xid'] != parsed['xid']:
|
||||
return
|
||||
payload = headinfo['payload']
|
||||
if struct.unpack('!H', bytes(payload[:2]))[0] != 0:
|
||||
return
|
||||
length = struct.unpack('!H', bytes(payload[2:4]))[0]
|
||||
attrstr = bytes(payload[4:4+length])
|
||||
parsed['attributes'] = _parse_attrlist(attrstr)
|
||||
|
||||
|
||||
def _add_attributes(parsed):
|
||||
attrq = _generate_attr_request(parsed['services'][0], parsed['xid'])
|
||||
target = None
|
||||
# prefer reaching out to an fe80 if present, to be highly robust
|
||||
# in face of network changes
|
||||
for addr in parsed['addresses']:
|
||||
if addr[0].startswith('fe80'):
|
||||
target = addr
|
||||
# however if no fe80 seen, roll with the first available address
|
||||
if not target:
|
||||
target = parsed['addresses'][0]
|
||||
if len(target) == 4:
|
||||
net = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
|
||||
else:
|
||||
net = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
try:
|
||||
net.connect(target)
|
||||
except socket.error:
|
||||
return
|
||||
net.sendall(attrq)
|
||||
rsp = net.recv(8192)
|
||||
net.close()
|
||||
_parse_attrs(rsp, parsed)
|
||||
|
||||
|
||||
def query_srvtypes(target):
|
||||
"""Query the srvtypes advertised by the target
|
||||
|
||||
:param target: A sockaddr tuple (if you get the peer info)
|
||||
"""
|
||||
payload = b'\x00\x00\xff\xff\x00\x07DEFAULT'
|
||||
header = _generate_slp_header(payload, False, functionid=9, xid=1)
|
||||
packet = header + payload
|
||||
if len(target) == 2:
|
||||
net = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
elif len(target) == 4:
|
||||
net = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
|
||||
else:
|
||||
raise Exception('Unrecognized target {0}'.format(repr(target)))
|
||||
tries = 3
|
||||
connected = False
|
||||
while tries and not connected:
|
||||
tries -= 1
|
||||
try:
|
||||
net.connect(target)
|
||||
connected = True
|
||||
except socket.error:
|
||||
pass
|
||||
if not connected:
|
||||
return [u'']
|
||||
net.sendall(packet)
|
||||
rs = net.recv(8192)
|
||||
net.close()
|
||||
parsed = _parse_slp_header(rs)
|
||||
if parsed:
|
||||
payload = parsed['payload']
|
||||
if payload[:2] != '\x00\x00':
|
||||
return
|
||||
stypelen = struct.unpack('!H', bytes(payload[2:4]))[0]
|
||||
stypes = payload[4:4+stypelen].decode('utf-8')
|
||||
return stypes.split(',')
|
||||
|
||||
def rescan(handler):
|
||||
known_peers = set([])
|
||||
for scanned in scan():
|
||||
for addr in scanned['addresses']:
|
||||
ip = addr[0].partition('%')[0] # discard scope if present
|
||||
if ip not in neighutil.neightable:
|
||||
continue
|
||||
if addr in known_peers:
|
||||
break
|
||||
known_peers.add(addr)
|
||||
else:
|
||||
handler(scanned)
|
||||
|
||||
|
||||
def snoop(handler):
|
||||
"""Watch for SLP activity
|
||||
|
||||
handler will be called with a dictionary of relevant attributes
|
||||
|
||||
:param handler:
|
||||
:return:
|
||||
"""
|
||||
tracelog = log.Logger('trace')
|
||||
active_scan(handler)
|
||||
net = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
|
||||
net.setsockopt(IPPROTO_IPV6, socket.IPV6_V6ONLY, 1)
|
||||
slpg = socket.inet_pton(socket.AF_INET6, 'ff01::123')
|
||||
slpg2 = socket.inet_pton(socket.AF_INET6, 'ff02::123')
|
||||
for i6idx in util.list_interface_indexes():
|
||||
mreq = slpg + struct.pack('=I', i6idx)
|
||||
net.setsockopt(IPPROTO_IPV6, socket.IPV6_JOIN_GROUP, mreq)
|
||||
mreq = slpg2 + struct.pack('=I', i6idx)
|
||||
net.setsockopt(IPPROTO_IPV6, socket.IPV6_JOIN_GROUP, mreq)
|
||||
net4 = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
net.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
net4.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
for i4 in util.list_ips():
|
||||
if 'broadcast' not in i4:
|
||||
continue
|
||||
slpmcast = socket.inet_aton('239.255.255.253') + \
|
||||
socket.inet_aton(i4['addr'])
|
||||
try:
|
||||
net4.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP,
|
||||
slpmcast)
|
||||
except socket.error as e:
|
||||
if e.errno != 98:
|
||||
raise
|
||||
# socket in use can occur when aliased ipv4 are encountered
|
||||
net.bind(('', 427))
|
||||
net4.bind(('', 427))
|
||||
|
||||
while True:
|
||||
try:
|
||||
newmacs = set([])
|
||||
r, _, _ = select.select((net, net4), (), (), 60)
|
||||
# clear known_peers and peerbymacaddress
|
||||
# to avoid stale info getting in...
|
||||
# rely upon the select(0.2) to catch rapid fire and aggregate ip
|
||||
# addresses that come close together
|
||||
# calling code needs to understand deeper context, as snoop
|
||||
# will now yield dupe info over time
|
||||
known_peers = set([])
|
||||
peerbymacaddress = {}
|
||||
neighutil.update_neigh()
|
||||
while r:
|
||||
for s in r:
|
||||
(rsp, peer) = s.recvfrom(9000)
|
||||
ip = peer[0].partition('%')[0]
|
||||
if ip not in neighutil.neightable:
|
||||
continue
|
||||
if peer in known_peers:
|
||||
continue
|
||||
known_peers.add(peer)
|
||||
mac = neighutil.neightable[ip]
|
||||
if mac in peerbymacaddress:
|
||||
peerbymacaddress[mac]['addresses'].append(peer)
|
||||
else:
|
||||
q = query_srvtypes(peer)
|
||||
if not q or not q[0]:
|
||||
# SLP might have started and not ready yet
|
||||
# ignore for now
|
||||
known_peers.discard(peer)
|
||||
continue
|
||||
# we want to prioritize the very well known services
|
||||
svcs = []
|
||||
for svc in q:
|
||||
if svc in _slp_services:
|
||||
svcs.insert(0, svc)
|
||||
else:
|
||||
svcs.append(svc)
|
||||
peerbymacaddress[mac] = {
|
||||
'services': svcs,
|
||||
'addresses': [peer],
|
||||
}
|
||||
newmacs.add(mac)
|
||||
r, _, _ = select.select((net, net4), (), (), 0.2)
|
||||
for mac in newmacs:
|
||||
peerbymacaddress[mac]['xid'] = 1
|
||||
_add_attributes(peerbymacaddress[mac])
|
||||
peerbymacaddress[mac]['hwaddr'] = mac
|
||||
handler(peerbymacaddress[mac])
|
||||
except Exception as e:
|
||||
tracelog.log(traceback.format_exc(), ltype=log.DataTypes.event,
|
||||
event=log.Events.stacktrace)
|
||||
|
||||
|
||||
def active_scan(handler):
|
||||
known_peers = set([])
|
||||
for scanned in scan():
|
||||
for addr in scanned['addresses']:
|
||||
ip = addr[0].partition('%')[0] # discard scope if present
|
||||
if ip not in neighutil.neightable:
|
||||
continue
|
||||
if addr in known_peers:
|
||||
break
|
||||
known_peers.add(addr)
|
||||
else:
|
||||
handler(scanned)
|
||||
|
||||
|
||||
def scan(srvtypes=_slp_services, addresses=None, localonly=False):
|
||||
"""Find targets providing matching requested srvtypes
|
||||
|
||||
This is a generator that will iterate over respondants to the SrvType
|
||||
requested.
|
||||
|
||||
:param srvtypes: An iterable list of the service types to find
|
||||
:param addresses: An iterable of addresses/ranges. Default is to scan
|
||||
local network segment using multicast and broadcast.
|
||||
Each address can be a single address, hyphen-delimited
|
||||
range, or an IP/CIDR indication of a network.
|
||||
:return: Iterable set of results
|
||||
"""
|
||||
net = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
|
||||
net4 = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
# increase RCVBUF to max, mitigate chance of
|
||||
# failure due to full buffer.
|
||||
net.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 16777216)
|
||||
net4.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 16777216)
|
||||
# SLP is very poor at scanning large counts and managing it, so we
|
||||
# must make the best of it
|
||||
# Some platforms/config default to IPV6ONLY, we are doing IPv4
|
||||
# too, so force it
|
||||
#net.setsockopt(IPPROTO_IPV6, socket.IPV6_V6ONLY, 0)
|
||||
# we are going to do broadcast, so allow that...
|
||||
initxid = random.randint(0, 32768)
|
||||
xididx = 0
|
||||
xidmap = {}
|
||||
# First we give fast repsonders of each srvtype individual chances to be
|
||||
# processed, mitigating volume of response traffic
|
||||
rsps = {}
|
||||
for srvtype in srvtypes:
|
||||
xididx += 1
|
||||
_find_srvtype(net, net4, srvtype, addresses, initxid + xididx)
|
||||
xidmap[initxid + xididx] = srvtype
|
||||
_grab_rsps((net, net4), rsps, 0.1, xidmap)
|
||||
# now do a more slow check to work to get stragglers,
|
||||
# but fortunately the above should have taken the brunt of volume, so
|
||||
# reduced chance of many responses overwhelming receive buffer.
|
||||
_grab_rsps((net, net4), rsps, 1, xidmap)
|
||||
# now to analyze and flesh out the responses
|
||||
for id in rsps:
|
||||
if localonly:
|
||||
for addr in rsps[id]['addresses']:
|
||||
if 'fe80' in addr[0]:
|
||||
break
|
||||
else:
|
||||
continue
|
||||
_add_attributes(rsps[id])
|
||||
del rsps[id]['payload']
|
||||
del rsps[id]['function']
|
||||
del rsps[id]['xid']
|
||||
yield rsps[id]
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
def testsnoop(a):
|
||||
print(repr(a))
|
||||
snoop(testsnoop)
|
||||
@@ -0,0 +1,230 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 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.
|
||||
|
||||
# Documented somewhat at
|
||||
# http://buildingskb.schneider-electric.com/view.php?AID=15197
|
||||
|
||||
# Here is the payload of an SSDP 'announce', sent to the multicast v4/v6 1900
|
||||
# NOTIFY * HTTP/1.1
|
||||
# HOST: 239.255.255.250:1900
|
||||
# CACHE-CONTROL: max-age=1800
|
||||
# AL: https://172.30.254.151:8080/redfish/v1
|
||||
# SERVER: Linux/3.14.28-ltsi Redfish/1.0
|
||||
# NT: urn:dmtf-org:service:redfish-rest:1
|
||||
# USN: uuid:00000000-0000-0000-0005-000000000001::urn:dmtf-org:service:redfish-rest:1
|
||||
# NTS: ssdp:alive
|
||||
|
||||
|
||||
import confluent.neighutil as neighutil
|
||||
import confluent.util as util
|
||||
import eventlet.green.select as select
|
||||
import eventlet.green.socket as socket
|
||||
import struct
|
||||
|
||||
mcastv4addr = '239.255.255.250'
|
||||
mcastv6addr = 'ff02::c'
|
||||
|
||||
ssdp6mcast = socket.inet_pton(socket.AF_INET6, mcastv6addr)
|
||||
smsg = ('M-SEARCH * HTTP/1.1\r\n'
|
||||
'HOST: {0}:1900\r\n'
|
||||
'MAN: "ssdp:discover"\r\n'
|
||||
'ST: {1}\r\n'
|
||||
'MX: 3\r\n\r\n')
|
||||
|
||||
|
||||
def scan(services, target=None):
|
||||
for service in services:
|
||||
for rply in _find_service(service, target):
|
||||
yield rply
|
||||
|
||||
|
||||
def snoop(handler, byehandler=None):
|
||||
"""Watch for SSDP notify messages
|
||||
|
||||
The handler shall be called on any service coming online.
|
||||
byehandler is called whenever a system advertises that it is departing.
|
||||
If no byehandler is specified, byebye messages are ignored. The handler is
|
||||
given (as possible), the mac address, a list of viable sockaddrs to reference
|
||||
the peer, and the notification type (e.g.
|
||||
'urn:dmtf-org:service:redfish-rest:1'
|
||||
|
||||
:param handler: A handler for online notifications from network
|
||||
:param byehandler: Optional handler for devices going off the network
|
||||
"""
|
||||
# Normally, I like using v6/v4 agnostic socket. However, since we are
|
||||
# dabbling in multicast wizardry here, such sockets can cause big problems,
|
||||
# so we will have two distinct sockets
|
||||
known_peers = set([])
|
||||
net6 = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
|
||||
net6.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 1)
|
||||
for ifidx in util.list_interface_indexes():
|
||||
v6grp = ssdp6mcast + struct.pack('=I', ifidx)
|
||||
net6.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_JOIN_GROUP, v6grp)
|
||||
net6.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
net4 = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
for i4 in util.list_ips():
|
||||
ssdp4mcast = socket.inet_pton(socket.AF_INET, mcastv4addr) + \
|
||||
socket.inet_aton(i4['addr'])
|
||||
net4.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP,
|
||||
ssdp4mcast)
|
||||
net4.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
net4.bind(('', 1900))
|
||||
net6.bind(('', 1900))
|
||||
peerbymacaddress = {}
|
||||
while True:
|
||||
newmacs = set([])
|
||||
machandlers = {}
|
||||
r, _, _ = select.select((net4, net6), (), (), 60)
|
||||
neighutil.update_neigh()
|
||||
while r:
|
||||
for s in r:
|
||||
(rsp, peer) = s.recvfrom(9000)
|
||||
rsp = rsp.split('\r\n')
|
||||
method, _, _ = rsp[0].split(' ', 2)
|
||||
if method == 'NOTIFY':
|
||||
ip = peer[0].partition('%')[0]
|
||||
if ip not in neighutil.neightable:
|
||||
continue
|
||||
if peer in known_peers:
|
||||
continue
|
||||
mac = neighutil.neightable[ip]
|
||||
known_peers.add(peer)
|
||||
newmacs.add(mac)
|
||||
if mac in peerbymacaddress:
|
||||
peerbymacaddress[mac]['peers'].append(peer)
|
||||
else:
|
||||
peerbymacaddress[mac] = {
|
||||
'hwaddr': mac,
|
||||
'peers': [peer],
|
||||
}
|
||||
peerdata = peerbymacaddress[mac]
|
||||
for headline in rsp[1:]:
|
||||
if not headline:
|
||||
continue
|
||||
header, _, value = headline.partition(':')
|
||||
header = header.strip()
|
||||
value = value.strip()
|
||||
if header == 'NT':
|
||||
peerdata['service'] = value
|
||||
elif header == 'NTS':
|
||||
if value == 'ssdp:byebye':
|
||||
machandlers[mac] = byehandler
|
||||
elif value == 'ssdp:alive':
|
||||
machandlers[mac] = handler
|
||||
r, _, _ = select.select((net4, net6), (), (), 0.1)
|
||||
for mac in newmacs:
|
||||
thehandler = machandlers.get(mac, None)
|
||||
if thehandler:
|
||||
thehandler(peerbymacaddress[mac])
|
||||
|
||||
|
||||
def _find_service(service, target):
|
||||
net4 = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
net6 = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
|
||||
net6.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 1)
|
||||
if target:
|
||||
addrs = socket.getaddrinfo(target, 1900, 0, socket.SOCK_DGRAM)
|
||||
for addr in addrs:
|
||||
host = addr[4][0]
|
||||
if addr[0] == socket.AF_INET:
|
||||
net4.sendto(smsg.format(host, service), addr[4])
|
||||
elif addr[0] == socket.AF_INET6:
|
||||
host = '[{0}]'.format(host)
|
||||
net6.sendto(smsg.format(host, service), addr[4])
|
||||
else:
|
||||
net4.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
|
||||
for idx in util.list_interface_indexes():
|
||||
net6.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_MULTICAST_IF,
|
||||
idx)
|
||||
try:
|
||||
net6.sendto(smsg.format('[{0}]'.format(mcastv6addr), service
|
||||
), (mcastv6addr, 1900, 0, 0))
|
||||
except socket.error:
|
||||
# ignore interfaces without ipv6 multicast causing error
|
||||
pass
|
||||
for i4 in util.list_ips():
|
||||
if 'broadcast' not in i4:
|
||||
continue
|
||||
addr = i4['addr']
|
||||
bcast = i4['broadcast']
|
||||
net4.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_IF,
|
||||
socket.inet_aton(addr))
|
||||
net4.sendto(smsg.format(mcastv4addr, service),
|
||||
(mcastv4addr, 1900))
|
||||
net4.sendto(smsg.format(bcast, service), (bcast, 1900))
|
||||
# SSDP by spec encourages responses to spread out over a 3 second interval
|
||||
# hence we must be a bit more patient
|
||||
r, _, _ = select.select((net4, net6), (), (), 4)
|
||||
peerdata = {}
|
||||
while r:
|
||||
for s in r:
|
||||
(rsp, peer) = s.recvfrom(9000)
|
||||
neighutil.refresh_neigh()
|
||||
_parse_ssdp(peer, rsp, peerdata)
|
||||
r, _, _ = select.select((net4, net6), (), (), 4)
|
||||
for nid in peerdata:
|
||||
yield peerdata[nid]
|
||||
|
||||
|
||||
def _parse_ssdp(peer, rsp, peerdata):
|
||||
ip = peer[0].partition('%')[0]
|
||||
nid = ip
|
||||
mac = None
|
||||
if ip in neighutil.neightable:
|
||||
nid = neighutil.neightable[ip]
|
||||
mac = nid
|
||||
headlines = rsp.split('\r\n')
|
||||
try:
|
||||
_, code, _ = headlines[0].split(' ', 2)
|
||||
except ValueError:
|
||||
return
|
||||
if code == '200':
|
||||
if nid in peerdata:
|
||||
peerdatum = peerdata[nid]
|
||||
else:
|
||||
peerdatum = {
|
||||
'peers': [peer],
|
||||
'hwaddr': mac,
|
||||
}
|
||||
peerdata[nid] = peerdatum
|
||||
for headline in headlines[1:]:
|
||||
if not headline:
|
||||
continue
|
||||
header, _, value = headline.partition(':')
|
||||
header = header.strip()
|
||||
value = value.strip()
|
||||
if header == 'AL' or header == 'LOCATION':
|
||||
if 'urls' not in peerdatum:
|
||||
peerdatum['urls'] = [value]
|
||||
elif value not in peerdatum['urls']:
|
||||
peerdatum['urls'].append(value)
|
||||
elif header == 'ST':
|
||||
if 'services' not in peerdatum:
|
||||
peerdatum['services'] = [value]
|
||||
elif value not in peerdatum['services']:
|
||||
peerdatum['services'].append(value)
|
||||
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
for rsp in scan(['urn:dmtf-org:service:redfish-rest:1']):
|
||||
print(repr(rsp))
|
||||
def fun(a):
|
||||
print(repr(a))
|
||||
def byefun(a):
|
||||
print('bye' + repr(a))
|
||||
snoop(fun, byefun)
|
||||
@@ -1,7 +1,7 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2014 IBM Corporation
|
||||
# Copyright 2015 Lenovo
|
||||
# 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.
|
||||
@@ -21,73 +21,92 @@ import json
|
||||
|
||||
class ConfluentException(Exception):
|
||||
apierrorcode = 500
|
||||
apierrorstr = 'Unexpected Error'
|
||||
_apierrorstr = 'Unexpected Error'
|
||||
|
||||
def get_error_body(self):
|
||||
errstr = ' - '.join((self.apierrorstr, str(self)))
|
||||
errstr = ' - '.join((self._apierrorstr, str(self)))
|
||||
return json.dumps({'error': errstr })
|
||||
|
||||
@property
|
||||
def apierrorstr(self):
|
||||
if str(self):
|
||||
return self._apierrorstr + ' - ' + str(self)
|
||||
return self._apierrorstr
|
||||
|
||||
|
||||
class NotFoundException(ConfluentException):
|
||||
# Something that could be construed as a name was not found
|
||||
# basically, picture an http error code 404
|
||||
apierrorcode = 404
|
||||
apierrorstr = 'Request path not recognized'
|
||||
_apierrorstr = 'Target not found'
|
||||
|
||||
|
||||
class InvalidArgumentException(ConfluentException):
|
||||
# Something from the remote client wasn't correct
|
||||
# like http code 400
|
||||
apierrorcode = 400
|
||||
apierrorstr = 'Bad Request'
|
||||
_apierrorstr = 'Bad Request'
|
||||
|
||||
|
||||
class TargetEndpointUnreachable(ConfluentException):
|
||||
# A target system was unavailable. For example, a BMC
|
||||
# was unreachable. http code 504
|
||||
apierrorcode = 504
|
||||
apierrorstr = 'Unreachable Target'
|
||||
_apierrorstr = 'Unreachable Target'
|
||||
|
||||
|
||||
class TargetEndpointBadCredentials(ConfluentException):
|
||||
# target was reachable, but authentication/authorization
|
||||
# failed
|
||||
apierrorcode = 502
|
||||
apierrorstr = 'Bad Credentials'
|
||||
_apierrorstr = 'Bad Credentials'
|
||||
|
||||
|
||||
class LockedCredentials(ConfluentException):
|
||||
# A request was performed that required a credential, but the credential
|
||||
# store is locked
|
||||
apierrorstr = 'Credential store locked'
|
||||
_apierrorstr = 'Credential store locked'
|
||||
|
||||
|
||||
class DegradedCollective(ConfluentException):
|
||||
# We are in a collective with at least half of the member nodes missing
|
||||
_apierrorstr = 'Collective does not have quorum'
|
||||
|
||||
|
||||
class ForbiddenRequest(ConfluentException):
|
||||
# The client request is not allowed by authorization engine
|
||||
apierrorcode = 403
|
||||
apierrorstr = 'Forbidden'
|
||||
_apierrorstr = 'Forbidden'
|
||||
|
||||
|
||||
class NotImplementedException(ConfluentException):
|
||||
# The current configuration/plugin is unable to perform
|
||||
# the requested task. http code 501
|
||||
apierrorcode = 501
|
||||
apierrorstr = '501 - Not Implemented'
|
||||
_apierrorstr = '501 - Not Implemented'
|
||||
|
||||
|
||||
|
||||
class GlobalConfigError(ConfluentException):
|
||||
# The configuration in the global config file is not right
|
||||
apierrorstr = 'Global configuration contains an error'
|
||||
_apierrorstr = 'Global configuration contains an error'
|
||||
|
||||
|
||||
class TargetResourceUnavailable(ConfluentException):
|
||||
# This is meant for scenarios like asking to read a sensor that is
|
||||
# currently unavailable. This may be a persistent or transient state
|
||||
apierrocode = 503
|
||||
_apierrorstr = 'Target Resource Unavailable'
|
||||
|
||||
class PubkeyInvalid(ConfluentException):
|
||||
apierrorcode = 502
|
||||
apierrorstr = '502 - Invalid certificate or key on target'
|
||||
_apierrorstr = '502 - Invalid certificate or key on target'
|
||||
|
||||
def __init__(self, text, certificate, fingerprint, attribname, event):
|
||||
super(PubkeyInvalid, self).__init__(self, text)
|
||||
self.fingerprint = fingerprint
|
||||
self.attrname = attribname
|
||||
self.message = text
|
||||
bodydata = {'message': text,
|
||||
'event': event,
|
||||
'fingerprint': fingerprint,
|
||||
@@ -100,7 +119,7 @@ class PubkeyInvalid(ConfluentException):
|
||||
|
||||
class LoggedOut(ConfluentException):
|
||||
apierrorcode = 401
|
||||
apierrorstr = '401 - Logged out'
|
||||
_apierrorstr = '401 - Logged out'
|
||||
|
||||
def get_error_body(self):
|
||||
return '{"loggedout": 1}'
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
|
||||
# Copyright 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.
|
||||
|
||||
# provide managing firmware update process and firmware repository if/when
|
||||
# the time comes
|
||||
|
||||
import confluent.exceptions as exc
|
||||
import confluent.messages as msg
|
||||
import eventlet
|
||||
import os
|
||||
import pwd
|
||||
import socket
|
||||
|
||||
updatesbytarget = {}
|
||||
uploadsbytarget = {}
|
||||
downloadsbytarget = {}
|
||||
updatepool = eventlet.greenpool.GreenPool(256)
|
||||
|
||||
|
||||
def execupdate(handler, filename, updateobj, type, owner, node):
|
||||
if type != 'ffdc' and not os.path.exists(filename):
|
||||
errstr = '{0} does not appear to exist on {1}'.format(
|
||||
filename, socket.gethostname())
|
||||
updateobj.handle_progress({'phase': 'error', 'progress': 0.0,
|
||||
'detail': errstr})
|
||||
return
|
||||
if type == 'ffdc' and os.path.isdir(filename):
|
||||
filename += '/' + node + '.svcdata'
|
||||
try:
|
||||
if type == 'firmware':
|
||||
completion = handler(filename, progress=updateobj.handle_progress,
|
||||
bank=updateobj.bank)
|
||||
else:
|
||||
completion = handler(filename, progress=updateobj.handle_progress)
|
||||
if completion is None:
|
||||
completion = 'complete'
|
||||
if owner:
|
||||
pwent = pwd.getpwnam(owner)
|
||||
os.chown(filename, pwent.pw_uid, pwent.pw_gid)
|
||||
updateobj.handle_progress({'phase': completion, 'progress': 100.0})
|
||||
except exc.PubkeyInvalid as pi:
|
||||
errstr = 'Certificate mismatch detected, does not match value in ' \
|
||||
'attribute {0}'.format(pi.attrname)
|
||||
updateobj.handle_progress({'phase': 'error', 'progress': 0.0,
|
||||
'detail': errstr})
|
||||
except Exception as e:
|
||||
updateobj.handle_progress({'phase': 'error', 'progress': 0.0,
|
||||
'detail': str(e)})
|
||||
|
||||
class Updater(object):
|
||||
def __init__(self, node, handler, filename, tenant=None, name=None,
|
||||
bank=None, type='firmware', owner=None):
|
||||
self.bank = bank
|
||||
self.node = node
|
||||
self.phase = 'initializing'
|
||||
self.detail = ''
|
||||
self.percent = 0.0
|
||||
self.updateproc = updatepool.spawn(execupdate, handler, filename,
|
||||
self, type, owner, node)
|
||||
if type == 'firmware':
|
||||
myparty = updatesbytarget
|
||||
elif type == 'mediaupload':
|
||||
myparty = uploadsbytarget
|
||||
elif type == 'ffdc':
|
||||
myparty = downloadsbytarget
|
||||
if (node, tenant) not in myparty:
|
||||
myparty[(node, tenant)] = {}
|
||||
if name is None:
|
||||
name = 1
|
||||
while '{0}'.format(name) in myparty[(node, tenant)]:
|
||||
name += 1
|
||||
self.name = '{0}'.format(name)
|
||||
myparty[(node, tenant)][self.name] = self
|
||||
|
||||
def handle_progress(self, progress):
|
||||
self.phase = progress.get('phase', 'unknown')
|
||||
self.percent = float(progress.get('progress', 100.0))
|
||||
self.detail = progress.get('detail', '')
|
||||
|
||||
def cancel(self):
|
||||
self.updateproc.kill()
|
||||
|
||||
@property
|
||||
def progress(self):
|
||||
return {'phase': self.phase, 'progress': self.percent,
|
||||
'detail': self.detail}
|
||||
|
||||
|
||||
def remove_updates(nodes, tenant, element, type='firmware'):
|
||||
if len(element) < 5 and element[:2] != ['media', 'uploads']:
|
||||
raise exc.InvalidArgumentException()
|
||||
upid = element[-1]
|
||||
if type == 'firmware':
|
||||
myparty = updatesbytarget
|
||||
elif type == 'ffdc':
|
||||
myparty = downloadsbytarget
|
||||
else:
|
||||
myparty = uploadsbytarget
|
||||
for node in nodes:
|
||||
try:
|
||||
upd = myparty[(node, tenant)][upid]
|
||||
except KeyError:
|
||||
raise exc.NotFoundException('No active update matches request')
|
||||
upd.cancel()
|
||||
del myparty[(node, tenant)][upid]
|
||||
yield msg.DeletedResource(
|
||||
'nodes/{0}/inventory/firmware/updates/active/{1}'.format(
|
||||
node, upid))
|
||||
|
||||
|
||||
def list_updates(nodes, tenant, element, type='firmware'):
|
||||
showmode = False
|
||||
if type == 'mediaupload':
|
||||
myparty = uploadsbytarget
|
||||
verb = 'upload'
|
||||
elif type == 'ffdc':
|
||||
verb = 'download'
|
||||
myparty = downloadsbytarget
|
||||
else:
|
||||
myparty = updatesbytarget
|
||||
verb = 'update'
|
||||
if type == 'firmware':
|
||||
specificlen = 4
|
||||
else:
|
||||
specificlen = 2
|
||||
if len(element) > specificlen:
|
||||
showmode = True
|
||||
upid = element[-1]
|
||||
for node in nodes:
|
||||
if showmode:
|
||||
try:
|
||||
updater = myparty[(node, tenant)][upid]
|
||||
except KeyError:
|
||||
raise exc.NotFoundException(
|
||||
'No matching {0} process found'.format(verb))
|
||||
yield msg.KeyValueData(updater.progress, name=node)
|
||||
else:
|
||||
for updateid in myparty.get((node, tenant), {}):
|
||||
yield msg.ChildCollection(updateid)
|
||||
@@ -0,0 +1,119 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 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.
|
||||
|
||||
#This handles port forwarding for web interfaces on management devices
|
||||
#It will also hijack port 3900 and do best effort..
|
||||
|
||||
import eventlet
|
||||
import eventlet.green.select as select
|
||||
import eventlet.green.socket as socket
|
||||
forwardersbyclient = {}
|
||||
relaysbysession = {}
|
||||
sessionsbyip = {}
|
||||
ipsbysession = {}
|
||||
sockhandler = {}
|
||||
vidtargetbypeer = {}
|
||||
vidforwarder = None
|
||||
|
||||
def handle_connection(incoming, outgoing):
|
||||
while True:
|
||||
r, _, _ = select.select((incoming, outgoing), (), (), 60)
|
||||
for mysock in r:
|
||||
data = mysock.recv(32768)
|
||||
if not data:
|
||||
return
|
||||
if mysock == incoming:
|
||||
outgoing.sendall(data)
|
||||
elif mysock == outgoing:
|
||||
incoming.sendall(data)
|
||||
|
||||
|
||||
def forward_port(sock, target, clientip, sessionid):
|
||||
while True:
|
||||
conn, cli = sock.accept()
|
||||
if cli[0] != clientip:
|
||||
conn.close()
|
||||
continue
|
||||
try:
|
||||
client = socket.create_connection((target, 443))
|
||||
except Exception:
|
||||
conn.close()
|
||||
continue
|
||||
if sessionid not in relaysbysession:
|
||||
relaysbysession[sessionid] = {}
|
||||
relaysbysession[sessionid][eventlet.spawn(
|
||||
handle_connection, conn, client)] = conn
|
||||
|
||||
|
||||
def forward_video():
|
||||
sock = eventlet.listen(('::', 3900, 0, 0), family=socket.AF_INET6)
|
||||
while True:
|
||||
conn, cli = sock.accept()
|
||||
if cli[0] not in vidtargetbypeer or not sessionsbyip.get(cli[0], None):
|
||||
conn.close()
|
||||
continue
|
||||
try:
|
||||
vidclient = socket.create_connection((vidtargetbypeer[cli[0]],
|
||||
3900))
|
||||
except Exception:
|
||||
conn.close()
|
||||
continue
|
||||
eventlet.spawn_n(handle_connection, conn, vidclient)
|
||||
|
||||
|
||||
def close_session(sessionid):
|
||||
for addr in forwardersbyclient.get(sessionid, []):
|
||||
killsock = forwardersbyclient[sessionid][addr]
|
||||
sockhandler[killsock].kill()
|
||||
del sockhandler[killsock]
|
||||
killsock.close()
|
||||
if sessionid in forwardersbyclient:
|
||||
del forwardersbyclient[sessionid]
|
||||
for clip in ipsbysession.get(sessionid, ()):
|
||||
sessionsbyip[clip].discard(sessionid)
|
||||
if sessionid in ipsbysession:
|
||||
del ipsbysession[sessionid]
|
||||
for relay in list(relaysbysession.get(sessionid, ())):
|
||||
conn = relaysbysession[sessionid][relay]
|
||||
relay.kill()
|
||||
conn.close()
|
||||
if sessionid in relaysbysession:
|
||||
del relaysbysession[sessionid]
|
||||
|
||||
|
||||
def get_port(addr, clientip, sessionid):
|
||||
global vidforwarder
|
||||
if socket.getaddrinfo(clientip, 0)[0][0] == socket.AF_INET:
|
||||
clientip = '::ffff:' + clientip
|
||||
if sessionid not in ipsbysession:
|
||||
ipsbysession[sessionid] = set([])
|
||||
if clientip not in sessionsbyip:
|
||||
sessionsbyip[clientip] = set([])
|
||||
sessionsbyip[clientip].add(sessionid)
|
||||
ipsbysession[sessionid].add(clientip)
|
||||
if sessionid not in forwardersbyclient:
|
||||
forwardersbyclient[sessionid] = {}
|
||||
if addr not in forwardersbyclient[sessionid]:
|
||||
newsock = eventlet.listen(('::', 0, 0, 0),
|
||||
family=socket.AF_INET6)
|
||||
forwardersbyclient[sessionid][addr] = newsock
|
||||
sockhandler[newsock] = eventlet.spawn(forward_port, newsock, addr,
|
||||
clientip, sessionid)
|
||||
if not vidforwarder:
|
||||
vidforwarder = eventlet.spawn(forward_video)
|
||||
vidtargetbypeer[clientip] = addr
|
||||
return forwardersbyclient[sessionid][addr].getsockname()[1]
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user