mirror of
https://github.com/xcat2/confluent.git
synced 2026-09-29 08:41:00 +00:00
Compare commits
557 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5c288a27dd | |||
| d9be6ae2e9 | |||
| 59789bae7d | |||
| 521be5d44d | |||
| 6fb82bbbad | |||
| 90e546bcac | |||
| 147d59cba7 | |||
| 6a6fd3184e | |||
| c532cf9ecf | |||
| 8909fb16d6 | |||
| 44d6bde3ff | |||
| 56fa13279e | |||
| 5008128d57 | |||
| ed320f4a17 | |||
| de8292f6dd | |||
| 8e071a2568 | |||
| 7d84d0a021 | |||
| c5f75bfa15 | |||
| 99d01d707f | |||
| 3e1690c860 | |||
| a3f5630535 | |||
| 8d6744947e | |||
| 21b3c89974 | |||
| 9718881c7b | |||
| fcbbdc8a8d | |||
| f03bb36dbe | |||
| 97a950b145 | |||
| a0e445d1b1 | |||
| 65629548c9 | |||
| 6cfbf4533c | |||
| 5794dd7f8c | |||
| 48fa74e7a9 | |||
| 59d5110b8f | |||
| 051d79727b | |||
| 4cdbc7807b | |||
| feecee82db | |||
| 395c0d4697 | |||
| a963a8ca35 | |||
| d95464df6f | |||
| 3f6e6d4c39 | |||
| c3176ab86a | |||
| 4dab5fc527 | |||
| 8897842fc4 | |||
| a251a538b0 | |||
| 480a747dcf | |||
| af025f7304 | |||
| 21edd82177 | |||
| 8641885f86 | |||
| 64cc2416d1 | |||
| 2787e1d862 | |||
| 514a121c15 | |||
| 00ce48b046 | |||
| 44929e7975 | |||
| da82fef0cb | |||
| efcac0b181 | |||
| 46e2f53018 | |||
| cf51928b3d | |||
| 151ba2e567 | |||
| bc87077397 | |||
| a77b65737e | |||
| 19c2963cf9 | |||
| a0ea8eeae3 | |||
| 6ad1ce4df5 | |||
| c53264872a | |||
| d9f2c0b266 | |||
| d528d45820 | |||
| 4eeac8d71a | |||
| 6cc0eb0797 | |||
| bfd0de1a4a | |||
| a787ac62c3 | |||
| fd9b4a8650 | |||
| 373bf3dca7 | |||
| 0ad0c626c2 | |||
| fbc4fc6846 | |||
| 3efc153615 | |||
| b7ff093e48 | |||
| 7275e98039 | |||
| 2b0c50dc23 | |||
| 54439d5f18 | |||
| 65b4cbe8cc | |||
| c8931ae6e7 | |||
| 083f5c8654 | |||
| 8bbeeafa49 | |||
| 422f210f74 | |||
| 2e6029bd2c | |||
| 81c0adbbe3 | |||
| 6d5f0cdb16 | |||
| c633286019 | |||
| ba113d6445 | |||
| d2efb16c71 | |||
| 739e302506 | |||
| ae181b7753 | |||
| b76b415a6e | |||
| ef2b324eed | |||
| ffe9606de1 | |||
| 8ed2d5a551 | |||
| 3501b3c347 | |||
| e55314d759 | |||
| 27410a9b6b | |||
| 2db01746d5 | |||
| 208be0beef | |||
| d34f8af798 | |||
| b2013e93c5 | |||
| 2a72a6184d | |||
| 7e4dcfa99c | |||
| ee82831370 | |||
| 0869669ef6 | |||
| 6a77a13539 | |||
| 56e9a67ef8 | |||
| 52d5eb9876 | |||
| b819a488f1 | |||
| 3fc31f7332 | |||
| 21c3579287 | |||
| 4a094f669e | |||
| 67eecffd29 | |||
| e288e8bad5 | |||
| a8cad7a70f | |||
| 6de605c298 | |||
| e09c2ed8eb | |||
| cd5366e73f | |||
| 509f8c30d5 | |||
| c63c8076bb | |||
| 6800c8055c | |||
| ffc55b1594 | |||
| 481342340e | |||
| d33c6be758 | |||
| 44f3630cf5 | |||
| 3eaba23e6f | |||
| 5ac0a6e650 | |||
| 9ac83665c6 | |||
| 30f9d28c2c | |||
| 0168e46f24 | |||
| 067e99d6ce | |||
| ad828e609d | |||
| cc5a5c9972 | |||
| cd2361b80b | |||
| c042583a64 | |||
| e32d3cf4cc | |||
| 2b86c878a8 | |||
| 3564de8c6d | |||
| 7b5361a019 | |||
| 65e1dfcc57 | |||
| ba039e9e3e | |||
| a6809aae98 | |||
| 4d5bfb13bf | |||
| 93e9a54e86 | |||
| 25028c8acc | |||
| 906c671d90 | |||
| 8fbd99cf5c | |||
| c86ac2885f | |||
| 952fa3d022 | |||
| 90e0f93d37 | |||
| d78adc334d | |||
| 31f2161b57 | |||
| 571a34cba2 | |||
| 52fa5158f6 | |||
| 907f66ae8b | |||
| c8c275f804 | |||
| 7a08fee4b5 | |||
| 36f0d888cd | |||
| 81451a6451 | |||
| 02eb195e3f | |||
| 87e7a90c37 | |||
| bafc25005f | |||
| 33c1137ccf | |||
| abfeef5a0a | |||
| e81579f414 | |||
| 47edb1dbd1 | |||
| 6a8cb8deaa | |||
| c6516f9d62 | |||
| 72448aa0b4 | |||
| 6290c169f5 | |||
| e5bbd226ff | |||
| 037ed43c70 | |||
| 0a816acf4f | |||
| 2c9c778ca7 | |||
| bf005eace6 | |||
| 34c6d6a4d7 | |||
| b402ddd656 | |||
| 1ae055fa8f | |||
| 89cf255ae7 | |||
| c070148aed | |||
| d6097ca706 | |||
| c3eed19309 | |||
| 40dbe63336 | |||
| d6ecee955b | |||
| d7d3ae344c | |||
| 5c4944a1e4 | |||
| 06b31f4845 | |||
| 7fecd0ac5c | |||
| 36d5d60edc | |||
| 6e26b19c67 | |||
| 16430e1ec9 | |||
| 5d572f17f9 | |||
| ae49cf290e | |||
| 5ead803c8a | |||
| 7232a0c1b3 | |||
| dce25d802e | |||
| 835d1fc0ab | |||
| c28a963d62 | |||
| 9fd091daad | |||
| 3c21ca8739 | |||
| 996b1ba45b | |||
| 4af1f998fb | |||
| b2c1137321 | |||
| d484e9db43 | |||
| 6397709e47 | |||
| e0c0f0f1f3 | |||
| ca29f6ae35 | |||
| 2c8681a9f3 | |||
| b927572872 | |||
| b5df380ee4 | |||
| 5c61430ccc | |||
| 2b275cd369 | |||
| f79dac7bd2 | |||
| fc5f16fb01 | |||
| 907d25164f | |||
| 0b85fab529 | |||
| adb4ce919e | |||
| 9379c85d0e | |||
| 11ffa7a091 | |||
| bacba8972a | |||
| 5404497e70 | |||
| 70690517de | |||
| a3162daf62 | |||
| 8c886b751c | |||
| 69630edfa9 | |||
| cdce1f1833 | |||
| 48079f297b | |||
| bf24d0f501 | |||
| 1d6111b8dd | |||
| e59d237d11 | |||
| fb3dc9a200 | |||
| b0d2d44b75 | |||
| f1e83d938b | |||
| e643b7ed7d | |||
| 163b29a07c | |||
| c5fa0bfd79 | |||
| a258653186 | |||
| 656e82c3fe | |||
| 898ff065e0 | |||
| 779b5c9ede | |||
| 5be08ddb1d | |||
| 16abf7cb64 | |||
| 269acf9943 | |||
| c649ae5fec | |||
| c3f2e131b2 | |||
| 4124e0fcc0 | |||
| 0e2e6267cd | |||
| e8119d330d | |||
| 5ae6717709 | |||
| 0cd94447f7 | |||
| ce4f8c1837 | |||
| 8ad06f79e7 | |||
| 5481da269e | |||
| 6ea307d415 | |||
| 59aa23b2f5 | |||
| 4446308030 | |||
| 7703c6c2ab | |||
| f5b6d434f3 | |||
| 8ce5a7dccf | |||
| 23c9e6315a | |||
| 38f9583be3 | |||
| 1b355ec468 | |||
| 2bbf4b9e98 | |||
| 1248894cf3 | |||
| c43365d2dd | |||
| 4e7c098e75 | |||
| ef6c89b883 | |||
| 99c06813d9 | |||
| 686b59c2b4 | |||
| 46b909c291 | |||
| 9abb163c7e | |||
| 7e25dd805f | |||
| 31220292e5 | |||
| 161cf37f46 | |||
| ad64cda249 | |||
| db812ac292 | |||
| a322118877 | |||
| ebfbbcca23 | |||
| 275525d3f3 | |||
| 938a6e44df | |||
| ca6b203a09 | |||
| c478cb5d6e | |||
| ca9e7d1d93 | |||
| 2691722f48 | |||
| e194222553 | |||
| 8f611f0e59 | |||
| 1fdcf19563 | |||
| add1a1b32a | |||
| 8abe384e1a | |||
| 14577be963 | |||
| e6b8d0dabc | |||
| b1a91ad409 | |||
| 996fd82920 | |||
| 5e6c66826f | |||
| 22d79867c8 | |||
| 52d25d563b | |||
| 68eeb95ea3 | |||
| 95d5ff6a4c | |||
| b42114bea0 | |||
| e0877bc0b1 | |||
| 5289d34206 | |||
| f7f8247d02 | |||
| 57e23a6f52 | |||
| 73b234d29e | |||
| bfe55e276d | |||
| 44bcca99b6 | |||
| 4cb595684e | |||
| b153a14ff3 | |||
| b620838189 | |||
| 4540354ff2 | |||
| 74963a73cc | |||
| 9fe200b525 | |||
| b07d4e9736 | |||
| f649efa110 | |||
| 521013e50a | |||
| 25c8f93336 | |||
| 59f00dd10b | |||
| 2e93af9b5e | |||
| 337ab3b1a0 | |||
| f4cf74b699 | |||
| 085981f74c | |||
| 8a5f1c6dc5 | |||
| 09cb6963f0 | |||
| 188feec0b4 | |||
| 1902a333ae | |||
| f6c46ddcb8 | |||
| e23253815c | |||
| d979d29b0b | |||
| bca676ed15 | |||
| deed8b4b9b | |||
| 2c94a10e23 | |||
| 299181223e | |||
| 1378b01feb | |||
| 18713797b9 | |||
| 6588ac0990 | |||
| efd13c960f | |||
| 4f14de0136 | |||
| 6cecf6635c | |||
| 5b7807fe3d | |||
| 5f52ee9578 | |||
| 88275f5110 | |||
| 55c5bff0f9 | |||
| 3782127590 | |||
| 74355bd6f9 | |||
| bc995520b7 | |||
| 2d63e68494 | |||
| ceba3ff34f | |||
| 334ec00f3f | |||
| 35380e5bac | |||
| 66471624bc | |||
| 8a03bc48de | |||
| 5831be091a | |||
| 8197c750bb | |||
| a9f0312acd | |||
| 21700e914a | |||
| 5e15ae8e30 | |||
| 5eef76f5d3 | |||
| df72bfa715 | |||
| a6333459c4 | |||
| 075891bf74 | |||
| a937511354 | |||
| 22503e7e11 | |||
| 01d73308ed | |||
| cb16385ddb | |||
| 853585f942 | |||
| 53996a1e9d | |||
| 558171124c | |||
| 05d028e9e9 | |||
| b0a3bb18fe | |||
| 7207013abc | |||
| b511a02f20 | |||
| c60cb3a027 | |||
| 9f137fa6d4 | |||
| aa92898249 | |||
| 2cb694b374 | |||
| 24fd1e72e1 | |||
| 5b21a5b2c3 | |||
| c8ad94313e | |||
| f71d51769b | |||
| b5213e6972 | |||
| 8d72f4d64a | |||
| fcd46c7b72 | |||
| 6efb8ff1b7 | |||
| c227c2353b | |||
| cb2133e871 | |||
| c56c1948a9 | |||
| 34b30a1bd8 | |||
| 0e7bfe4f40 | |||
| a93a759b72 | |||
| cba514692c | |||
| 2bb0a70a24 | |||
| 1b84cd43fe | |||
| 39839a9f2a | |||
| 8473cb1cf8 | |||
| 1a10b5d747 | |||
| 82a7012527 | |||
| 199a948b33 | |||
| 6fc305d6a0 | |||
| 75b6221dcf | |||
| bd5032268e | |||
| 0aa0289b06 | |||
| 96df7871cb | |||
| 82ee69b56c | |||
| 37899ad39e | |||
| 43fb4467b7 | |||
| f5f9403eff | |||
| 78e7381442 | |||
| 005cefc914 | |||
| 5f38cce51e | |||
| 5d358eaeb0 | |||
| 115a6e9dd1 | |||
| a034d05cc8 | |||
| 6378f823f3 | |||
| 079dfed11e | |||
| d836bf7298 | |||
| 25b969a4db | |||
| 950abde20e | |||
| 37be696f23 | |||
| 73cab3774d | |||
| c2d52d4f83 | |||
| fd17559a73 | |||
| 3945ccd5c3 | |||
| 7c006b33bc | |||
| c96b5f0270 | |||
| 7e72240d98 | |||
| c150a848fd | |||
| bc7a197a8c | |||
| 6973736c6a | |||
| d70c3dcad7 | |||
| f9055a258e | |||
| 76a96fa054 | |||
| c784a4ec9b | |||
| 2ff47b886d | |||
| 51037e2487 | |||
| 614b916ca4 | |||
| 4077346d30 | |||
| 8a4bf22a7e | |||
| f2dd501de9 | |||
| f835057ae4 | |||
| 48913218a6 | |||
| a11e4b71ce | |||
| e9ba49a4aa | |||
| deb90fbca9 | |||
| 8cae5ea101 | |||
| f830658818 | |||
| 3105b9b1f9 | |||
| f525c25ba6 | |||
| 3012de1fe4 | |||
| be930fc076 | |||
| 2d0199a4e9 | |||
| 6b70a4322a | |||
| 6a784e3a1c | |||
| 3b2b96a4cf | |||
| 32ddb33de3 | |||
| b77ed8dbff | |||
| d5c093a30d | |||
| cf9d2a43e8 | |||
| 2f566fb81d | |||
| 92a3b02dc5 | |||
| 94c8cf3ff2 | |||
| 049897fc15 | |||
| 8741a27c24 | |||
| 1c494fc4fc | |||
| 1ee418392f | |||
| 2a7eeb6e08 | |||
| 5c83c78a90 | |||
| 6a466b0100 | |||
| 67b93ee6b0 | |||
| 6b4a21d613 | |||
| 5f46899358 | |||
| a3bc17b465 | |||
| 20a37f8db5 | |||
| c6b8aaf372 | |||
| 5baab5bef4 | |||
| 73c06fd25e | |||
| 8d9a082739 | |||
| 32602fbba3 | |||
| 2f616d4586 | |||
| 15dc4937ee | |||
| 10cb1b77dd | |||
| d86e1fc4eb | |||
| 29fcd59f8b | |||
| 78a1741e0e | |||
| 4329c1d388 | |||
| c13ebe36ae | |||
| 9495a41f85 | |||
| b0b5493ff7 | |||
| 484435c732 | |||
| 326f56219b | |||
| e098c0ba91 | |||
| a5d5f4a6a4 | |||
| 61e7c90ad1 | |||
| 51194b275b | |||
| e57cdf9a7b | |||
| e634817b34 | |||
| 10ce7a9de9 | |||
| 0724ad812b | |||
| a3b0b0240d | |||
| 7f06c69d3c | |||
| b5af770f21 | |||
| 247650868e | |||
| 9e8b2dd973 | |||
| 99fdb20f87 | |||
| 60bb4c89fb | |||
| 5d52fd2fc1 | |||
| 18bebde337 | |||
| c68c4d8cf7 | |||
| 1de84f0417 | |||
| 44bf2872b7 | |||
| c209010126 | |||
| 21b4a2f6f3 | |||
| 36fc23d692 | |||
| f601032a66 | |||
| 7c550bd68e | |||
| db5f861dc5 | |||
| d04be19ae5 | |||
| 07532e2a3f | |||
| e7be24d478 | |||
| 34b7abcb2d | |||
| 47a53a51e4 | |||
| abc15974da | |||
| b3bf6929df | |||
| 2a8d61ecf6 | |||
| cf3e9037ab | |||
| 03135543a6 | |||
| 38228ebc9b | |||
| d6110c7118 | |||
| f92b1ed4a3 | |||
| 368087fb51 | |||
| 46d62e67de | |||
| 118d1aec0d | |||
| 7c9089c87d | |||
| ba18b9936f | |||
| 3b7ecd0095 | |||
| 19e9583b47 | |||
| 8352007570 | |||
| f7965d235a | |||
| 6aec9534e7 | |||
| 3ee6334db2 | |||
| 582a4de62d | |||
| c9959d4082 | |||
| fa11fb54cb | |||
| ee3b824870 | |||
| 55f5b30369 | |||
| 784e4bed2f | |||
| df7cba00fd | |||
| dfb720d0ee | |||
| f5d5cbd67b | |||
| 319fec2145 | |||
| 8787d23b3a | |||
| 9b48110155 | |||
| 3064e7bef6 | |||
| 1d4df8af3a | |||
| 2aba6e469c | |||
| de58593f14 |
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python
|
||||
#!/usr/bin/python2
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2017 Lenovo
|
||||
@@ -35,6 +35,9 @@ import confluent.client
|
||||
argparser = optparse.OptionParser(usage="Usage: <other command> | %prog [options]")
|
||||
argparser.add_option('-a', '--abbreviate', action='store_true',
|
||||
help='Attempt to use confluent server to shorten noderanges')
|
||||
argparser.add_option('-b', '--base',
|
||||
help='Use given node as reference for comparison when '
|
||||
'using -d, instead of using the most common result')
|
||||
argparser.add_option('-d', '--diff', action='store_true',
|
||||
help='Show what differs between most common '
|
||||
'output group and others')
|
||||
@@ -48,6 +51,8 @@ argparser.add_option('-c', '--count', action='store_true',
|
||||
argparser.add_option('-r', '--reverse', action='store_true',
|
||||
help='Reverse sort order to show biggest output group '
|
||||
'last')
|
||||
argparser.add_option('-l', '--log', action='store', type='string', dest='log',
|
||||
help='Log each output to file, using {node} as a placeholder for node.')
|
||||
(options, args) = argparser.parse_args()
|
||||
if sys.stdin.isatty():
|
||||
argparser.print_help()
|
||||
@@ -63,7 +68,7 @@ else:
|
||||
def print_current():
|
||||
if options.diff:
|
||||
grouped.print_deviants(skipmodal=options.skipcommon, count=options.count,
|
||||
reverse=options.reverse)
|
||||
reverse=options.reverse, basenode=options.base)
|
||||
else:
|
||||
grouped.print_all(skipmodal=options.skipcommon,
|
||||
count=options.count,
|
||||
@@ -80,6 +85,12 @@ while fullline:
|
||||
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:
|
||||
|
||||
+180
-126
@@ -1,8 +1,8 @@
|
||||
#!/usr/bin/env python
|
||||
#!/usr/bin/python2
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2014 IBM Corporation
|
||||
# Copyright 2015-2016 Lenovo
|
||||
# Copyright 2015-2018 Lenovo
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -49,6 +49,7 @@ import select
|
||||
import shlex
|
||||
import signal
|
||||
import socket
|
||||
import struct
|
||||
import sys
|
||||
import time
|
||||
try:
|
||||
@@ -89,6 +90,11 @@ netserver = None
|
||||
laststate = {}
|
||||
|
||||
|
||||
class BailOut(Exception):
|
||||
def __init__(self, errorcode=0):
|
||||
self.errorcode = errorcode
|
||||
|
||||
|
||||
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 "
|
||||
@@ -353,7 +359,7 @@ def do_command(command, server):
|
||||
if argv[0] == 'exit':
|
||||
if os.environ.get('TERM', '') not in ('linux'):
|
||||
sys.stdout.write('\x1b]0;\x07')
|
||||
sys.exit(0)
|
||||
raise Bailout()
|
||||
elif argv[0] in ('help', '?'):
|
||||
return print_help()
|
||||
elif argv[0] == 'cd':
|
||||
@@ -434,6 +440,10 @@ def do_command(command, server):
|
||||
currconsole = targpath
|
||||
startrequest = {'operation': 'start', 'path': targpath,
|
||||
'parameters': {}}
|
||||
height, width = struct.unpack(
|
||||
'hh', fcntl.ioctl(sys.stdout, termios.TIOCGWINSZ, b'....'))[:2]
|
||||
startrequest['parameters']['width'] = width
|
||||
startrequest['parameters']['height'] = height
|
||||
for param in argv[2:]:
|
||||
(parmkey, parmval) = param.split("=")
|
||||
startrequest['parameters'][parmkey] = parmval
|
||||
@@ -478,7 +488,8 @@ def createresource(args):
|
||||
collection = targpath
|
||||
else:
|
||||
collection, _, resname = targpath.rpartition('/')
|
||||
keydata['name'] = resname
|
||||
if 'name' not in keydata:
|
||||
keydata['name'] = resname
|
||||
makecall(session.create, (collection, keydata))
|
||||
|
||||
|
||||
@@ -493,6 +504,13 @@ def makecall(callout, args):
|
||||
if 'errorcode' in response:
|
||||
exitcode = response['errorcode']
|
||||
sys.stderr.write('Error: ' + response['error'] + '\n')
|
||||
if 'databynode' in response:
|
||||
lresponse = response['databynode']
|
||||
for node in lresponse:
|
||||
if 'errorcode' in lresponse[node]:
|
||||
exitcode = lresponse[node]['errorcode']
|
||||
if 'error' in lresponse[node]:
|
||||
sys.stderr.write('{0}: Error - {1}\n'.format(node, lresponse[node]['error']))
|
||||
|
||||
|
||||
def clearvalues(resource, attribs):
|
||||
@@ -575,11 +593,20 @@ def fullpath_target(currpath, forcepath=False):
|
||||
ntarget += '/'
|
||||
return ntarget
|
||||
|
||||
def do_resize(a, b):
|
||||
if not inconsole:
|
||||
return
|
||||
height, width = struct.unpack(
|
||||
'hh', fcntl.ioctl(sys.stdout, termios.TIOCGWINSZ, b'....'))[:2]
|
||||
tlvdata.send(session.connection, {'operation': 'resize', 'width': width,
|
||||
'height': height})
|
||||
|
||||
|
||||
def startconsole(nodename):
|
||||
global inconsole
|
||||
global consolename
|
||||
global didconsole
|
||||
signal.signal(signal.SIGWINCH, do_resize)
|
||||
didconsole = True
|
||||
consolename = nodename
|
||||
tty.setraw(sys.stdin.fileno())
|
||||
@@ -598,11 +625,12 @@ def quitconfetty(code=0, fullexit=False, fixterm=True):
|
||||
if oldtcattr is not None:
|
||||
termios.tcsetattr(sys.stdin.fileno(), termios.TCSANOW, oldtcattr)
|
||||
# Request default color scheme, to undo potential weirdness of terminal
|
||||
sys.stdout.write('\x1b[m')
|
||||
if sys.stdout.isatty():
|
||||
sys.stdout.write('\x1b[m')
|
||||
if fullexit:
|
||||
if os.environ.get('TERM', '') not in ('linux'):
|
||||
if sys.stdout.isatty() and os.environ.get('TERM', '') not in ('linux'):
|
||||
sys.stdout.write('\x1b]0;\x07')
|
||||
sys.exit(code)
|
||||
raise BailOut(code)
|
||||
else:
|
||||
tlvdata.send(session.connection, {'operation': 'stop',
|
||||
'path': currconsole})
|
||||
@@ -795,6 +823,10 @@ parser.add_option("-s", "--server", dest="netserver",
|
||||
parser.add_option("-c", "--control", dest="controlpath",
|
||||
help="Path to offer terminal control",
|
||||
metavar="PATH")
|
||||
parser.add_option(
|
||||
'-m', '--mintime', default=0,
|
||||
help='Minimum time to run or else pause for input (used to keep a '
|
||||
'terminal from closing quickly on error)')
|
||||
opts, shellargs = parser.parse_args()
|
||||
|
||||
username = None
|
||||
@@ -823,45 +855,6 @@ def server_connect():
|
||||
passphrase = getpass.getpass("Passphrase: ")
|
||||
session.authenticate(username, passphrase)
|
||||
|
||||
|
||||
try:
|
||||
server_connect()
|
||||
except EOFError, KeyboardInterrupt:
|
||||
sys.exit(0)
|
||||
except socket.gaierror:
|
||||
sys.stderr.write('Could not connect to confluent\n')
|
||||
sys.exit(1)
|
||||
# clear on start can help with readable of TUI, but it
|
||||
# can be annoying, so for now don't do it.
|
||||
# sys.stdout.write('\x1b[H\x1b[J')
|
||||
# sys.stdout.flush()
|
||||
|
||||
if sys.stdout.isatty():
|
||||
import readline
|
||||
|
||||
readline.parse_and_bind("tab: complete")
|
||||
readline.parse_and_bind("set bell-style none")
|
||||
dl = readline.get_completer_delims().replace('-', '')
|
||||
readline.set_completer_delims(dl)
|
||||
readline.set_completer(completer)
|
||||
|
||||
doexit = False
|
||||
inconsole = False
|
||||
pendingcommand = ""
|
||||
session_node = get_session_node(shellargs)
|
||||
if session_node is not None:
|
||||
consoleonly = True
|
||||
do_command("start /nodes/%s/console/session" % session_node, netserver)
|
||||
doexit = True
|
||||
elif shellargs:
|
||||
command = " ".join(shellargs)
|
||||
do_command(command, netserver)
|
||||
quitconfetty(fullexit=True, fixterm=False)
|
||||
|
||||
powerstate = None
|
||||
powertime = None
|
||||
|
||||
|
||||
def check_power_state():
|
||||
tlvdata.send(
|
||||
session.connection,
|
||||
@@ -870,86 +863,147 @@ def check_power_state():
|
||||
return
|
||||
|
||||
|
||||
while inconsole or not doexit:
|
||||
if inconsole:
|
||||
try:
|
||||
rdylist, _, _ = select.select(
|
||||
(sys.stdin, session.connection), (), (), 10)
|
||||
except select.error:
|
||||
rdylist = ()
|
||||
for fh in rdylist:
|
||||
if fh == session.connection:
|
||||
# this only should get called in the
|
||||
# case of a console session
|
||||
# each command should slurp up all relevant
|
||||
# recv potential
|
||||
#fh.read()
|
||||
try:
|
||||
data = tlvdata.recv(fh)
|
||||
except Exception:
|
||||
data = None
|
||||
if type(data) == dict:
|
||||
updatestatus(data)
|
||||
continue
|
||||
if data is not None:
|
||||
if clearpowermessage:
|
||||
sys.stdout.write("\x1b[2J\x1b[;H")
|
||||
clearpowermessage = False
|
||||
try:
|
||||
sys.stdout.write(data)
|
||||
except IOError: # Some times circumstances are bad
|
||||
# resort to byte at a time...
|
||||
for d in data:
|
||||
sys.stdout.write(d)
|
||||
now = time.time()
|
||||
if ('showtime' not in laststate or
|
||||
(now // 60) != laststate['showtime'] // 60):
|
||||
# don't bother churning if minute does not change
|
||||
laststate['showtime'] = now
|
||||
updatestatus()
|
||||
sys.stdout.flush()
|
||||
else:
|
||||
deadline = 5
|
||||
connected = False
|
||||
while not connected and deadline > 0:
|
||||
try:
|
||||
server_connect()
|
||||
connected = True
|
||||
except (socket.gaierror, socket.error):
|
||||
pass
|
||||
if not connected:
|
||||
time.sleep(1)
|
||||
deadline -=1
|
||||
if connected:
|
||||
do_command(
|
||||
"start /nodes/%s/console/session skipreplay=True" % consolename,
|
||||
netserver)
|
||||
else:
|
||||
doexit = True
|
||||
inconsole = False
|
||||
sys.stdout.write("\r\n[remote disconnected]\r\n")
|
||||
break
|
||||
else:
|
||||
try:
|
||||
myinput = fh.read()
|
||||
myinput = check_escape_seq(myinput, fh)
|
||||
if myinput:
|
||||
tlvdata.send(session.connection, myinput)
|
||||
except IOError:
|
||||
pass
|
||||
if powerstate is None or powertime < time.time() - 10: # Check powerstate every 10 seconds
|
||||
powertime = time.time()
|
||||
powerstate = True
|
||||
check_power_state()
|
||||
else:
|
||||
currcommand = prompt()
|
||||
try:
|
||||
do_command(currcommand, netserver)
|
||||
except socket.error:
|
||||
if sys.stdout.isatty():
|
||||
import readline
|
||||
|
||||
|
||||
def main():
|
||||
global inconsole
|
||||
try:
|
||||
server_connect()
|
||||
except EOFError, KeyboardInterrupt:
|
||||
raise BailOut(0)
|
||||
except socket.gaierror:
|
||||
sys.stderr.write('Could not connect to confluent\n')
|
||||
raise BailOut(1)
|
||||
# clear on start can help with readable of TUI, but it
|
||||
# can be annoying, so for now don't do it.
|
||||
# sys.stdout.write('\x1b[H\x1b[J')
|
||||
# sys.stdout.flush()
|
||||
global powerstate, powertime, clearpowermessage
|
||||
|
||||
|
||||
if sys.stdout.isatty():
|
||||
|
||||
readline.parse_and_bind("tab: complete")
|
||||
readline.parse_and_bind("set bell-style none")
|
||||
dl = readline.get_completer_delims().replace('-', '')
|
||||
readline.set_completer_delims(dl)
|
||||
readline.set_completer(completer)
|
||||
|
||||
doexit = False
|
||||
inconsole = False
|
||||
pendingcommand = ""
|
||||
session_node = get_session_node(shellargs)
|
||||
if session_node is not None:
|
||||
consoleonly = True
|
||||
do_command("start /nodes/%s/console/session" % session_node, netserver)
|
||||
doexit = True
|
||||
elif shellargs:
|
||||
command = " ".join(shellargs)
|
||||
do_command(command, netserver)
|
||||
quitconfetty(fullexit=True, fixterm=False)
|
||||
|
||||
powerstate = None
|
||||
powertime = None
|
||||
|
||||
while inconsole or not doexit:
|
||||
if inconsole:
|
||||
try:
|
||||
rdylist, _, _ = select.select(
|
||||
(sys.stdin, session.connection), (), (), 10)
|
||||
except select.error:
|
||||
rdylist = ()
|
||||
for fh in rdylist:
|
||||
if fh == session.connection:
|
||||
# this only should get called in the
|
||||
# case of a console session
|
||||
# each command should slurp up all relevant
|
||||
# recv potential
|
||||
#fh.read()
|
||||
try:
|
||||
data = tlvdata.recv(fh)
|
||||
except Exception:
|
||||
data = None
|
||||
if type(data) == dict:
|
||||
updatestatus(data)
|
||||
continue
|
||||
if data is not None:
|
||||
if clearpowermessage:
|
||||
sys.stdout.write("\x1b[2J\x1b[;H")
|
||||
clearpowermessage = False
|
||||
try:
|
||||
sys.stdout.write(data)
|
||||
except IOError: # Some times circumstances are bad
|
||||
# resort to byte at a time...
|
||||
for d in data:
|
||||
sys.stdout.write(d)
|
||||
now = time.time()
|
||||
if ('showtime' not in laststate or
|
||||
(now // 60) != laststate['showtime'] // 60):
|
||||
# don't bother churning if minute does not change
|
||||
laststate['showtime'] = now
|
||||
updatestatus()
|
||||
sys.stdout.flush()
|
||||
else:
|
||||
deadline = 5
|
||||
connected = False
|
||||
while not connected and deadline > 0:
|
||||
try:
|
||||
server_connect()
|
||||
connected = True
|
||||
except (socket.gaierror, socket.error):
|
||||
pass
|
||||
if not connected:
|
||||
time.sleep(1)
|
||||
deadline -=1
|
||||
if connected:
|
||||
do_command(
|
||||
"start /nodes/%s/console/session skipreplay=True" % consolename,
|
||||
netserver)
|
||||
else:
|
||||
doexit = True
|
||||
inconsole = False
|
||||
sys.stdout.write("\r\n[remote disconnected]\r\n")
|
||||
break
|
||||
else:
|
||||
try:
|
||||
myinput = fh.read()
|
||||
myinput = check_escape_seq(myinput, fh)
|
||||
if myinput:
|
||||
tlvdata.send(session.connection, myinput)
|
||||
except IOError:
|
||||
pass
|
||||
if powerstate is None or powertime < time.time() - 10: # Check powerstate every 10 seconds
|
||||
powertime = time.time()
|
||||
powerstate = True
|
||||
check_power_state()
|
||||
else:
|
||||
currcommand = prompt()
|
||||
try:
|
||||
server_connect()
|
||||
do_command(currcommand, netserver)
|
||||
except socket.error:
|
||||
doexit = True
|
||||
sys.stdout.write('Lost connection to server')
|
||||
quitconfetty(fullexit=True)
|
||||
try:
|
||||
server_connect()
|
||||
do_command(currcommand, netserver)
|
||||
except socket.error:
|
||||
doexit = True
|
||||
sys.stdout.write('Lost connection to server')
|
||||
quitconfetty(fullexit=True)
|
||||
|
||||
if __name__ == '__main__':
|
||||
errcode = 0
|
||||
deadline = 0
|
||||
if opts.mintime:
|
||||
deadline = os.times()[4] + float(opts.mintime)
|
||||
try:
|
||||
main()
|
||||
except BailOut as e:
|
||||
errcode = e.errorcode
|
||||
finally:
|
||||
if deadline and os.times()[4] < deadline:
|
||||
sys.stderr.write('[Exited early, hit enter to continue]')
|
||||
sys.stdin.readline()
|
||||
if errcode == 0:
|
||||
errcode = exitcode
|
||||
sys.exit(errcode)
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
#!/usr/bin/python2
|
||||
import optparse
|
||||
import signal
|
||||
import sys
|
||||
import os
|
||||
try:
|
||||
signal.signal(signal.SIGPIPE, signal.SIG_DFL)
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
path = os.path.dirname(os.path.realpath(__file__))
|
||||
path = os.path.realpath(os.path.join(path, '..', 'lib', 'python'))
|
||||
if path.startswith('/opt'):
|
||||
sys.path.append(path)
|
||||
|
||||
import confluent.client as client
|
||||
import confluent.sortutil as sortutil
|
||||
|
||||
|
||||
def lookupdata(data, key):
|
||||
ret = data.get(key, {}).get('value', '')
|
||||
if ret is None:
|
||||
ret = ''
|
||||
return ret
|
||||
|
||||
def main():
|
||||
argparser = optparse.OptionParser(
|
||||
usage='''\n %prog noderange -o ansible.hosts
|
||||
\n ''')
|
||||
argparser.add_option('-o', '--output',
|
||||
help='Ansible hosts file')
|
||||
(options, args) = argparser.parse_args()
|
||||
try:
|
||||
noderange = args[0]
|
||||
except IndexError:
|
||||
argparser.print_help()
|
||||
sys.exit(1)
|
||||
if not options.output:
|
||||
sys.stderr.write('Output file must be specified by -o\n')
|
||||
sys.exit(1)
|
||||
sess = client.Command()
|
||||
databynode = {}
|
||||
for res in sess.read('/noderange/{0}/attributes/all'.format(noderange)):
|
||||
for node in res.get('databynode', {}):
|
||||
if node not in databynode:
|
||||
databynode[node] = {}
|
||||
databynode[node].update(res['databynode'][node])
|
||||
nodesbygroup = {}
|
||||
with open(options.output, 'w') as importfile:
|
||||
needempty = False
|
||||
for node in sortutil.natural_sort(databynode):
|
||||
data = databynode[node]
|
||||
if not data.get('groups', []):
|
||||
importfile.write(node + '\n')
|
||||
needempty = True
|
||||
for g in data.get('groups', []):
|
||||
if g not in nodesbygroup:
|
||||
nodesbygroup[g] = set([node])
|
||||
else:
|
||||
nodesbygroup[g].add(node)
|
||||
if needempty:
|
||||
importfile.write('\n')
|
||||
for group in sortutil.natural_sort(nodesbygroup):
|
||||
importfile.write('[{0}]\n'.format(group))
|
||||
for node in sortutil.natural_sort(nodesbygroup[group]):
|
||||
importfile.write('{0}\n'.format(node))
|
||||
importfile.write('\n')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,76 @@
|
||||
#!/usr/bin/python2
|
||||
import csv
|
||||
import optparse
|
||||
import signal
|
||||
import sys
|
||||
import os
|
||||
try:
|
||||
signal.signal(signal.SIGPIPE, signal.SIG_DFL)
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
path = os.path.dirname(os.path.realpath(__file__))
|
||||
path = os.path.realpath(os.path.join(path, '..', 'lib', 'python'))
|
||||
if path.startswith('/opt'):
|
||||
sys.path.append(path)
|
||||
|
||||
import confluent.client as client
|
||||
import confluent.sortutil as sortutil
|
||||
|
||||
headers = '# Type,Serial Number,Current IP,Current username,Current password,New password,Recovery password,Switch enable password,New IPv4,IPv4 Subnet mask,IPv4 Default gateway,IPv4 DNS1,IPv4 DNS2,New IPv6,IPv6 Prefix,IPv6 Default gateway,IPv6 DNS1,IPv6 DNS2,Domain,Host name,Display name,Rack,Lowest Rack Unit,Height,Force,Stored credentials ID,Managed authentication,Group Name'.split(',')
|
||||
|
||||
def lookupdata(data, key):
|
||||
ret = data.get(key, {}).get('value', '')
|
||||
if ret is None:
|
||||
ret = ''
|
||||
return ret
|
||||
|
||||
def main():
|
||||
argparser = optparse.OptionParser(
|
||||
usage='''\n %prog noderange -o bulkimport.csv
|
||||
\n ''')
|
||||
argparser.add_option('-o', '--output',
|
||||
help='File to write for bulk import into xClarity Administrator')
|
||||
(options, args) = argparser.parse_args()
|
||||
try:
|
||||
noderange = args[0]
|
||||
except IndexError:
|
||||
argparser.print_help()
|
||||
sys.exit(1)
|
||||
if not options.output:
|
||||
sys.stderr.write('Output file must be specified by -o\n')
|
||||
sys.exit(1)
|
||||
if 'XCCUSER' not in os.environ or 'XCCPASS' not in os.environ:
|
||||
sys.stderr.write('Must specify XCCUSER and XCCPASS in environment variables\n')
|
||||
sys.exit(1)
|
||||
xccuser = os.environ['XCCUSER']
|
||||
xccpass = os.environ['XCCPASS']
|
||||
sess = client.Command()
|
||||
databynode = {}
|
||||
for res in sess.read('/noderange/{0}/attributes/all'.format(noderange)):
|
||||
for node in res.get('databynode', {}):
|
||||
if node not in databynode:
|
||||
databynode[node] = {}
|
||||
databynode[node].update(res['databynode'][node])
|
||||
for res in sess.read(
|
||||
'/noderange/{0}/configuration/management_controller/net_interfaces/management'.format(
|
||||
noderange)):
|
||||
for node in res.get('databynode', {}):
|
||||
currip = res['databynode'][node].get('ipv4_address', {}).get(
|
||||
'value', '')
|
||||
if currip:
|
||||
databynode[node]['hardwaremanagement.manager'] = {
|
||||
'value': currip.split('/', 1)[0]}
|
||||
with open(options.output, 'w') as importfile:
|
||||
bulkimport = csv.writer(importfile)
|
||||
bulkimport.writerow(headers)
|
||||
for node in sortutil.natural_sort(databynode):
|
||||
data = databynode[node]
|
||||
row = ['server', lookupdata(data, 'id.serial'), lookupdata(data, 'hardwaremanagement.manager'), xccuser, xccpass, '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', node, lookupdata(data, 'location.rack'), lookupdata(data, 'location.u'), '', '', '','', '']
|
||||
bulkimport.writerow(row)
|
||||
|
||||
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,103 @@
|
||||
#!/usr/bin/python2
|
||||
import csv
|
||||
import optparse
|
||||
import signal
|
||||
import sys
|
||||
import os
|
||||
try:
|
||||
signal.signal(signal.SIGPIPE, signal.SIG_DFL)
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
path = os.path.dirname(os.path.realpath(__file__))
|
||||
path = os.path.realpath(os.path.join(path, '..', 'lib', 'python'))
|
||||
if path.startswith('/opt'):
|
||||
sys.path.append(path)
|
||||
|
||||
import confluent.client as client
|
||||
import confluent.sortutil as sortutil
|
||||
|
||||
|
||||
def lookupdata(data, key):
|
||||
ret = data.get(key, {}).get('value', '')
|
||||
if ret is None:
|
||||
ret = ''
|
||||
return ret
|
||||
|
||||
def main():
|
||||
argparser = optparse.OptionParser(
|
||||
usage='''\n %prog noderange -o xcatnodes.def
|
||||
\n ''')
|
||||
argparser.add_option('-o', '--output',
|
||||
help='xCAT stanza file')
|
||||
argparser.add_option('-m', '--macs',
|
||||
help='xCAT macs.csv to write')
|
||||
(options, args) = argparser.parse_args()
|
||||
try:
|
||||
noderange = args[0]
|
||||
except IndexError:
|
||||
argparser.print_help()
|
||||
sys.exit(1)
|
||||
if not (options.output or options.macs):
|
||||
sys.stderr.write('Output file must be specified by -o or -m\n')
|
||||
sys.exit(1)
|
||||
sess = client.Command()
|
||||
databynode = {}
|
||||
for res in sess.read('/noderange/{0}/attributes/all'.format(noderange)):
|
||||
for node in res.get('databynode', {}):
|
||||
if node not in databynode:
|
||||
databynode[node] = {}
|
||||
databynode[node].update(res['databynode'][node])
|
||||
if options.output:
|
||||
with open(options.output, 'w') as importfile:
|
||||
for node in sortutil.natural_sort(databynode):
|
||||
xcatattrs = collect_attribute_data(databynode, node)
|
||||
importfile.write('{0}:\n'.format(node))
|
||||
importfile.write(' objtype=node\n')
|
||||
importfile.write(' arch=x86_64\n')
|
||||
importfile.write(' netboot=xnba\n')
|
||||
importfile.write(' mgt=ipmi\n')
|
||||
for attr in xcatattrs:
|
||||
if xcatattrs[attr] is None:
|
||||
continue
|
||||
importfile.write(' {0}={1}\n'.format(attr,
|
||||
xcatattrs[attr]))
|
||||
importfile.write('\n')
|
||||
if options.macs:
|
||||
with open(options.macs, 'w') as importfile:
|
||||
macsv = csv.writer(importfile)
|
||||
macsv.writerow(['#node', 'mac'])
|
||||
for node in sortutil.natural_sort(databynode):
|
||||
xcatattrs = collect_attribute_data(databynode, node)
|
||||
macsv.writerow([node, xcatattrs['mac']])
|
||||
|
||||
|
||||
|
||||
def collect_attribute_data(databynode, node):
|
||||
data = databynode[node]
|
||||
xcatattrs = {}
|
||||
xcatattrs['groups'] = ','.join(data.get('groups', []))
|
||||
xcatattrs['bmc'] = data.get('hardwaremanagement.manager', {}).get(
|
||||
'value', None)
|
||||
xcatattrs['mpa'] = data.get('enclosure.manager', {}).get(
|
||||
'value', None)
|
||||
xcatattrs['slotid'] = data.get('enclosure.bay', {}).get(
|
||||
'value', None)
|
||||
gotmac = False
|
||||
for key in data:
|
||||
if key.startswith('net.') and 'hwaddr' in key:
|
||||
currmac = data[key].get('value', None)
|
||||
if currmac:
|
||||
if gotmac:
|
||||
sys.stderr.write(
|
||||
'Ignoring {0} and using only {1} for mac, '
|
||||
'multiple macs not supported by '
|
||||
'confluent2xcat\n'.format(key, gotmac))
|
||||
continue
|
||||
gotmac = key
|
||||
xcatattrs['mac'] = currmac
|
||||
return xcatattrs
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python
|
||||
#!/usr/bin/python2
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2017 Lenovo
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
__author__ = 'alin37'
|
||||
|
||||
from getpass import getpass
|
||||
import optparse
|
||||
import os
|
||||
import signal
|
||||
@@ -47,6 +48,8 @@ argparser.add_option('-e', '--environment', action='store_true',
|
||||
'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()
|
||||
|
||||
|
||||
@@ -67,12 +70,24 @@ exitcode = 0
|
||||
nodetype="noderange"
|
||||
|
||||
if len(args) > 1:
|
||||
if "=" in args[1] or options.clear or options.environment:
|
||||
if "=" in args[1] or options.clear or options.environment or options.prompt:
|
||||
if "=" in args[1] and options.clear:
|
||||
print("Can not clear and set at the same time!")
|
||||
argparser.print_help()
|
||||
sys.exit(1)
|
||||
exitcode=client.updateattrib(session,args,nodetype, noderange, options)
|
||||
argassign = None
|
||||
if options.prompt:
|
||||
argassign = {}
|
||||
for arg in args[1:]:
|
||||
oneval = 1
|
||||
twoval = 2
|
||||
while oneval != twoval:
|
||||
oneval = getpass('Enter value for {0}: '.format(arg))
|
||||
twoval = getpass('Confirm value for {0}: '.format(arg))
|
||||
if oneval != twoval:
|
||||
print('Values did not match.')
|
||||
argassign[arg] = twoval
|
||||
exitcode=client.updateattrib(session,args,nodetype, noderange, options, argassign)
|
||||
try:
|
||||
# setting user output to what the user inputs
|
||||
if args[1] == 'all':
|
||||
@@ -111,6 +126,6 @@ else:
|
||||
sys.stderr.write(res['error'] + '\n')
|
||||
exitcode = 1
|
||||
else:
|
||||
print res['item']['href'].replace('/', '')
|
||||
print(res['item']['href'].replace('/', ''))
|
||||
|
||||
sys.exit(exitcode)
|
||||
sys.exit(exitcode)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python
|
||||
#!/usr/bin/python2
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2015-2017 Lenovo
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python
|
||||
#!/usr/bin/python2
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2015 Lenovo
|
||||
@@ -35,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 '
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python
|
||||
#!/usr/bin/python2
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2017 Lenovo
|
||||
@@ -43,7 +43,7 @@ def bailout(msg, code=1):
|
||||
sys.exit(code)
|
||||
|
||||
|
||||
argparser = optparse.OptionParser()
|
||||
argparser = optparse.OptionParser(usage="Usage: %prog [options] noderange [option|option=value]")
|
||||
argparser.add_option('-c', '--comparedefault', dest='comparedefault',
|
||||
action='store_true', default=False,
|
||||
help='Compare given settings to default or list settings '
|
||||
@@ -58,6 +58,16 @@ 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 = {
|
||||
@@ -88,6 +98,8 @@ setmode = None
|
||||
assignment = {}
|
||||
queryparms = {}
|
||||
printsys = []
|
||||
printbmc = []
|
||||
printallbmc = False
|
||||
setsys = {}
|
||||
forceset = False
|
||||
needval = None
|
||||
@@ -112,7 +124,7 @@ def _assign_value():
|
||||
|
||||
|
||||
def parse_config_line(arguments):
|
||||
global setmode, forceset, key, value, needval, candidate, path, attrib
|
||||
global setmode, printallbmc, forceset, key, value, needval, candidate, path, attrib
|
||||
for param in arguments:
|
||||
if param == 'show':
|
||||
continue # forgive muscle memory of pasu users
|
||||
@@ -122,6 +134,7 @@ def parse_config_line(arguments):
|
||||
continue
|
||||
if needval:
|
||||
key = needval
|
||||
needval = None
|
||||
value = param
|
||||
_assign_value()
|
||||
continue
|
||||
@@ -143,6 +156,8 @@ def parse_config_line(arguments):
|
||||
if setmode != False:
|
||||
bailout('Cannot do set and query in same command')
|
||||
if '.' not in param:
|
||||
if param == 'bmc':
|
||||
printallbmc = True
|
||||
matchedparms = False
|
||||
for candidate in cfgpaths:
|
||||
if candidate.startswith('{0}.'.format(param)):
|
||||
@@ -161,7 +176,10 @@ def parse_config_line(arguments):
|
||||
if not matchedparms:
|
||||
printsys.append(param)
|
||||
elif param not in cfgpaths:
|
||||
printsys.append(param)
|
||||
if param.startswith('bmc.'):
|
||||
printbmc.append(param.replace('bmc.', ''))
|
||||
else:
|
||||
printsys.append(param)
|
||||
else:
|
||||
path, attrib = cfgpaths[param]
|
||||
path = '/noderange/{0}/{1}'.format(noderange, path)
|
||||
@@ -174,12 +192,38 @@ if options.batch:
|
||||
argfile = open(options.batch, 'r')
|
||||
argset = argfile.readline()
|
||||
while argset:
|
||||
parse_config_line(shlex.split(argset))
|
||||
try:
|
||||
argset = argset[:argset.index('#')]
|
||||
except ValueError:
|
||||
pass
|
||||
argset = argset.strip()
|
||||
if argset:
|
||||
parse_config_line(shlex.split(argset))
|
||||
argset = argfile.readline()
|
||||
else:
|
||||
parse_config_line(args[1:])
|
||||
session = client.Command()
|
||||
rcode = 0
|
||||
if options.restoredefault:
|
||||
if options.restoredefault.lower() in (
|
||||
'sys', 'system', 'uefi', 'bios'):
|
||||
for fr in session.update(
|
||||
'/noderange/{0}/configuration/system/clear'.format(noderange),
|
||||
{'clear': True}):
|
||||
rcode |= client.printerror(fr)
|
||||
sys.exit(rcode)
|
||||
elif options.restoredefault.lower() in (
|
||||
'bmc', 'imm', 'xcc'):
|
||||
for fr in session.update(
|
||||
'/noderange/{0}/configuration/management_controller/clear'.format(noderange),
|
||||
{'clear': True}):
|
||||
rcode |= client.printerror(fr)
|
||||
sys.exit(rcode)
|
||||
else:
|
||||
sys.stderr.write(
|
||||
'Unrecognized component to restore defaults: {0}\n'.format(
|
||||
options.restoredefault))
|
||||
sys.exit(1)
|
||||
if setmode:
|
||||
if options.exclude:
|
||||
sys.stderr.write('Cannot use exclude and assign at the same time\n')
|
||||
@@ -188,8 +232,12 @@ if setmode:
|
||||
attrnamebypath = {}
|
||||
for key in assignment:
|
||||
if key not in cfgpaths:
|
||||
path = 'configuration/system/all'
|
||||
attrib = key
|
||||
if key.startswith('bmc.'):
|
||||
path = 'configuration/management_controller/extended/all'
|
||||
attrib = key.replace('bmc.', '')
|
||||
else:
|
||||
path = 'configuration/system/all'
|
||||
attrib = key
|
||||
else:
|
||||
path, attrib = cfgpaths[key]
|
||||
if path not in updatebypath:
|
||||
@@ -221,10 +269,19 @@ else:
|
||||
NullOpt(), queryparms[path])
|
||||
if rc:
|
||||
sys.exit(rc)
|
||||
if printbmc or printallbmc:
|
||||
rcode = client.print_attrib_path(
|
||||
'/noderange/{0}/configuration/management_controller/extended/all'.format(noderange),
|
||||
session, printbmc, options, attrprefix='bmc.')
|
||||
if printsys or options.exclude:
|
||||
if printsys == 'all':
|
||||
printsys = []
|
||||
path = '/noderange/{0}/configuration/system/all'.format(noderange)
|
||||
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)
|
||||
sys.exit(rcode)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python
|
||||
#!/usr/bin/python2
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2015 Lenovo
|
||||
@@ -17,7 +17,14 @@
|
||||
|
||||
import optparse
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
path = os.path.dirname(os.path.realpath(__file__))
|
||||
path = os.path.realpath(os.path.join(path, '..', 'lib', 'python'))
|
||||
if path.startswith('/opt'):
|
||||
sys.path.append(path)
|
||||
import confluent.client as client
|
||||
import confluent.sortutil as sortutil
|
||||
|
||||
confettypath = os.path.join(os.path.dirname(sys.argv[0]), 'confetty')
|
||||
argparser = optparse.OptionParser(
|
||||
@@ -26,9 +33,45 @@ argparser = optparse.OptionParser(
|
||||
"ctrl-'e', then release ctrl, then 'c', then '?' for a full list. "
|
||||
"For example, ctrl-'e', then 'c', then '.' will exit the current "
|
||||
"console")
|
||||
argparser.add_option('-t', '--tile', action='store_true', default=False,
|
||||
help='Tile console windows in the terminal')
|
||||
(options, args) = argparser.parse_args()
|
||||
if len(args) != 1:
|
||||
argparser.print_help()
|
||||
sys.exit(1)
|
||||
os.execl(confettypath, confettypath, 'start',
|
||||
'/nodes/{0}/console/session'.format(args[0]))
|
||||
if options.tile:
|
||||
null = open('/dev/null', 'w')
|
||||
nodes = []
|
||||
sess = client.Command()
|
||||
for res in sess.read('/noderange/{0}/nodes/'.format(args[0])):
|
||||
node = res.get('item', {}).get('href', '/').replace('/', '')
|
||||
if not node:
|
||||
sys.stderr.write(res.get('error', repr(res)) + '\n')
|
||||
sys.exit(1)
|
||||
nodes.append(node)
|
||||
initial = True
|
||||
pane = 0
|
||||
for node in sortutil.natural_sort(nodes):
|
||||
if initial:
|
||||
initial = False
|
||||
subprocess.call(
|
||||
['tmux', 'new-session', '-d', '-s',
|
||||
'nodeconsole_{0}'.format(os.getpid()), '-x', '800', '-y',
|
||||
'800', '{0} -m 5 start /nodes/{1}/console/session'.format(
|
||||
confettypath, node)])
|
||||
else:
|
||||
subprocess.call(['tmux', 'select-pane', '-t', str(pane)])
|
||||
subprocess.call(['tmux', 'set-option', 'pane-border-status', 'top'], stderr=null)
|
||||
pane += 1
|
||||
subprocess.call(
|
||||
['tmux', 'split', '-h',
|
||||
'{0} -m 5 start /nodes/{1}/console/session'.format(
|
||||
confettypath, node)])
|
||||
subprocess.call(['tmux', 'select-layout', 'tiled'], stdout=null)
|
||||
subprocess.call(['tmux', 'select-pane', '-t', '0'])
|
||||
subprocess.call(['tmux', 'set-option', 'pane-border-status', 'top'], stderr=null)
|
||||
os.execlp('tmux', 'tmux', 'attach', '-t', 'nodeconsole_{0}'.format(
|
||||
os.getpid()))
|
||||
else:
|
||||
os.execl(confettypath, confettypath, 'start',
|
||||
'/nodes/{0}/console/session'.format(args[0]))
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python
|
||||
#!/usr/bin/python2
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2017 Lenovo
|
||||
@@ -55,4 +55,4 @@ for r in session.create('/noderange/', attribs):
|
||||
exitcode |= 1
|
||||
if 'created' in r:
|
||||
print('{0}: created'.format(r['created']))
|
||||
sys.exit(exitcode)
|
||||
sys.exit(exitcode)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python
|
||||
#!/usr/bin/python2
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2017 Lenovo
|
||||
@@ -19,6 +19,7 @@ import csv
|
||||
import optparse
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
path = os.path.dirname(os.path.realpath(__file__))
|
||||
path = os.path.realpath(os.path.join(path, '..', 'lib', 'python'))
|
||||
@@ -26,31 +27,49 @@ if path.startswith('/opt'):
|
||||
sys.path.append(path)
|
||||
|
||||
import confluent.client as client
|
||||
import confluent.sortutil as sortutil
|
||||
|
||||
tabformat = '{0:>15}|{1:>15}|{2:>15}|{3:>36}|{4:>17}|{5:>12}|{6:>48}'
|
||||
columns = ['Node', 'Model', 'Serial', 'UUID', 'Mac Address', 'Type',
|
||||
'Current IP Addresses']
|
||||
delimit = ['-' * 15, '-' * 15, '-' * 15, '-' * 36, '-' * 17, '-' * 12,
|
||||
'-' * 48]
|
||||
defcolumns = ['Node', 'Model', 'Serial', 'UUID', 'Mac Address', 'Type',
|
||||
'Current IP Addresses']
|
||||
columnmapping = {
|
||||
'Node': 'nodename',
|
||||
'Model': 'modelnumber',
|
||||
'Serial': 'serialnumber',
|
||||
'UUID': 'uuid',
|
||||
'Type': 'types',
|
||||
'Current IP Addresses': 'ipaddrs',
|
||||
'IP': 'ipaddrs',
|
||||
'Bay': 'bay',
|
||||
'Mac Address': 'macs',
|
||||
'Mac': 'macs',
|
||||
'Switch': 'switch',
|
||||
'Port': 'port',
|
||||
'Advertised IP': 'otheripaddrs',
|
||||
'Other IP': 'otheripaddrs',
|
||||
}
|
||||
|
||||
|
||||
def dumpmacs(procinfo):
|
||||
return ','.join(procinfo['macs']) # + procinfo.get('relatedmacs', []))
|
||||
|
||||
|
||||
def print_disco(options, session, currmac):
|
||||
def print_disco(options, session, currmac, outhandler, columns):
|
||||
procinfo = {}
|
||||
for tmpinfo in session.read('/discovery/by-mac/{0}'.format(currmac)):
|
||||
|
||||
procinfo.update(tmpinfo)
|
||||
record = [procinfo['nodename'], procinfo['modelnumber'],
|
||||
procinfo['serialnumber'], procinfo['uuid'], dumpmacs(procinfo),
|
||||
','.join(procinfo['types']),
|
||||
','.join(sorted(procinfo['ipaddrs']))]
|
||||
if options.csv:
|
||||
csv.writer(sys.stdout).writerow(record)
|
||||
else:
|
||||
print(tabformat.format(*record))
|
||||
if 'Switch' in columns or 'Port' in columns:
|
||||
for tmpinfo in session.read(
|
||||
'/networking/macs/by-mac/{0}'.format(currmac)):
|
||||
if 'ports' in tmpinfo:
|
||||
# The api sorts so that the most specific available value
|
||||
# is last
|
||||
procinfo.update(tmpinfo['ports'][-1])
|
||||
record = []
|
||||
for col in columns:
|
||||
rawcol = columnmapping[col]
|
||||
rawval = procinfo.get(rawcol, '')
|
||||
if isinstance(rawval, list):
|
||||
record.append(','.join(rawval))
|
||||
else:
|
||||
record.append(str(rawval))
|
||||
outhandler.add_row(record)
|
||||
|
||||
|
||||
def process_header(header):
|
||||
@@ -72,7 +91,9 @@ def process_header(header):
|
||||
elif datum in ('bmc', 'imm', 'xcc'):
|
||||
fields.append('hardwaremanagement.manager')
|
||||
elif datum in ('bmc gateway', 'xcc gateway', 'imm gateway'):
|
||||
fields.append('net.bmc.gateway')
|
||||
fields.append('net.bmc.ipv4_gateway')
|
||||
elif datum in ('bmc_gateway', 'xcc_gateway', 'imm_gateway'):
|
||||
fields.append('net.bmc.ipv4_gateway')
|
||||
elif datum in ('bmcuser', 'username', 'user'):
|
||||
fields.append('secret.hardwaremanagementuser')
|
||||
elif datum in ('bmcpass', 'password', 'pass'):
|
||||
@@ -122,24 +143,44 @@ def datum_to_attrib(datum):
|
||||
del datum['node']
|
||||
return datum
|
||||
|
||||
unique_fields = frozenset(['serial', 'mac', 'uuid'])
|
||||
|
||||
def import_csv(options, session):
|
||||
nodedata = []
|
||||
unique_data = {}
|
||||
with open(options.importfile, 'r') as datasrc:
|
||||
records = csv.reader(datasrc)
|
||||
fields = process_header(next(records))
|
||||
for field in fields:
|
||||
if field in unique_fields:
|
||||
unique_data[field] = set([])
|
||||
broken = False
|
||||
for record in records:
|
||||
currfields = list(fields)
|
||||
nodedatum = {}
|
||||
for datum in record:
|
||||
nodedatum[currfields.pop(0)] = datum
|
||||
currfield = currfields.pop(0)
|
||||
if currfield in unique_fields:
|
||||
if datum in unique_data[currfield]:
|
||||
sys.stderr.write(
|
||||
"Import contains duplicate values "
|
||||
"({0} with value {1}\n".format(currfield, datum)
|
||||
)
|
||||
sys.exit(1)
|
||||
unique_data[currfield].add(datum)
|
||||
nodedatum[currfield] = datum
|
||||
if not datum_complete(nodedatum):
|
||||
sys.exit(1)
|
||||
if not search_record(nodedatum, options, session) and not broken:
|
||||
blocking_scan(session)
|
||||
if not search_record(nodedatum, options, session):
|
||||
sys.stderr.write(
|
||||
"Could not match the following data: " +
|
||||
repr(nodedatum) + '\n')
|
||||
sys.exit(1)
|
||||
broken = True
|
||||
nodedata.append(nodedatum)
|
||||
if broken:
|
||||
sys.exit(1)
|
||||
for datum in nodedata:
|
||||
maclist = search_record(datum, options, session)
|
||||
datum = datum_to_attrib(datum)
|
||||
@@ -165,13 +206,28 @@ def import_csv(options, session):
|
||||
|
||||
|
||||
def list_discovery(options, session):
|
||||
if options.csv:
|
||||
csv.writer(sys.stdout).writerow(columns)
|
||||
orderby = None
|
||||
if options.fields:
|
||||
columns = []
|
||||
for field in options.fields.split(','):
|
||||
for cdt in columnmapping:
|
||||
if cdt.lower().replace(
|
||||
' ', '') == field.lower().replace(' ', ''):
|
||||
columns.append(cdt)
|
||||
else:
|
||||
print(tabformat.format(*columns))
|
||||
print(tabformat.format(*delimit))
|
||||
columns = defcolumns
|
||||
if options.order:
|
||||
for field in columns:
|
||||
if options.order.lower() == field.lower():
|
||||
orderby = field
|
||||
outhandler = client.Tabulator(columns)
|
||||
for mac in list_matching_macs(options, session):
|
||||
print_disco(options, session, mac)
|
||||
print_disco(options, session, mac, outhandler, columns)
|
||||
if options.csv:
|
||||
outhandler.write_csv(sys.stdout, orderby)
|
||||
else:
|
||||
for row in outhandler.get_table(orderby):
|
||||
print(row)
|
||||
|
||||
def clear_discovery(options, session):
|
||||
for mac in list_matching_macs(options, session):
|
||||
@@ -181,8 +237,10 @@ def clear_discovery(options, session):
|
||||
else:
|
||||
print(repr(res))
|
||||
|
||||
def list_matching_macs(options, session):
|
||||
def list_matching_macs(options, session, node=None):
|
||||
path = '/discovery/'
|
||||
if node:
|
||||
path += 'by-node/{0}/'.format(node)
|
||||
if options.model:
|
||||
path += 'by-model/{0}/'.format(options.model)
|
||||
if options.serial:
|
||||
@@ -205,21 +263,25 @@ def list_matching_macs(options, session):
|
||||
path += 'by-mac/'
|
||||
return [x['item']['href'] for x in session.read(path)]
|
||||
|
||||
def assign_discovery(options, session):
|
||||
def assign_discovery(options, session, needid=True):
|
||||
abort = False
|
||||
if options.importfile:
|
||||
return import_csv(options, session)
|
||||
if not (options.serial or options.uuid or options.mac):
|
||||
if not options.node:
|
||||
sys.stderr.write("Node (-n) must be specified for assignment\n")
|
||||
abort = True
|
||||
if needid and not (options.serial or options.uuid or options.mac):
|
||||
sys.stderr.write(
|
||||
"UUID (-u), serial (-s), or ether address (-e) required for "
|
||||
"assignment\n")
|
||||
abort = True
|
||||
if not options.node:
|
||||
sys.stderr.write("Node (-n) must be specified for assignment\n")
|
||||
abort = True
|
||||
if abort:
|
||||
sys.exit(1)
|
||||
matches = list_matching_macs(options, session)
|
||||
matches = list_matching_macs(options, session, None if needid else options.node)
|
||||
if not matches:
|
||||
# Do a rescan to catch missing requested data
|
||||
blocking_scan(session)
|
||||
matches = list_matching_macs(options, session, None if needid else options.node)
|
||||
if not matches:
|
||||
sys.stderr.write("No matching discovery candidates found\n")
|
||||
sys.exit(1)
|
||||
@@ -230,6 +292,11 @@ def assign_discovery(options, session):
|
||||
else:
|
||||
print(repr(res))
|
||||
|
||||
def blocking_scan(session):
|
||||
list(session.update('/discovery/rescan', {'rescan': 'start'}))
|
||||
while(list(session.read('/discovery/rescan'))[0].get('scanning', False)):
|
||||
time.sleep(0.5)
|
||||
list(session.update('/networking/macs/rescan', {'rescan': 'start'}))
|
||||
|
||||
|
||||
def main():
|
||||
@@ -266,8 +333,13 @@ def main():
|
||||
parser.add_option('-i', '--import', dest='importfile',
|
||||
help='Import bulk assignment data from given CSV file',
|
||||
metavar='IMPORT.CSV')
|
||||
parser.add_option('-f', '--fields', dest='fields',
|
||||
help='Select fields for output',
|
||||
metavar='FIELDS')
|
||||
parser.add_option('-o', '--order', dest='order',
|
||||
help='Order output by given field', metavar='ORDER')
|
||||
(options, args) = parser.parse_args()
|
||||
if len(args) == 0 or args[0] not in ('list', 'assign', 'rescan', 'clear'):
|
||||
if len(args) == 0 or args[0] not in ('list', 'assign', 'reassign', 'rescan', 'clear'):
|
||||
parser.print_help()
|
||||
sys.exit(1)
|
||||
session = client.Command()
|
||||
@@ -277,10 +349,12 @@ def main():
|
||||
clear_discovery(options, session)
|
||||
if args[0] == 'assign':
|
||||
assign_discovery(options, session)
|
||||
if args[0] == 'reassign':
|
||||
assign_discovery(options, session, False)
|
||||
if args[0] == 'rescan':
|
||||
list(session.update('/discovery/rescan', {'rescan': 'start'}))
|
||||
print("Rescan initiated")
|
||||
blocking_scan(session)
|
||||
print("Rescan complete")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
main()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/python
|
||||
#!/usr/bin/python2
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2015-2017 Lenovo
|
||||
# Copyright 2015-2019 Lenovo
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -33,7 +33,8 @@ if path.startswith('/opt'):
|
||||
|
||||
import confluent.client as client
|
||||
|
||||
sys.stdout = codecs.getwriter('utf8')(sys.stdout)
|
||||
if sys.version_info[0] < 3:
|
||||
sys.stdout = codecs.getwriter('utf8')(sys.stdout)
|
||||
|
||||
argparser = optparse.OptionParser(
|
||||
usage="Usage: %prog [options] noderange [clear]")
|
||||
@@ -64,6 +65,8 @@ def format_event(evt):
|
||||
display = dt.strptime(evt['timestamp'], '%Y-%m-%dT%H:%M:%S')
|
||||
retparts.append(display.strftime('%m/%d/%Y %H:%M:%S'))
|
||||
dscparts = []
|
||||
if evt.get('log_id', None):
|
||||
retparts.append(evt['log_id'] + ':')
|
||||
if 'component_type' in evt and evt['component_type'] is not None:
|
||||
dscparts.append(evt['component_type'])
|
||||
if 'component' in evt and evt['component'] is not None:
|
||||
@@ -77,7 +80,10 @@ def format_event(evt):
|
||||
pass
|
||||
dscparts.append(evttext)
|
||||
retparts.append(' - '.join(dscparts))
|
||||
return ' '.join(retparts)
|
||||
msg = evt.get('message')
|
||||
if not msg:
|
||||
msg = ''
|
||||
return ' '.join(retparts) + msg
|
||||
|
||||
|
||||
if deletemode:
|
||||
@@ -98,4 +104,4 @@ for rsp in func('/noderange/{0}/events/hardware/log'.format(noderange)):
|
||||
if 'events' in thisdata:
|
||||
evtdata = thisdata['events']
|
||||
for evt in evtdata:
|
||||
print '{0}: {1}'.format(node, format_event(evt))
|
||||
print('{0}: {1}'.format(node, format_event(evt)))
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/python
|
||||
#!/usr/bin/python2
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2016-2017 Lenovo
|
||||
@@ -37,14 +37,14 @@ exitcode = 0
|
||||
|
||||
|
||||
def printfirm(node, prefix, data):
|
||||
if 'model' in data:
|
||||
if 'model' in data and data['model']:
|
||||
prefix += ' ' + data['model']
|
||||
builddesc = []
|
||||
if 'build' in data:
|
||||
if 'build' in data and data['build']:
|
||||
builddesc.append(data['build'])
|
||||
if 'date' in data:
|
||||
if 'date' in data and data['date']:
|
||||
builddesc.append(data['date'])
|
||||
if 'version' in data:
|
||||
if 'version' in data and data['version']:
|
||||
version = data['version']
|
||||
if builddesc:
|
||||
version += ' ({0})'.format(' '.join(builddesc))
|
||||
@@ -55,7 +55,8 @@ def printfirm(node, prefix, data):
|
||||
components = ['all']
|
||||
|
||||
argparser = optparse.OptionParser(
|
||||
usage="Usage: %prog <noderange> [update [--backup <file>]]|[<components>]")
|
||||
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()
|
||||
@@ -66,9 +67,15 @@ try:
|
||||
if args[1] == 'update':
|
||||
upfile = args[2]
|
||||
else:
|
||||
if args[1] == 'list':
|
||||
comps = args[2:]
|
||||
else:
|
||||
comps = args[1:]
|
||||
components = []
|
||||
for arg in args[1:]:
|
||||
for arg in comps:
|
||||
components += arg.split(',')
|
||||
if not components:
|
||||
components = ['all']
|
||||
|
||||
except IndexError:
|
||||
argparser.print_help()
|
||||
@@ -144,7 +151,7 @@ def show_firmware(session):
|
||||
for prefix in inv:
|
||||
firmware_shown = True
|
||||
printfirm(node, prefix, inv[prefix])
|
||||
if not firmware_shown:
|
||||
if not firmware_shown and not exitcode:
|
||||
argparser.print_help()
|
||||
|
||||
|
||||
@@ -156,4 +163,4 @@ try:
|
||||
update_firmware(session, upfile)
|
||||
except KeyboardInterrupt:
|
||||
print('')
|
||||
sys.exit(exitcode)
|
||||
sys.exit(exitcode)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python
|
||||
#!/usr/bin/python2
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2017 Lenovo
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
__author__ = 'alin37'
|
||||
|
||||
from getpass import getpass
|
||||
import optparse
|
||||
import os
|
||||
import signal
|
||||
@@ -35,9 +36,9 @@ import confluent.client as client
|
||||
|
||||
argparser = optparse.OptionParser(
|
||||
usage='''\n %prog [options] \
|
||||
\n %prog [options] nodegroup [list of attributes] \
|
||||
\n %prog [options] nodegroup [list of attributes|all] \
|
||||
\n %prog [options] nodegroup nodes=value1,value2 \
|
||||
\n %prog -e nodegroup <attribute names to set> \
|
||||
\n %prog -e nodegroup <attribute names to set> \
|
||||
\n %prog [options] nodegroup nodes=value1,value2
|
||||
\n ''')
|
||||
argparser.add_option('-b', '--blame', action='store_true',
|
||||
@@ -47,6 +48,8 @@ argparser.add_option('-e', '--environment', action='store_true',
|
||||
'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()
|
||||
|
||||
|
||||
@@ -72,7 +75,19 @@ if len(args) > 1:
|
||||
print("Can not clear and set at the same time!")
|
||||
argparser.print_help()
|
||||
sys.exit(1)
|
||||
exitcode=client.updateattrib(session,args,nodetype, nodegroups, options)
|
||||
argassign = None
|
||||
if options.prompt:
|
||||
argassign = {}
|
||||
for arg in args[1:]:
|
||||
oneval = 1
|
||||
twoval = 2
|
||||
while oneval != twoval:
|
||||
oneval = getpass('Enter value for {0}: '.format(arg))
|
||||
twoval = getpass('Confirm value for {0}: '.format(arg))
|
||||
if oneval != twoval:
|
||||
print('Values did not match.')
|
||||
argassign[arg] = twoval
|
||||
exitcode=client.updateattrib(session,args,nodetype, nodegroups, options, argassign)
|
||||
try:
|
||||
# setting user output to what the user inputs
|
||||
if args[1] == 'all':
|
||||
@@ -110,4 +125,4 @@ else:
|
||||
else:
|
||||
print res['item']['href'].replace('/', '')
|
||||
|
||||
sys.exit(exitcode)
|
||||
sys.exit(exitcode)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python
|
||||
#!/usr/bin/python2
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2017 Lenovo
|
||||
@@ -55,4 +55,4 @@ for r in session.create('/nodegroups/', attribs):
|
||||
exitcode |= 1
|
||||
if 'created' in r:
|
||||
print('{0}: created'.format(r['created']))
|
||||
sys.exit(exitcode)
|
||||
sys.exit(exitcode)
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
#!/usr/bin/python2
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2015-2017 Lenovo
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
__author__ = 'jjohnson2,alin37,andywray'
|
||||
|
||||
import optparse
|
||||
import os
|
||||
import signal
|
||||
import sys
|
||||
|
||||
try:
|
||||
signal.signal(signal.SIGPIPE, signal.SIG_DFL)
|
||||
except AttributeError:
|
||||
pass
|
||||
path = os.path.dirname(os.path.realpath(__file__))
|
||||
path = os.path.realpath(os.path.join(path, '..', 'lib', 'python'))
|
||||
if path.startswith('/opt'):
|
||||
sys.path.append(path)
|
||||
|
||||
import confluent.client as client
|
||||
|
||||
def main():
|
||||
argparser = optparse.OptionParser(
|
||||
usage="Usage: %prog\n")
|
||||
(options, args) = argparser.parse_args()
|
||||
noderange=""
|
||||
nodelist=""
|
||||
nodelist = '/nodegroups/'
|
||||
session = client.Command()
|
||||
exitcode = 0
|
||||
showtype='all'
|
||||
requestargs=args[1:]
|
||||
nodetype='noderange'
|
||||
if len(args) > 0:
|
||||
argparser.print_help()
|
||||
sys.exit(1)
|
||||
else:
|
||||
for res in session.read(nodelist):
|
||||
if 'error' in res:
|
||||
sys.stderr.write(res['error'] + '\n')
|
||||
exitcode = 1
|
||||
else:
|
||||
print(res['item']['href'].replace('/', ''))
|
||||
|
||||
sys.exit(exitcode)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python
|
||||
#!/usr/bin/python2
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2017 Lenovo
|
||||
@@ -49,4 +49,4 @@ for r in session.delete('/nodegroups/{0}'.format(noderange)):
|
||||
exitcode |= 1
|
||||
if 'deleted' in r:
|
||||
print('{0}: deleted'.format(r['deleted']))
|
||||
sys.exit(exitcode)
|
||||
sys.exit(exitcode)
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
#!/usr/bin/python2
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2019 Lenovo
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
__author__ = 'jjohnson2,alin37,andywray'
|
||||
|
||||
import optparse
|
||||
import os
|
||||
import signal
|
||||
import sys
|
||||
|
||||
try:
|
||||
signal.signal(signal.SIGPIPE, signal.SIG_DFL)
|
||||
except AttributeError:
|
||||
pass
|
||||
path = os.path.dirname(os.path.realpath(__file__))
|
||||
path = os.path.realpath(os.path.join(path, '..', 'lib', 'python'))
|
||||
if path.startswith('/opt'):
|
||||
sys.path.append(path)
|
||||
|
||||
import confluent.client as client
|
||||
|
||||
def main():
|
||||
argparser = optparse.OptionParser(
|
||||
usage="Usage: %prog <group> <new group name>\n")
|
||||
(options, args) = argparser.parse_args()
|
||||
noderange=""
|
||||
nodelist=""
|
||||
nodelist = '/nodegroups/'
|
||||
session = client.Command()
|
||||
exitcode = 0
|
||||
requestargs=args[1:]
|
||||
nodetype='noderange'
|
||||
if len(args) != 2:
|
||||
argparser.print_help()
|
||||
sys.exit(1)
|
||||
else:
|
||||
for res in session.update(
|
||||
'/nodegroups/{0}/attributes/rename'.format(args[0]),
|
||||
{'rename': args[1]}):
|
||||
if 'error' in res:
|
||||
sys.stderr.write(res['error'] + '\n')
|
||||
exitcode = 1
|
||||
else:
|
||||
print('{0}: {1}'.format(res['oldname'], res['newname']))
|
||||
|
||||
sys.exit(exitcode)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python
|
||||
#!/usr/bin/python2
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2015-2017 Lenovo
|
||||
@@ -32,7 +32,8 @@ if path.startswith('/opt'):
|
||||
|
||||
import confluent.client as client
|
||||
|
||||
sys.stdout = codecs.getwriter('utf8')(sys.stdout)
|
||||
if sys.version_info[0] < 3:
|
||||
sys.stdout = codecs.getwriter('utf8')(sys.stdout)
|
||||
|
||||
argparser = optparse.OptionParser(usage="Usage: %prog <noderange>")
|
||||
(options, args) = argparser.parse_args()
|
||||
@@ -44,6 +45,13 @@ except IndexError:
|
||||
client.check_globbing(noderange)
|
||||
|
||||
|
||||
codemappings = {
|
||||
'ok': 0,
|
||||
'warning': 1,
|
||||
'critical': 2,
|
||||
'failed': 2,
|
||||
}
|
||||
|
||||
def main():
|
||||
global session, exitcode, healthbynode, healthexplanations, health, node, sensor, explanation
|
||||
session = client.Command()
|
||||
@@ -66,9 +74,11 @@ def main():
|
||||
if 'error' in health[node]:
|
||||
sys.stderr.write('{0}: Error: {1}\n'.format(
|
||||
node, health[node]['error']))
|
||||
exitcode |= 1
|
||||
exitcode = 3
|
||||
if 'health' in health[node]:
|
||||
healthbynode[node] = health[node]['health']['value']
|
||||
if codemappings[healthbynode[node]] > exitcode:
|
||||
exitcode = codemappings[healthbynode[node]]
|
||||
if 'sensors' in health[node]:
|
||||
healthexplanations[node] = []
|
||||
for sensor in health[node]['sensors']:
|
||||
@@ -94,5 +104,5 @@ try:
|
||||
main()
|
||||
except KeyboardInterrupt:
|
||||
print('')
|
||||
sys.exit(0)
|
||||
sys.exit(exitcode)
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python
|
||||
#!/usr/bin/python2
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2015-2017 Lenovo
|
||||
@@ -31,7 +31,7 @@ if path.startswith('/opt'):
|
||||
|
||||
import confluent.client as client
|
||||
|
||||
argparser = optparse.OptionParser(usage="Usage: %prog <noderange> [on|off]")
|
||||
argparser = optparse.OptionParser(usage="Usage: %prog <noderange> [on|off|blink]")
|
||||
(options, args) = argparser.parse_args()
|
||||
try:
|
||||
noderange = args[0]
|
||||
@@ -42,9 +42,6 @@ client.check_globbing(noderange)
|
||||
identifystate = None
|
||||
if len(sys.argv) > 2:
|
||||
identifystate = sys.argv[2]
|
||||
else:
|
||||
argparser.print_help()
|
||||
sys.exit(1)
|
||||
session = client.Command()
|
||||
exitcode = 0
|
||||
sys.exit(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/python
|
||||
#!/usr/bin/python2
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2016-2017 Lenovo
|
||||
@@ -34,7 +34,8 @@ if path.startswith('/opt'):
|
||||
|
||||
import confluent.client as client
|
||||
|
||||
sys.stdout = codecs.getwriter('utf8')(sys.stdout)
|
||||
if sys.version_info[0] < 3:
|
||||
sys.stdout = codecs.getwriter('utf8')(sys.stdout)
|
||||
|
||||
filters = []
|
||||
|
||||
@@ -50,15 +51,17 @@ def print_mem_info(node, prefix, meminfo):
|
||||
memdescfmt = '{0}GB PC'
|
||||
if meminfo['memory_type'] == 'DDR3 SDRAM':
|
||||
memdescfmt += '3-{1} '
|
||||
elif meminfo['memory_type'] == 'DDR4 SDRAM':
|
||||
elif 'DDR4' in meminfo['memory_type']:
|
||||
memdescfmt += '4-{1} '
|
||||
else:
|
||||
print('{0}: {1}: Unrecognized Memory'.format(node, prefix))
|
||||
return
|
||||
if meminfo['ecc']:
|
||||
if meminfo.get('ecc', False):
|
||||
memdescfmt += 'ECC '
|
||||
capacity = meminfo['capacity_mb'] / 1024
|
||||
memdescfmt += meminfo['module_type']
|
||||
modtype = meminfo.get('module_type', None)
|
||||
if modtype:
|
||||
memdescfmt += modtype
|
||||
memdesc = memdescfmt.format(capacity, meminfo['speed'])
|
||||
print('{0}: {1} description: {2}'.format(node, prefix, memdesc))
|
||||
print('{0}: {1} manufacturer: {2}'.format(
|
||||
@@ -66,10 +69,11 @@ def print_mem_info(node, prefix, meminfo):
|
||||
print('{0}: {1} model: {2}'.format(node, prefix, meminfo['model']))
|
||||
print('{0}: {1} serial number: {2}'.format(node, prefix,
|
||||
meminfo['serial']))
|
||||
print('{0}: {1} manufacture date: {2}'.format(node, prefix,
|
||||
meminfo['manufacture_date']))
|
||||
print('{0}: {1} manufacture location: {2}'.format(
|
||||
node, prefix, meminfo['manufacture_location']))
|
||||
if 'manufacture_date' in meminfo:
|
||||
print('{0}: {1} manufacture date: {2}'.format(node, prefix,
|
||||
meminfo['manufacture_date']))
|
||||
print('{0}: {1} manufacture location: {2}'.format(
|
||||
node, prefix, meminfo['manufacture_location']))
|
||||
|
||||
exitcode = 0
|
||||
|
||||
@@ -87,6 +91,7 @@ def printerror(res, node=None):
|
||||
|
||||
|
||||
url = '/noderange/{0}/inventory/hardware/all/all'
|
||||
usedprefixes = set([])
|
||||
|
||||
argparser = optparse.OptionParser(
|
||||
usage="Usage: %prog <noderange> [serial|model|uuid|mac]")
|
||||
@@ -129,6 +134,11 @@ try:
|
||||
continue
|
||||
for inv in res['databynode'][node]['inventory']:
|
||||
prefix = inv['name']
|
||||
idx = 2
|
||||
while (node, prefix) in usedprefixes:
|
||||
prefix = '{0} {1}'.format(inv['name'], idx)
|
||||
idx += 1
|
||||
usedprefixes.add((node, prefix))
|
||||
if not inv['present']:
|
||||
if not filters:
|
||||
if options.json:
|
||||
@@ -136,7 +146,7 @@ try:
|
||||
databynode[node] = {}
|
||||
databynode[node][prefix] = inv
|
||||
else:
|
||||
print '{0}: {1}: Not Present'.format(node, prefix)
|
||||
print('{0}: {1}: Not Present'.format(node, prefix))
|
||||
continue
|
||||
info = inv['information']
|
||||
info.pop('board_extra', None)
|
||||
|
||||
Executable
+146
@@ -0,0 +1,146 @@
|
||||
#!/usr/bin/python2
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2019 Lenovo
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import optparse
|
||||
import os
|
||||
import signal
|
||||
import sys
|
||||
import time
|
||||
|
||||
try:
|
||||
signal.signal(signal.SIGPIPE, signal.SIG_DFL)
|
||||
except AttributeError:
|
||||
pass
|
||||
path = os.path.dirname(os.path.realpath(__file__))
|
||||
path = os.path.realpath(os.path.join(path, '..', 'lib', 'python'))
|
||||
if path.startswith('/opt'):
|
||||
sys.path.append(path)
|
||||
|
||||
import confluent.client as client
|
||||
|
||||
exitcode = 0
|
||||
|
||||
argparser = optparse.OptionParser(
|
||||
usage="Usage: "
|
||||
"%prog <noderange> [list][install <file>|save <directory>|delete <name>]")
|
||||
(options, args) = argparser.parse_args()
|
||||
upfile = None
|
||||
downdir = None
|
||||
delete = False
|
||||
try:
|
||||
noderange = args[0]
|
||||
if len(args) > 1:
|
||||
if args[1] == 'install':
|
||||
upfile = args[2]
|
||||
elif args[1] == 'save':
|
||||
downdir = args[2]
|
||||
elif args[1] == 'delete':
|
||||
delete = args[2]
|
||||
elif args[1] != 'list':
|
||||
argparser.print_help()
|
||||
sys.exit(1)
|
||||
except IndexError:
|
||||
argparser.print_help()
|
||||
sys.exit(1)
|
||||
client.check_globbing(noderange)
|
||||
|
||||
|
||||
def install_license(session, filename):
|
||||
global exitcode
|
||||
if not os.path.exists(filename):
|
||||
sys.stderr.write('Unable to locate requested file {0}\n'.format(
|
||||
filename))
|
||||
sys.exit(404)
|
||||
resource = '/noderange/{0}/configuration/' \
|
||||
'management_controller/licenses/'.format(noderange)
|
||||
filename = os.path.abspath(filename)
|
||||
instargs = {'filename': filename}
|
||||
for res in session.create(resource, instargs):
|
||||
for node in res.get('databynode', []):
|
||||
if 'error' in res['databynode'][node]:
|
||||
sys.stderr.write('{0}: {1}\n'.format(
|
||||
node, res['databynode'][node]['error']))
|
||||
sys.exit(res['databynode'][node].get('errorcode', 1))
|
||||
show_licenses(session)
|
||||
|
||||
|
||||
def save_licenses(session, dirname):
|
||||
global exitcode
|
||||
resource = '/noderange/{0}/configuration/' \
|
||||
'management_controller/save_licenses'.format(noderange)
|
||||
filename = os.path.abspath(dirname)
|
||||
if not os.path.exists(filename):
|
||||
sys.stderr.write('Unable to locate specified directory {0}\n'.format(
|
||||
filename))
|
||||
sys.exit(404)
|
||||
instargs = {'dirname': filename}
|
||||
for res in session.create(resource, instargs):
|
||||
for node in res.get('databynode', {}):
|
||||
fname = res['databynode'][node].get('filename', None)
|
||||
if fname:
|
||||
print('{0}: Saved license to {1}'.format(node, fname))
|
||||
else:
|
||||
sys.stderr.write('{0}: {1}', node, repr(res['databynode'][node]))
|
||||
|
||||
|
||||
def show_licenses(session):
|
||||
global exitcode
|
||||
for res in session.read(
|
||||
'/noderange/{0}/configuration/management_controller/licenses/'
|
||||
'all'.format(noderange)):
|
||||
for node in res.get('databynode', {}):
|
||||
for license in res['databynode'][node].get('License', []):
|
||||
msg = '{0}: {1}'.format(node, license.get('feature',
|
||||
'Unknown'))
|
||||
if license.get('state', 'Active') != 'Active':
|
||||
msg += ' ({0})'.format(license['state'])
|
||||
print(msg)
|
||||
|
||||
|
||||
def delete_license(session, licname):
|
||||
global exitcode
|
||||
licstodel = []
|
||||
for res in list(session.read(
|
||||
'/noderange/{0}/configuration/management_controller/licenses/'
|
||||
'all'.format(noderange))):
|
||||
for node in res.get('databynode', {}):
|
||||
for license in res['databynode'][node].get('License', []):
|
||||
if license.get('feature', None) == licname:
|
||||
prefix = '/nodes/{0}/configuration/management_controller/licenses/'.format(node)
|
||||
for currlic in list(session.read(prefix)):
|
||||
currlic = currlic.get('item', {}).get('href', 'all')
|
||||
if currlic == 'all':
|
||||
continue
|
||||
currname = list(session.read(prefix + currlic))[0]
|
||||
currname = currname.get('License', [{}])[0].get('feature', None)
|
||||
if currname == licname:
|
||||
list(session.delete(prefix + currlic))
|
||||
show_licenses(session)
|
||||
|
||||
try:
|
||||
session = client.Command()
|
||||
if upfile:
|
||||
install_license(session, upfile)
|
||||
elif downdir:
|
||||
save_licenses(session, downdir)
|
||||
elif delete:
|
||||
delete_license(session, delete)
|
||||
else:
|
||||
show_licenses(session)
|
||||
except KeyboardInterrupt:
|
||||
print('')
|
||||
sys.exit(exitcode)
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python
|
||||
#!/usr/bin/python2
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2015-2017 Lenovo
|
||||
@@ -61,7 +61,7 @@ def main():
|
||||
sys.stderr.write(res['error'] + '\n')
|
||||
exitcode = 1
|
||||
else:
|
||||
print res['item']['href'].replace('/', '')
|
||||
print(res['item']['href'].replace('/', ''))
|
||||
|
||||
sys.exit(exitcode)
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/python
|
||||
#!/usr/bin/python2
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2018 Lenovo
|
||||
@@ -83,7 +83,8 @@ def list_media(noderange, media):
|
||||
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):
|
||||
if (url and not url.startswith('file:') and
|
||||
not res['databynode'][node].get('secure', False)):
|
||||
name += ' (insecure)'
|
||||
if not name:
|
||||
continue
|
||||
@@ -187,4 +188,4 @@ def main():
|
||||
sys.exit(1)
|
||||
handler(noderange, media)
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
main()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python
|
||||
#!/usr/bin/python2
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2015-2017 Lenovo
|
||||
@@ -72,4 +72,4 @@ if options.previous:
|
||||
# add dictionary to session
|
||||
session.add_precede_dict(prev)
|
||||
|
||||
sys.exit(session.simple_noderange_command(noderange, '/power/state', setstate))
|
||||
sys.exit(session.simple_noderange_command(noderange, '/power/state', setstate))
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python
|
||||
#!/usr/bin/python2
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2017 Lenovo
|
||||
@@ -49,4 +49,4 @@ for r in session.delete('/noderange/{0}'.format(noderange)):
|
||||
exitcode |= 1
|
||||
if 'deleted' in r:
|
||||
print('{0}: deleted'.format(r['deleted']))
|
||||
sys.exit(exitcode)
|
||||
sys.exit(exitcode)
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
#!/usr/bin/python2
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2019 Lenovo
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import optparse
|
||||
import os
|
||||
import signal
|
||||
import sys
|
||||
try:
|
||||
signal.signal(signal.SIGPIPE, signal.SIG_DFL)
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
path = os.path.dirname(os.path.realpath(__file__))
|
||||
path = os.path.realpath(os.path.join(path, '..', 'lib', 'python'))
|
||||
if path.startswith('/opt'):
|
||||
sys.path.append(path)
|
||||
|
||||
import confluent.client as client
|
||||
|
||||
argparser = optparse.OptionParser(usage="Usage: %prog <noderange> <newname>")
|
||||
(options, args) = argparser.parse_args()
|
||||
try:
|
||||
noderange = args[0]
|
||||
except IndexError:
|
||||
argparser.print_help()
|
||||
sys.exit(1)
|
||||
client.check_globbing(noderange)
|
||||
identifystate = None
|
||||
if len(sys.argv) > 2:
|
||||
newname = sys.argv[2]
|
||||
else:
|
||||
argparser.print_help()
|
||||
sys.exit(1)
|
||||
session = client.Command()
|
||||
exitcode = 0
|
||||
sys.exit(
|
||||
session.simple_noderange_command(noderange, 'attributes/rename', newname))
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python
|
||||
#!/usr/bin/python2
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2015-2017 Lenovo
|
||||
|
||||
Executable
+153
@@ -0,0 +1,153 @@
|
||||
#!/usr/bin/python2
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2016-2017 Lenovo
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from collections import deque
|
||||
import optparse
|
||||
import os
|
||||
import select
|
||||
import shlex
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
try:
|
||||
signal.signal(signal.SIGPIPE, signal.SIG_DFL)
|
||||
except AttributeError:
|
||||
pass
|
||||
path = os.path.dirname(os.path.realpath(__file__))
|
||||
path = os.path.realpath(os.path.join(path, '..', 'lib', 'python'))
|
||||
if path.startswith('/opt'):
|
||||
sys.path.append(path)
|
||||
|
||||
import confluent.client as client
|
||||
import confluent.screensqueeze as sq
|
||||
import confluent.sortutil as sortutil
|
||||
|
||||
|
||||
def run():
|
||||
argparser = optparse.OptionParser(
|
||||
usage="Usage: %prog location noderange:location",
|
||||
)
|
||||
argparser.add_option('-f', '-c', '--count', type='int', default=168,
|
||||
help='Number of nodes to concurrently rsync')
|
||||
# among other things, FD_SETSIZE limits. Besides, spawning too many
|
||||
# processes can be unkind for the unaware on memory pressure and such...
|
||||
argparser.disable_interspersed_args()
|
||||
(options, args) = argparser.parse_args()
|
||||
if len(args) < 2 or ':' not in args[-1]:
|
||||
argparser.print_help()
|
||||
sys.exit(1)
|
||||
concurrentprocs = options.count
|
||||
noderange, targpath = args[-1].split(':', 1)
|
||||
client.check_globbing(noderange)
|
||||
c = client.Command()
|
||||
cmdstr = " ".join(args[:-1])
|
||||
cmdstr = 'rsync -av --info=progress2 ' + cmdstr
|
||||
cmdstr += ' {node}:' + targpath
|
||||
|
||||
currprocs = 0
|
||||
all = set([])
|
||||
pipedesc = {}
|
||||
pendingexecs = deque()
|
||||
exitcode = 0
|
||||
|
||||
for exp in c.create('/noderange/{0}/attributes/expression'.format(noderange),
|
||||
{'expression': cmdstr}):
|
||||
if 'error' in exp:
|
||||
sys.stderr.write(exp['error'] + '\n')
|
||||
exitcode |= exp.get('errorcode', 1)
|
||||
ex = exp.get('databynode', ())
|
||||
for node in ex:
|
||||
cmd = ex[node]['value'].encode('utf-8')
|
||||
cmdv = shlex.split(cmd)
|
||||
if currprocs < concurrentprocs:
|
||||
currprocs += 1
|
||||
run_cmdv(node, cmdv, all, pipedesc)
|
||||
else:
|
||||
pendingexecs.append((node, cmdv))
|
||||
if not all or exitcode:
|
||||
sys.exit(exitcode)
|
||||
rdy, _, _ = select.select(all, [], [], 10)
|
||||
nodeerrs = {}
|
||||
pernodeout = {}
|
||||
pernodefile = {}
|
||||
output = sq.ScreenPrinter(noderange, c)
|
||||
|
||||
while all:
|
||||
for r in rdy:
|
||||
desc = pipedesc[r]
|
||||
node = desc['node']
|
||||
data = True
|
||||
while data and select.select([r], [], [], 0)[0]:
|
||||
data = r.read(1)
|
||||
if data:
|
||||
if desc['type'] == 'stdout':
|
||||
if node not in pernodeout:
|
||||
pernodeout[node] = ''
|
||||
pernodeout[node] += data
|
||||
if '\n' in pernodeout[node]:
|
||||
currout, pernodeout[node] = pernodeout[node].split('\n', 1)
|
||||
if currout:
|
||||
pernodefile[node] = os.path.basename(currout)
|
||||
if '\r' in pernodeout[node]:
|
||||
currout, pernodeout[node] = pernodeout[node].split('\r', 1)
|
||||
if currout:
|
||||
currout = currout.split()
|
||||
try:
|
||||
currout = currout[1]
|
||||
output.set_output(node, '{0}:{1}'.format(pernodefile[node], currout))
|
||||
except IndexError:
|
||||
pernodefile = currout[0]
|
||||
pass
|
||||
else:
|
||||
output.set_output(node, 'error!')
|
||||
if node not in nodeerrs:
|
||||
nodeerrs[node] = ''
|
||||
nodeerrs[node] += data
|
||||
else:
|
||||
pop = desc['popen']
|
||||
ret = pop.poll()
|
||||
if ret is not None:
|
||||
exitcode = exitcode | ret
|
||||
all.discard(r)
|
||||
r.close()
|
||||
if node not in nodeerrs:
|
||||
output.set_output(node, 'complete')
|
||||
if desc['type'] == 'stdout' and pendingexecs:
|
||||
node, cmdv = pendingexecs.popleft()
|
||||
run_cmdv(node, cmdv, all, pipedesc)
|
||||
if all:
|
||||
rdy, _, _ = select.select(all, [], [], 10)
|
||||
for node in nodeerrs:
|
||||
for line in nodeerrs[node].split('\n'):
|
||||
sys.stderr.write('{0}: {1}\n'.format(node, line))
|
||||
sys.exit(exitcode)
|
||||
|
||||
|
||||
def run_cmdv(node, cmdv, all, pipedesc):
|
||||
nopen = subprocess.Popen(
|
||||
cmdv, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
pipedesc[nopen.stdout] = {'node': node, 'popen': nopen,
|
||||
'type': 'stdout'}
|
||||
pipedesc[nopen.stderr] = {'node': node, 'popen': nopen,
|
||||
'type': 'stderr'}
|
||||
all.add(nopen.stdout)
|
||||
all.add(nopen.stderr)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
run()
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python
|
||||
#!/usr/bin/python2
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2016-2017 Lenovo
|
||||
@@ -39,11 +39,13 @@ import confluent.sortutil as sortutil
|
||||
|
||||
def run():
|
||||
argparser = optparse.OptionParser(
|
||||
usage="Usage: %prog noderange commandexpression",
|
||||
usage="Usage: %prog [options] noderange commandexpression",
|
||||
epilog="Expressions are the same as in attributes, e.g. "
|
||||
"'ipmitool -H {hardwaremanagement.manager}' will be expanded.")
|
||||
argparser.add_option('-f', '-c', '--count', type='int', default=168,
|
||||
help='Number of commands to run at a time')
|
||||
argparser.add_option('-n', '--nonodeprefix', action='store_true',
|
||||
help='Do not prefix output with node names')
|
||||
# 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()
|
||||
@@ -85,7 +87,7 @@ def run():
|
||||
desc = pipedesc[r]
|
||||
node = desc['node']
|
||||
data = True
|
||||
while data and select.select([r], [], [], 0):
|
||||
while data and select.select([r], [], [], 0)[0]:
|
||||
data = r.readline()
|
||||
if data:
|
||||
if desc['type'] == 'stdout':
|
||||
@@ -93,7 +95,10 @@ def run():
|
||||
pernodeout[node] = []
|
||||
pernodeout[node].append(data)
|
||||
else:
|
||||
sys.stderr.write('{0}: {1}'.format(node, data))
|
||||
if options.nonodeprefix:
|
||||
sys.stderr.write(data)
|
||||
else:
|
||||
sys.stderr.write('{0}: {1}'.format(node, data))
|
||||
sys.stderr.flush()
|
||||
else:
|
||||
pop = desc['popen']
|
||||
@@ -107,7 +112,10 @@ def run():
|
||||
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))
|
||||
if options.nonodeprefix:
|
||||
sys.stdout.write(line)
|
||||
else:
|
||||
sys.stdout.write('{0}: {1}'.format(node, line))
|
||||
sys.stdout.flush()
|
||||
if all:
|
||||
rdy, _, _ = select.select(all, [], [], 10)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python
|
||||
#!/usr/bin/python2
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2015-2017 Lenovo
|
||||
@@ -114,7 +114,8 @@ def sensorpass(showout=True, appendtime=False):
|
||||
continue
|
||||
for redundant_state in ('Non-Critical', 'Critical'):
|
||||
try:
|
||||
sensedata['states'].remove(redundant_state)
|
||||
if sensedata.get('states', False):
|
||||
sensedata['states'].remove(redundant_state)
|
||||
except ValueError:
|
||||
pass
|
||||
resultdata[node][sensedata['name']] = sensedata
|
||||
@@ -132,11 +133,12 @@ def sensorpass(showout=True, appendtime=False):
|
||||
showval = u' {0} '.format(sensedata['value'])
|
||||
if sensedata['units'] not in (None, u''):
|
||||
showval += sensedata['units']
|
||||
if sensedata['health'] != 'ok':
|
||||
if sensedata.get('health', 'ok') != 'ok':
|
||||
datadescription = [sensedata['health']]
|
||||
else:
|
||||
datadescription = []
|
||||
datadescription.extend(sensedata['states'])
|
||||
if sensedata.get('states', False):
|
||||
datadescription.extend(sensedata['states'])
|
||||
if datadescription:
|
||||
if showval == '':
|
||||
showval += u' {0}'.format(
|
||||
@@ -148,7 +150,7 @@ def sensorpass(showout=True, appendtime=False):
|
||||
showval += ' @' + time.strftime(
|
||||
'%Y-%m-%dT%H:%M:%S')
|
||||
print(u'{0}: {1}:{2}'.format(
|
||||
node, sensedata['name'], showval).encode('utf-8'))
|
||||
node, sensedata['name'], showval).encode('utf8'))
|
||||
sys.stdout.flush()
|
||||
return resultdata
|
||||
|
||||
@@ -156,7 +158,7 @@ def sensorpass(showout=True, appendtime=False):
|
||||
def format_csv(csvwriter, orderedsensors, resdata, showtime=True):
|
||||
for nodekey in resdata:
|
||||
if showtime:
|
||||
if showtime.is_integer():
|
||||
if isinstance(showtime, int):
|
||||
rowdata = [time.strftime('%Y-%m-%dT%H:%M:%S'), nodekey]
|
||||
else:
|
||||
rowdata = [time.strftime('%Y-%m-%dT%H:%M:%S.') +
|
||||
@@ -183,6 +185,7 @@ def format_csv(csvwriter, orderedsensors, resdata, showtime=True):
|
||||
except KeyError:
|
||||
rowdata.append('N/A')
|
||||
csvwriter.writerow(rowdata)
|
||||
sys.stdout.flush()
|
||||
|
||||
|
||||
def main():
|
||||
@@ -220,7 +223,7 @@ def main():
|
||||
sys.exit(exitcode)
|
||||
sleeptime = nextstart - os.times()[4]
|
||||
if sleeptime > 0:
|
||||
time.sleep(nextstart - os.times()[4])
|
||||
time.sleep(sleeptime)
|
||||
else:
|
||||
if options.csv:
|
||||
format_csv(csvwriter, orderedsensors, resdata, showtime=False)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python
|
||||
#!/usr/bin/python2
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2015 Lenovo
|
||||
@@ -53,8 +53,8 @@ except IndexError:
|
||||
argparser.print_help()
|
||||
sys.exit(1)
|
||||
bootdev = None
|
||||
if len(sys.argv) > 2:
|
||||
bootdev = sys.argv[2]
|
||||
if len(args) > 1:
|
||||
bootdev = args[1]
|
||||
if bootdev in ('net', 'pxe'):
|
||||
bootdev = 'network'
|
||||
session = client.Command()
|
||||
@@ -65,4 +65,4 @@ else:
|
||||
bootmode = 'uefi'
|
||||
sys.exit(session.simple_noderange_command(noderange, '/boot/nextdevice', bootdev,
|
||||
bootmode=bootmode,
|
||||
persistent=options.persist))
|
||||
persistent=options.persist))
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python
|
||||
#!/usr/bin/python2
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2016-2017 Lenovo
|
||||
@@ -39,11 +39,13 @@ import confluent.sortutil as sortutil
|
||||
def run():
|
||||
|
||||
argparser = optparse.OptionParser(
|
||||
usage="Usage: %prog noderange commandexpression",
|
||||
usage="Usage: %prog [options] noderange commandexpression",
|
||||
epilog="Expressions are the same as in attributes, e.g. "
|
||||
"'ipmitool -H {hardwaremanagement.manager}' will be expanded.")
|
||||
argparser.add_option('-f', '-c', '--count', type='int', default=168,
|
||||
help='Number of commands to run at a time')
|
||||
argparser.add_option('-n', '--nonodeprefix', action='store_true',
|
||||
help='Do not prefix output with node names')
|
||||
# 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()
|
||||
@@ -94,7 +96,10 @@ def run():
|
||||
pernodeout[node] = []
|
||||
pernodeout[node].append(data)
|
||||
else:
|
||||
sys.stderr.write('{0}: {1}'.format(node, data))
|
||||
if options.nonodeprefix:
|
||||
sys.stderr.write(data)
|
||||
else:
|
||||
sys.stderr.write('{0}: {1}'.format(node, data))
|
||||
sys.stderr.flush()
|
||||
else:
|
||||
pop = desc['popen']
|
||||
@@ -108,7 +113,10 @@ def run():
|
||||
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))
|
||||
if options.nonodeprefix:
|
||||
sys.stdout.write(line)
|
||||
else:
|
||||
sys.stdout.write('{0}: {1}'.format(node, line))
|
||||
sys.stdout.flush()
|
||||
if all:
|
||||
rdy, _, _ = select.select(all, [], [], 10)
|
||||
|
||||
@@ -0,0 +1,224 @@
|
||||
#!/usr/bin/python2
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2018 Lenovo
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import optparse
|
||||
import os
|
||||
import signal
|
||||
import sys
|
||||
|
||||
try:
|
||||
signal.signal(signal.SIGPIPE, signal.SIG_DFL)
|
||||
except AttributeError:
|
||||
pass
|
||||
path = os.path.dirname(os.path.realpath(__file__))
|
||||
path = os.path.realpath(os.path.join(path, '..', 'lib', 'python'))
|
||||
if path.startswith('/opt'):
|
||||
sys.path.append(path)
|
||||
|
||||
import confluent.client as client
|
||||
exitcode = 0
|
||||
|
||||
class OptParser(optparse.OptionParser):
|
||||
|
||||
def format_epilog(self, formatter):
|
||||
return self.expand_prog_name(self.epilog)
|
||||
|
||||
def mbtohuman(mb):
|
||||
if mb > 1000000:
|
||||
return '{0:.3f} TB'.format(mb/1000000.0)
|
||||
if mb > 1000:
|
||||
return '{0:.3f} GB'.format(mb/1000.0)
|
||||
return '{0:.3f} MB'.format(mb)
|
||||
|
||||
def showstorage(noderange, options, args):
|
||||
global exitcode
|
||||
session = client.Command()
|
||||
disks = {}
|
||||
arrays = {}
|
||||
volumes = {}
|
||||
scfg = session.read('/noderange/{0}/configuration/storage/all'.format(
|
||||
noderange))
|
||||
_print_cfg(scfg)
|
||||
|
||||
|
||||
def _print_cfg(scfg):
|
||||
global exitcode
|
||||
storagebynode = {}
|
||||
for e in scfg:
|
||||
if 'error' in e:
|
||||
sys.stderr.write(e['error'] + '\n')
|
||||
exitcode = e.get('errorcode', 1)
|
||||
for node in e.get('databynode', {}):
|
||||
if node not in storagebynode:
|
||||
storagebynode[node] = {'disks': [], 'arrays': [],
|
||||
'volumes': []}
|
||||
curr = e['databynode'][node]
|
||||
storagebynode[node][curr['type'] + 's'].append(curr)
|
||||
for node in storagebynode:
|
||||
for disk in sorted(storagebynode[node]['disks'],
|
||||
key=lambda x: x['name']):
|
||||
print('{0}: Disk {1} Description: {2}'.format(
|
||||
node, disk['name'], disk['description']))
|
||||
print('{0}: Disk {1} State: {2}'.format(node, disk['name'],
|
||||
disk['state']))
|
||||
print('{0}: Disk {1} FRU: {2}'.format(node, disk['name'],
|
||||
disk['fru']))
|
||||
print('{0}: Disk {1} Serial Number: {2}'.format(node, disk['name'],
|
||||
disk['serial']))
|
||||
if disk['array']:
|
||||
print('{0}: Disk {1} Array: {2}'.format(node, disk['name'],
|
||||
disk['array']))
|
||||
for arr in storagebynode[node]['arrays']:
|
||||
print('{0}: Array {1} Available Capacity: {2}'.format(
|
||||
node, arr['id'], mbtohuman(arr['available'])))
|
||||
print('{0}: Array {1} Total Capacity: {2}'.format(
|
||||
node, arr['id'], mbtohuman(arr['capacity'])))
|
||||
print('{0}: Array {1} RAID: {2}'.format(node, arr['id'],
|
||||
arr['raid']))
|
||||
print('{0}: Array {1} Disks: {2}'.format(node, arr['id'], ','.join(
|
||||
arr['disks'])))
|
||||
print(
|
||||
'{0}: Array {1} Volumes: {2}'.format(node, arr['id'], ','.join(
|
||||
arr['volumes'])))
|
||||
for vol in storagebynode[node]['volumes']:
|
||||
print('{0}: Volume {1}: Size: {2}'.format(node, vol['name'],
|
||||
mbtohuman(vol['size'])))
|
||||
print('{0}: Volume {1}: State: {2}'.format(node, vol['name'],
|
||||
vol['state']))
|
||||
print('{0}: Volume {1}: Array {2}'.format(node, vol['name'],
|
||||
vol['array']))
|
||||
|
||||
|
||||
def createstorage(noderange, options, args):
|
||||
if options.raidlevel is None or options.disks is None:
|
||||
sys.stderr.write('-r and -d are required arguments to create array\n')
|
||||
sys.exit(1)
|
||||
session = client.Command()
|
||||
names = options.name
|
||||
if names is None:
|
||||
names = ''.join(args)
|
||||
parms = {'disks': options.disks, 'raidlevel': options.raidlevel,
|
||||
'name': names}
|
||||
if options.size:
|
||||
parms['size'] = options.size
|
||||
if options.stripsizes:
|
||||
parms['stripsizes'] = options.stripsizes
|
||||
_print_cfg(session.create(
|
||||
'/noderange/{0}/configuration/storage/volumes/{1}'.format(
|
||||
noderange, names), parms))
|
||||
|
||||
|
||||
def deletestorage(noderange, options, args):
|
||||
if options.name is None:
|
||||
if len(args) == 1:
|
||||
names = args[0]
|
||||
else:
|
||||
sys.stderr.write('-n is required to indicate volume(s) to delete\n')
|
||||
sys.exit(1)
|
||||
else:
|
||||
names = options.name
|
||||
session = client.Command()
|
||||
for rsp in session.delete(
|
||||
'/noderange/{0}/configuration/storage/volumes/{1}'.format(
|
||||
noderange, names)):
|
||||
if 'deleted' in rsp:
|
||||
print('Deleted: {0}'.format(rsp['deleted']))
|
||||
elif 'databynode' in rsp:
|
||||
for node in rsp['databynode']:
|
||||
if 'error' in rsp['databynode'][node]:
|
||||
sys.stderr.write('{0}: {1}\n'.format(
|
||||
node, rsp['databynode'][node]['error']))
|
||||
else:
|
||||
sys.stderr.write('{0}: {1}\n'.format(
|
||||
node, repr(rsp['databynode'][node])))
|
||||
else:
|
||||
print(repr(rsp))
|
||||
|
||||
|
||||
def setdisk(noderange, options, args):
|
||||
if options.disks is None:
|
||||
if len(args):
|
||||
names = args.pop(0)
|
||||
else:
|
||||
sys.stderr.write('-d is required to indicate disk to modify\n')
|
||||
sys.exit(1)
|
||||
else:
|
||||
names = options.disks
|
||||
if not len(args) or args[0] not in ('hotspare', 'jbod', 'unconfigured'):
|
||||
sys.stderr.write('diskset requires valid state as argument (hotspare, jbod, unconfigured)\n')
|
||||
sys.exit(1)
|
||||
session = client.Command()
|
||||
scfg = session.update('/noderange/{0}/configuration/storage/disks/{1}'.format(noderange, names), {'state': args[0]})
|
||||
_print_cfg(scfg)
|
||||
|
||||
funmap = {
|
||||
'create': createstorage,
|
||||
'show': showstorage,
|
||||
'diskset': setdisk,
|
||||
'delete': deletestorage,
|
||||
'rm': deletestorage,
|
||||
}
|
||||
|
||||
|
||||
|
||||
def main():
|
||||
argparser = OptParser(
|
||||
usage='Usage: %prog <noderange> [show|create|delete|diskset]',
|
||||
epilog='',
|
||||
)
|
||||
argparser.add_option('-r', '--raidlevel', type='int',
|
||||
help='RAID level to use when creating an array')
|
||||
argparser.add_option('-d', '--disks', type='str',
|
||||
help='Comma separated list of disks to use, or the '
|
||||
'word "rest" to indicate use of all available '
|
||||
'disks')
|
||||
argparser.add_option('-s', '--size', type='str',
|
||||
help='Comma separated list of sizes to use when '
|
||||
'creating volumes. The sizes may be absolute '
|
||||
'size (e.g. 16gb), percentage (10%) or the word '
|
||||
'"rest" to use remaining capacity, default '
|
||||
'behavior is to use all capacity to make a '
|
||||
'volume')
|
||||
argparser.add_option('-n', '--name', type='str',
|
||||
help='Comma separated list of names to use when '
|
||||
'naming volumes, or selecting a volume for '
|
||||
'delete. Default behavior is to use '
|
||||
'implementation provided default names.')
|
||||
argparser.add_option('-z', '--stripsizes', type='str',
|
||||
help='Comma separated list of stripsizes to use when creating volumes. '
|
||||
'This value is in kilobytes. The default behavior is to allow the '
|
||||
'storage controller to decide.')
|
||||
(options, args) = argparser.parse_args()
|
||||
if len(args) == 1:
|
||||
args.append('show')
|
||||
try:
|
||||
noderange = args[0]
|
||||
operation = args[1]
|
||||
except IndexError:
|
||||
argparser.print_help()
|
||||
sys.exit(1)
|
||||
client.check_globbing(noderange)
|
||||
try:
|
||||
handler = funmap[operation]
|
||||
except KeyError:
|
||||
argparser.print_help()
|
||||
sys.exit(1)
|
||||
handler(noderange, options, args[2:])
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,147 @@
|
||||
#!/usr/bin/python2
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2018 Lenovo
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import optparse
|
||||
import os
|
||||
import signal
|
||||
import sys
|
||||
import time
|
||||
|
||||
try:
|
||||
signal.signal(signal.SIGPIPE, signal.SIG_DFL)
|
||||
except AttributeError:
|
||||
pass
|
||||
path = os.path.dirname(os.path.realpath(__file__))
|
||||
path = os.path.realpath(os.path.join(path, '..', 'lib', 'python'))
|
||||
if path.startswith('/opt'):
|
||||
sys.path.append(path)
|
||||
|
||||
import confluent.client as client
|
||||
import confluent.screensqueeze as sq
|
||||
|
||||
exitcode = 0
|
||||
|
||||
|
||||
def get_update_progress(session, url):
|
||||
for res in session.read(url):
|
||||
status = res['phase']
|
||||
percent = res['progress']
|
||||
detail = res['detail']
|
||||
if status == 'error':
|
||||
text = 'error!'
|
||||
else:
|
||||
text = '{0}: {1:3.0f}%'.format(status, percent)
|
||||
return text, status, detail
|
||||
|
||||
|
||||
def printerror(res, node=None):
|
||||
global exitcode
|
||||
if 'errorcode' in res:
|
||||
exitcode = res['errorcode']
|
||||
if 'error' in res:
|
||||
if node:
|
||||
sys.stderr.write('{0}: {1}\n'.format(node, res['error']))
|
||||
else:
|
||||
sys.stderr.write('{0}\n'.format(res['error']))
|
||||
if 'errorcode' not in res:
|
||||
exitcode = 1
|
||||
for node in res.get('databynode', ()):
|
||||
printerror(res['databynode'][node], node)
|
||||
|
||||
|
||||
|
||||
def download_servicedata(noderange, media):
|
||||
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. It is saved to the location on the relevant '
|
||||
'management server (the confluent server if running remote, '
|
||||
'and the collective.manager if in collective)\n'
|
||||
'\n\nSee `man %prog` for more info.\n')
|
||||
(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()
|
||||
Executable
+167
@@ -0,0 +1,167 @@
|
||||
#!/usr/bin/python2
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2019 Lenovo
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import argparse
|
||||
import csv
|
||||
import fcntl
|
||||
import io
|
||||
import numpy as np
|
||||
|
||||
import os
|
||||
import sixel
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
class DumbWriter(sixel.SixelWriter):
|
||||
def restore_position(self, output):
|
||||
return
|
||||
|
||||
|
||||
def plot(gui, output, plotdata, bins):
|
||||
import matplotlib as mpl
|
||||
if not gui:
|
||||
mpl.use('Agg')
|
||||
import matplotlib.pyplot as plt
|
||||
n, bins, patches = plt.hist(plotdata, bins)
|
||||
plt.show()
|
||||
if not gui:
|
||||
if output:
|
||||
tdata = output
|
||||
else:
|
||||
tdata = io.BytesIO()
|
||||
plt.savefig(tdata)
|
||||
if not gui and not output:
|
||||
writer = DumbWriter()
|
||||
writer.draw(tdata)
|
||||
return n, bins
|
||||
|
||||
def textplot(plotdata, bins):
|
||||
n, bins = np.histogram(plotdata, bins)
|
||||
labels = []
|
||||
for bin in bins:
|
||||
labels.append('{0:0.1f}'.format(bin))
|
||||
width = 80
|
||||
# Since this will be primarily piped into, hard to get
|
||||
# terminal width
|
||||
labelwidth = 0
|
||||
for lab in labels:
|
||||
if len(lab) > labelwidth:
|
||||
labelwidth = len(lab)
|
||||
width -= (labelwidth) + 1
|
||||
labelfmt = '{{0:>{0}s}}|'.format(labelwidth)
|
||||
maxn = 0.0
|
||||
for lgth in n:
|
||||
if lgth > maxn:
|
||||
maxn = float(lgth)
|
||||
for i in range(len(n)):
|
||||
print(labelfmt.format(labels[i]) + '=' * int(np.round((n[i]/maxn) * width)))
|
||||
return n, bins
|
||||
|
||||
histogram = False
|
||||
aparser = argparse.ArgumentParser(description='Quick access to common statistics')
|
||||
aparser.add_argument('-c', type=int, default=0, help='Column number to analyze (default is last column)')
|
||||
aparser.add_argument('-d', default=None, help='Value used to separate columns')
|
||||
aparser.add_argument('-x', default=False, action='store_true', help='Output histogram in sixel format')
|
||||
aparser.add_argument('-s', default=0, help='Number of header lines to skip before processing')
|
||||
aparser.add_argument('-g', default=False, action='store_true', help='Open histogram in separate graphical window')
|
||||
aparser.add_argument('-o', default=None, help='Output histogram to the specified filename in PNG format')
|
||||
aparser.add_argument('-t', default=False, action='store_true', help='Output a histogram in text format')
|
||||
aparser.add_argument('-v', default=False, action='store_true', help='Attempt to list nodes relevant to each histogram bar (requires -s, -o, or -t)')
|
||||
aparser.add_argument('-b', type=int, default=10, help='Number of bins to use in histogram (default is 10)')
|
||||
args = aparser.parse_args(sys.argv[1:])
|
||||
plotdata = []
|
||||
headlines = int(args.s)
|
||||
while headlines >= 0:
|
||||
data = sys.stdin.readline()
|
||||
headlines -= 1
|
||||
if args.d:
|
||||
delimiter = args.d
|
||||
else:
|
||||
if '\t' in data:
|
||||
delimiter = '\t'
|
||||
elif ' ' in data:
|
||||
delimiter = ' '
|
||||
elif ',' in data:
|
||||
delimiter = ','
|
||||
else:
|
||||
delimiter = ' ' # handle single column
|
||||
data = list(csv.reader([data], delimiter=delimiter))[0]
|
||||
nodebydatum = {}
|
||||
idx = args.c - 1
|
||||
autoidx = False
|
||||
while data:
|
||||
node = None
|
||||
if ':' in data[0]:
|
||||
node, data[0] = data[0].split(':', 1)
|
||||
else:
|
||||
node = data[0]
|
||||
if idx == -1 and not autoidx:
|
||||
while not autoidx:
|
||||
try:
|
||||
datum = float(data[idx])
|
||||
except ValueError:
|
||||
idx -= 1
|
||||
continue
|
||||
except IndexError:
|
||||
sys.stderr.write('Unable to identify a numerical column\n')
|
||||
sys.exit(1)
|
||||
autoidx = True
|
||||
else:
|
||||
datum = float(data[idx])
|
||||
if node:
|
||||
if datum in nodebydatum:
|
||||
nodebydatum[datum].add(node)
|
||||
else:
|
||||
nodebydatum[datum] = set([node])
|
||||
plotdata.append(datum)
|
||||
data = sys.stdin.readline()
|
||||
data = list(csv.reader([data], delimiter=delimiter))[0]
|
||||
n = None
|
||||
if args.g or args.o or args.x:
|
||||
n, bins = plot(args.g, args.o, plotdata, bins=args.b)
|
||||
if args.t:
|
||||
n, bins = textplot(plotdata, bins=args.b)
|
||||
print('Samples: {5} Min: {3} Median: {0} Mean: {1} Max: {4} StandardDeviation: {2} Sum: {6}'.format(np.median(plotdata), np.mean(plotdata), np.std(plotdata), np.min(plotdata), np.max(plotdata), len(plotdata), np.sum(plotdata)))
|
||||
if args.v and n is not None and nodebydatum:
|
||||
print('')
|
||||
currbin = bins[0]
|
||||
bins = bins[1:]
|
||||
currbinmembers = []
|
||||
for datum in sorted(nodebydatum):
|
||||
if datum > bins[0]:
|
||||
nextbin = None
|
||||
endbin = bins[0]
|
||||
while len(bins) and bins[0] < datum:
|
||||
nextbin = bins[0]
|
||||
bins = bins[1:]
|
||||
if not nextbin:
|
||||
nextbin = np.max(plotdata)
|
||||
print('Entries between {0} and {1}'.format(currbin, endbin))
|
||||
currbin = nextbin
|
||||
print('-' * 80)
|
||||
print(','.join(sorted(currbinmembers)))
|
||||
print('')
|
||||
print('')
|
||||
currbinmembers = []
|
||||
for node in nodebydatum[datum]:
|
||||
currbinmembers.append(node)
|
||||
if currbinmembers:
|
||||
print('Entries between {0} and {1}'.format(currbin, np.max(plotdata)))
|
||||
print('-' * 80)
|
||||
print(','.join(sorted(currbinmembers)))
|
||||
print('')
|
||||
print('')
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../confluent_server/builddeb
|
||||
@@ -1,7 +1,7 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2014 IBM Corporation
|
||||
# Copyright 2015-2018 Lenovo
|
||||
# Copyright 2015-2019 Lenovo
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -15,8 +15,13 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import anydbm as dbm
|
||||
try:
|
||||
import anydbm as dbm
|
||||
except ImportError:
|
||||
import dbm
|
||||
import csv
|
||||
import errno
|
||||
import fnmatch
|
||||
import hashlib
|
||||
import os
|
||||
import shlex
|
||||
@@ -24,6 +29,7 @@ import socket
|
||||
import ssl
|
||||
import sys
|
||||
import confluent.tlvdata as tlvdata
|
||||
import confluent.sortutil as sortutil
|
||||
|
||||
SO_PASSCRED = 16
|
||||
|
||||
@@ -34,6 +40,58 @@ _attraliases = {
|
||||
}
|
||||
|
||||
|
||||
class Tabulator(object):
|
||||
def __init__(self, headers):
|
||||
self.headers = headers
|
||||
self.rows = []
|
||||
|
||||
def add_row(self, row):
|
||||
self.rows.append(row)
|
||||
|
||||
def get_table(self, order=None):
|
||||
i = 0
|
||||
fmtstr = ''
|
||||
separator = []
|
||||
for head in self.headers:
|
||||
if order and order == head:
|
||||
order = i
|
||||
neededlen = len(head)
|
||||
for row in self.rows:
|
||||
if len(row[i]) > neededlen:
|
||||
neededlen = len(row[i])
|
||||
separator.append('-' * (neededlen + 1))
|
||||
fmtstr += '{{{0}:>{1}}}|'.format(i, neededlen + 1)
|
||||
i = i + 1
|
||||
fmtstr = fmtstr[:-1]
|
||||
yield fmtstr.format(*self.headers)
|
||||
yield fmtstr.format(*separator)
|
||||
if order is not None:
|
||||
for row in sorted(
|
||||
self.rows,
|
||||
key=lambda x: sortutil.naturalize_string(x[order])):
|
||||
yield fmtstr.format(*row)
|
||||
else:
|
||||
for row in self.rows:
|
||||
yield fmtstr.format(*row)
|
||||
|
||||
def write_csv(self, output, order=None):
|
||||
output = csv.writer(output)
|
||||
output.writerow(self.headers)
|
||||
i = 0
|
||||
for head in self.headers:
|
||||
if order and order == head:
|
||||
order = i
|
||||
i = i + 1
|
||||
if order is not None:
|
||||
for row in sorted(
|
||||
self.rows,
|
||||
key=lambda x: sortutil.naturalize_string(x[order])):
|
||||
output.writerow(row)
|
||||
else:
|
||||
for row in self.rows:
|
||||
output.writerow(row)
|
||||
|
||||
|
||||
def printerror(res, node=None):
|
||||
exitcode = 0
|
||||
if 'errorcode' in res:
|
||||
@@ -49,7 +107,10 @@ def printerror(res, node=None):
|
||||
|
||||
|
||||
def cprint(txt):
|
||||
print(txt)
|
||||
try:
|
||||
print(txt)
|
||||
except UnicodeEncodeError:
|
||||
print(txt.encode('utf8'))
|
||||
sys.stdout.flush()
|
||||
|
||||
def _parseserver(string):
|
||||
@@ -81,6 +142,8 @@ class Command(object):
|
||||
self.serverloc = server
|
||||
if os.path.isabs(self.serverloc) and os.path.exists(self.serverloc):
|
||||
self._connect_unix()
|
||||
elif self.serverloc == '/var/run/confluent/api.sock':
|
||||
raise Exception('Confluent service is not available')
|
||||
else:
|
||||
self._connect_tls()
|
||||
tlvdata.recv(self.connection)
|
||||
@@ -265,14 +328,19 @@ class Command(object):
|
||||
if knownhosts:
|
||||
certdata = self.connection.getpeercert(binary_form=True)
|
||||
fingerprint = 'sha512$' + hashlib.sha512(certdata).hexdigest()
|
||||
fingerprint = fingerprint.encode('utf-8')
|
||||
hostid = '@'.join((port, server))
|
||||
khf = dbm.open(os.path.join(clientcfgdir, "knownhosts"), 'c', 384)
|
||||
if hostid in khf:
|
||||
if fingerprint == khf[hostid]:
|
||||
return
|
||||
else:
|
||||
replace = raw_input(
|
||||
"MISMATCHED CERTIFICATE DATA, ACCEPT NEW? (y/n):")
|
||||
try:
|
||||
replace = raw_input(
|
||||
"MISMATCHED CERTIFICATE DATA, ACCEPT NEW? (y/n):")
|
||||
except NameError:
|
||||
replace = input(
|
||||
"MISMATCHED CERTIFICATE DATA, ACCEPT NEW? (y/n):")
|
||||
if replace not in ('y', 'Y'):
|
||||
raise Exception("BAD CERTIFICATE")
|
||||
cprint('Adding new key for %s:%s' % (server, port))
|
||||
@@ -311,7 +379,7 @@ def attrrequested(attr, attrlist, seenattributes):
|
||||
candidate = candidate.replace('hm', 'hardwaremanagement', 1)
|
||||
if candidate in _attraliases:
|
||||
candidate = _attraliases[candidate]
|
||||
if candidate.lower() == attr.lower():
|
||||
if fnmatch.fnmatch(attr.lower(), candidate.lower()):
|
||||
seenattributes.add(truename)
|
||||
return True
|
||||
elif attr.lower().startswith(candidate.lower() + '.'):
|
||||
@@ -325,7 +393,7 @@ def printattributes(session, requestargs, showtype, nodetype, noderange, options
|
||||
return print_attrib_path(path, session, requestargs, options)
|
||||
|
||||
|
||||
def print_attrib_path(path, session, requestargs, options, rename=None):
|
||||
def print_attrib_path(path, session, requestargs, options, rename=None, attrprefix=None):
|
||||
exitcode = 0
|
||||
seenattributes = set([])
|
||||
for res in session.read(path):
|
||||
@@ -336,7 +404,7 @@ def print_attrib_path(path, session, requestargs, options, rename=None):
|
||||
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):
|
||||
key=lambda k: k[1].get('sortid', k[0]) if isinstance(k[1], dict) else k[0]):
|
||||
if attr == 'error':
|
||||
sys.stderr.write('{0}: Error: {1}\n'.format(node, val))
|
||||
continue
|
||||
@@ -348,6 +416,8 @@ def print_attrib_path(path, session, requestargs, options, rename=None):
|
||||
printattr = rename.get(attr, attr)
|
||||
else:
|
||||
printattr = attr
|
||||
if attrprefix:
|
||||
printattr = attrprefix + printattr
|
||||
currattr = res['databynode'][node][attr]
|
||||
if show_attr(attr, requestargs, seenattributes, options):
|
||||
if 'value' in currattr:
|
||||
@@ -407,9 +477,12 @@ def print_attrib_path(path, session, requestargs, options, rename=None):
|
||||
(currattr.get('default', None) is not None and
|
||||
currattr.get('value', None) is not None and
|
||||
currattr['value'] != currattr['default'])):
|
||||
cval = ','.join(currattr['value']) if isinstance(
|
||||
currattr['value'], list) else currattr['value']
|
||||
dval = ','.join(currattr['default']) if isinstance(
|
||||
currattr['default'], list) else currattr['default']
|
||||
cprint('{0}: {1}: {2} (Default: {3})'.format(
|
||||
node, printattr, currattr['value'],
|
||||
currattr['default']))
|
||||
node, printattr, cval, dval))
|
||||
else:
|
||||
|
||||
try:
|
||||
@@ -418,11 +491,14 @@ def print_attrib_path(path, session, requestargs, options, rename=None):
|
||||
details = False
|
||||
if details:
|
||||
if currattr.get('help', None):
|
||||
attrout += ' (Help: {0})'.format(
|
||||
currattr['help'].encode('utf-8'))
|
||||
attrout += u' (Help: {0})'.format(
|
||||
currattr['help'])
|
||||
if currattr.get('possible', None):
|
||||
attrout += ' (Choices: {0})'.format(
|
||||
','.join(currattr['possible']))
|
||||
try:
|
||||
attrout += u' (Choices: {0})'.format(
|
||||
','.join(currattr['possible']))
|
||||
except TypeError:
|
||||
pass
|
||||
cprint(attrout)
|
||||
if not exitcode:
|
||||
if requestargs:
|
||||
@@ -493,7 +569,7 @@ def printgroupattributes(session, requestargs, showtype, nodetype, noderange, op
|
||||
exitcode = 1
|
||||
return exitcode
|
||||
|
||||
def updateattrib(session, updateargs, nodetype, noderange, options):
|
||||
def updateattrib(session, updateargs, nodetype, noderange, options, dictassign=None):
|
||||
# update attribute
|
||||
exitcode = 0
|
||||
if options.clear:
|
||||
@@ -526,6 +602,14 @@ def updateattrib(session, updateargs, nodetype, noderange, options):
|
||||
'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:
|
||||
@@ -584,4 +668,4 @@ def check_globbing(noderange):
|
||||
'bash or change directories such that there is no filename '
|
||||
'that would conflict.'
|
||||
'\n'.format(noderange))
|
||||
sys.exit(1)
|
||||
sys.exit(1)
|
||||
|
||||
@@ -44,6 +44,8 @@ class ScreenPrinter(object):
|
||||
self.fieldwidth = maxlen + textlen + 1 # 1 for column
|
||||
|
||||
def set_output(self, node, text):
|
||||
if self.nodeoutput[node] == text:
|
||||
return
|
||||
self.nodeoutput[node] = text
|
||||
if len(text) >= self.textlen:
|
||||
self.textlen = len(text) + 1
|
||||
|
||||
@@ -169,13 +169,18 @@ class GroupedData(object):
|
||||
output.flush()
|
||||
|
||||
def print_deviants(self, output=sys.stdout, skipmodal=False, reverse=False,
|
||||
count=False):
|
||||
count=False, basenode=None):
|
||||
self.generate_byoutput()
|
||||
modaloutput = None
|
||||
ismodal = True
|
||||
revoutput = []
|
||||
if basenode:
|
||||
for checkout in self.byoutput:
|
||||
if basenode in self.byoutput[checkout]:
|
||||
modaloutput = checkout
|
||||
for outdata in sorted(
|
||||
self.byoutput, key=lambda x: [0 - len(self.byoutput[x]),
|
||||
self.byoutput, key=lambda x: [0 if modaloutput == x else 1,
|
||||
0 - len(self.byoutput[x]),
|
||||
humanify_nodename(
|
||||
self.get_group_text(
|
||||
self.byoutput[x]
|
||||
|
||||
@@ -25,6 +25,11 @@ try:
|
||||
except NameError:
|
||||
unicode = str
|
||||
|
||||
try:
|
||||
range = xrange
|
||||
except NameError:
|
||||
pass
|
||||
|
||||
def decodestr(value):
|
||||
ret = None
|
||||
try:
|
||||
@@ -34,11 +39,13 @@ def decodestr(value):
|
||||
ret = value.decode('cp437')
|
||||
except UnicodeDecodeError:
|
||||
ret = value
|
||||
except AttributeError:
|
||||
return value
|
||||
return ret
|
||||
|
||||
def unicode_dictvalues(dictdata):
|
||||
for key in dictdata:
|
||||
if isinstance(dictdata[key], str):
|
||||
if isinstance(dictdata[key], bytes):
|
||||
dictdata[key] = decodestr(dictdata[key])
|
||||
elif isinstance(dictdata[key], datetime):
|
||||
dictdata[key] = dictdata[key].strftime('%Y-%m-%dT%H:%M:%S')
|
||||
@@ -49,7 +56,7 @@ def unicode_dictvalues(dictdata):
|
||||
|
||||
|
||||
def _unicode_list(currlist):
|
||||
for i in xrange(len(currlist)):
|
||||
for i in range(len(currlist)):
|
||||
if isinstance(currlist[i], str):
|
||||
currlist[i] = decodestr(currlist[i])
|
||||
elif isinstance(currlist[i], dict):
|
||||
@@ -64,7 +71,7 @@ def send(handle, data):
|
||||
data = data.encode('utf-8')
|
||||
except AttributeError:
|
||||
pass
|
||||
if isinstance(data, str) or isinstance(data, unicode):
|
||||
if isinstance(data, bytes) or isinstance(data, unicode):
|
||||
# plain text, e.g. console data
|
||||
tl = len(data)
|
||||
if tl == 0:
|
||||
|
||||
@@ -24,10 +24,18 @@ a confluent server.
|
||||
%setup -n %{name}-%{version} -n %{name}-%{version}
|
||||
|
||||
%build
|
||||
python setup.py build
|
||||
%if "%{dist}" == ".el8"
|
||||
python3 setup.py build
|
||||
%else
|
||||
python2 setup.py build
|
||||
%endif
|
||||
|
||||
%install
|
||||
python setup.py install --single-version-externally-managed -O1 --root=$RPM_BUILD_ROOT --record=INSTALLED_FILES --install-scripts=/opt/confluent/bin --install-purelib=/opt/confluent/lib/python
|
||||
%if "%{dist}" == ".el8"
|
||||
python3 setup.py install --single-version-externally-managed -O1 --root=$RPM_BUILD_ROOT --record=INSTALLED_FILES --install-scripts=/opt/confluent/bin --install-purelib=/opt/confluent/lib/python
|
||||
%else
|
||||
python2 setup.py install --single-version-externally-managed -O1 --root=$RPM_BUILD_ROOT --record=INSTALLED_FILES --install-scripts=/opt/confluent/bin --install-purelib=/opt/confluent/lib/python
|
||||
%endif
|
||||
|
||||
|
||||
%clean
|
||||
|
||||
@@ -36,4 +36,256 @@ alias nodereseat='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export C
|
||||
alias noderun='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export CURRENT_CMDLINE; noderun'
|
||||
alias nodesensors='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export CURRENT_CMDLINE; nodesensors'
|
||||
alias nodesetboot='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export CURRENT_CMDLINE; nodesetboot'
|
||||
alias nodestorage='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export CURRENT_CMDLINE; nodestorage'
|
||||
alias nodeshell='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export CURRENT_CMDLINE; nodeshell'
|
||||
alias nodelicense='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export CURRENT_CMDLINE; nodelicense'
|
||||
# Do not continue for non-bash shells, the rest of this sets up bash completion functions
|
||||
[ -z "$BASH_VERSION" -o -z "$PS1" ] && return
|
||||
|
||||
|
||||
_confluent_get_args()
|
||||
{
|
||||
CMPARGS=($COMP_LINE)
|
||||
NUMARGS=${#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,blink -h")
|
||||
_confluent_generic_completion
|
||||
}
|
||||
|
||||
|
||||
_confluent_nodesetboot_completion()
|
||||
{
|
||||
COMP_CANDIDATES=("default,cd,network,setup,hd -h -b -p")
|
||||
_confluent_generic_completion
|
||||
}
|
||||
|
||||
_confluent_nodepower_completion()
|
||||
{
|
||||
COMP_CANDIDATES=("boot,off,on,status -h -p")
|
||||
_confluent_generic_completion
|
||||
}
|
||||
|
||||
_confluent_nodemedia_completion()
|
||||
{
|
||||
COMP_CANDIDATES=("list,upload,attach,detachall -h")
|
||||
_confluent_get_args
|
||||
if [ $NUMARGS -gt 3 ] && [ ${CMPARGS[-2]} == 'upload' ]; then
|
||||
compopt -o default
|
||||
COMPREPLY=()
|
||||
return
|
||||
fi
|
||||
if [ $NUMARGS -ge 3 ] && [ ! -z "$GENNED" ]; then
|
||||
COMPREPLY=($(compgen -W "$GENNED" -- ${COMP_WORDS[-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_nodelicense_completion()
|
||||
{
|
||||
_confluent_get_args
|
||||
if [ $NUMARGS == 3 ]; then
|
||||
COMPREPLY=($(compgen -W "install list save delete" -- ${COMP_WORDS[-1]}))
|
||||
return;
|
||||
fi
|
||||
if [ $NUMARGS == 4 ] && [ ${CMPARGS[2]} == 'install' ]; then
|
||||
compopt -o default
|
||||
COMPREPLY=()
|
||||
return
|
||||
fi
|
||||
if [ $NUMARGS == 4 ] && [ ${CMPARGS[2]} == 'save' ]; then
|
||||
compopt -o dirnames
|
||||
COMPREPLY=()
|
||||
return
|
||||
fi
|
||||
if [ $NUMARGS -lt 3 ]; then
|
||||
_confluent_nr_completion
|
||||
return
|
||||
fi
|
||||
}
|
||||
|
||||
_confluent_nodesupport_completion()
|
||||
{
|
||||
_confluent_get_args
|
||||
if [ $NUMARGS == 3 ]; then
|
||||
COMPREPLY=($(compgen -W "servicedata" -- ${COMP_WORDS[-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_nodeattrib_completion()
|
||||
{
|
||||
COMP_CANDIDATES=$(nodeattrib '~.>1' all | awk '{print $2}'|sed -e 's/://')
|
||||
_confluent_generic_completion
|
||||
}
|
||||
|
||||
|
||||
_confluent_nn_completion()
|
||||
{
|
||||
_confluent_get_args
|
||||
if [ $NUMARGS -gt 2 ]; then
|
||||
return;
|
||||
fi
|
||||
INPUT=${COMP_WORDS[-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_nodeattrib_completion nodeattrib
|
||||
complete -F _confluent_nodeattrib_completion nodegroupattrib
|
||||
complete -F _confluent_nr_completion nodebmcreset
|
||||
complete -F _confluent_nodesetboot_completion nodeboot
|
||||
complete -F _confluent_nr_completion nodeconfig
|
||||
complete -F _confluent_nn_completion nodeconsole
|
||||
complete -F _confluent_nr_completion nodeeventlog
|
||||
complete -F _confluent_nodefirmware_completion nodefirmware
|
||||
complete -F _confluent_ng_completion nodegroupattrib
|
||||
complete -F _confluent_ng_completion nodegroupremove
|
||||
complete -F _confluent_nr_completion nodehealth
|
||||
complete -F _confluent_nodeidentify_completion nodeidentify
|
||||
complete -F _confluent_nr_completion nodeinventory
|
||||
complete -F _confluent_nodeattrib_completion nodelist
|
||||
complete -F _confluent_nodemedia_completion nodemedia
|
||||
complete -F _confluent_nodepower_completion nodepower
|
||||
complete -F _confluent_nr_completion noderemove
|
||||
complete -F _confluent_nr_completion nodereseat
|
||||
complete -F _confluent_nodeshell_completion noderun
|
||||
complete -F _confluent_nr_completion nodesensors
|
||||
complete -F _confluent_nodesetboot_completion nodesetboot
|
||||
complete -F _confluent_nodeshell_completion nodeshell
|
||||
complete -F _confluent_nodesupport_completion nodesupport
|
||||
complete -F _confluent_nodelicense_completion nodelicense
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ collate(1) -- Organize text input by node
|
||||
|
||||
## SYNOPSIS
|
||||
|
||||
`<other command> | collate [-a] [-d] [-w] [-s] [-c] [-r]`
|
||||
`<other command> | collate [-a] [-d] [-w] [-s] [-c] [-r] [-l lognametemplate]`
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
@@ -26,6 +26,10 @@ node and group names sorted alphanumerically.
|
||||
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.
|
||||
@@ -85,4 +89,4 @@ node and group names sorted alphanumerically.
|
||||
` Processors.ExecuteDisableBit=Enable`
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
collective(1) -- Check and manage a confluent collective
|
||||
==============================
|
||||
|
||||
## SYNOPSIS
|
||||
|
||||
`collective invite <server>`
|
||||
`collective join <server> [-i TOKEN]`
|
||||
`collective show`
|
||||
`collective gencert`
|
||||
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
**collective** helps manage the collective mode of confluent, where multiple
|
||||
confluent servers are linked together to act as one. For example, the procedure to set up
|
||||
a collective to run on three servers called mgt1, mgt2, and mgt3, first install and start
|
||||
confluent as usual on the three servers. On mgt1, run `collective invite mgt2` and an
|
||||
invitation token will be output. On mgt2, either run `collective join mgt1` to paste
|
||||
the token interactively, or `collective join mgt1 -i <token>`. At this point, either
|
||||
mgt1 or mgt2 can bring in mgt3. For example on mgt2 run `collective invite mgt3` and
|
||||
on mgt3 run `collective join mgt2 -i <token>`
|
||||
|
||||
This can be linked together in the following manner with ssh:
|
||||
on mgt1:
|
||||
`# ssh mgt2 collective join mgt1 -i $(collective invite mgt2)`
|
||||
|
||||
Note that a collective is only redundant with 3 or more members. The collective
|
||||
will function so long as more than half of the members are online. A collective
|
||||
of two members is supported, but without redundancy.
|
||||
|
||||
Also note that the collective leader role is dynamic, but has no impact on interacting
|
||||
with confluent. It is merely an internal role that can dynamically change depending
|
||||
on circumstances.
|
||||
|
||||
## OPTIONS
|
||||
|
||||
* `-i`:
|
||||
Provide the token as an argument rather than interactively.
|
||||
|
||||
## EXAMPLES
|
||||
* Inviting a server called mgt2:
|
||||
`# collective invite mgt2`
|
||||
`bWd0MkA+BNQ6XAxMXlqJJa+EQRlihL/k9xCXnasgSQXZr989Pa1/ln7G3e1Ncxx6BMzMqqreHJVkPr2FrzjNit/UgHlg`
|
||||
|
||||
* On mgt2, joining mgt1:
|
||||
`# collective join mgt1 -i bWd0MkA+BNQ6XAxMXlqJJa+EQRlihL/k9xCXnasgSQXZr989Pa1/ln7G3e1Ncxx6BMzMqqreHJVkPr2FrzjNit/UgHlg`
|
||||
`Success`
|
||||
|
||||
* Showing the collective state:
|
||||
`# collective show`
|
||||
`Quorum: True`
|
||||
`Leader: mgt1`
|
||||
`Active collective members:`
|
||||
` mgt2`
|
||||
|
||||
@@ -7,6 +7,7 @@ nodeattrib(8) -- List or change confluent nodes attributes
|
||||
`nodeattrib <noderange> [<nodeattribute1=value1> <nodeattribute2=value2> ...]`
|
||||
`nodeattrib -c <noderange> <nodeattribute1> <nodeattribute2> ...`
|
||||
`nodeattrib -e <noderange> <nodeattribute1> <nodeattribute2> ...`
|
||||
`nodeattrib -p <noderange> <nodeattribute1> <nodeattribute2> ...`
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
@@ -22,6 +23,9 @@ 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.
|
||||
|
||||
Attributes may be specified by wildcard, for example `net.*switch` will report
|
||||
all attributes that begin with `net.` and end with `switch`.
|
||||
|
||||
If the word all is specified, then all available attributes are given.
|
||||
Omitting any attribute name or the word 'all' will display only attributes
|
||||
that are currently set.
|
||||
@@ -40,6 +44,8 @@ See nodegroupattrib(8) command on how to manage attributes on a group level.
|
||||
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:
|
||||
@@ -85,6 +91,12 @@ See nodegroupattrib(8) command on how to manage attributes on a group level.
|
||||
`n1: console.method: `
|
||||
`n2: console.method: `
|
||||
|
||||
* List all switches that a node is described as connected to:
|
||||
`# nodeattrib d1 net.*switch`
|
||||
`d1: net.mgt.switch: mgtswitch1`
|
||||
`d1: net.pxe.switch: pxeswitch1`
|
||||
`d1: net.switch:`
|
||||
|
||||
## SEE ALSO
|
||||
|
||||
nodegroupattrib(8), nodeattribexpressions(5)
|
||||
|
||||
@@ -9,6 +9,18 @@ expression to generate the value.
|
||||
An expression will contain some directives wrapped in `{}` characters. Within
|
||||
`{}` are a number of potential substitute values and operations.
|
||||
|
||||
Note that syntax of expressions can have overlap with the shell syntax.
|
||||
For example:
|
||||
|
||||
`$ echo (n2)`
|
||||
`-bash: syntax error near unexpected token `n2'`
|
||||
|
||||
In such a case, it helps to quote the expression to allow it to be passed:
|
||||
|
||||
`$ echo '(n2)'`
|
||||
`(n2)`
|
||||
|
||||
|
||||
The most common operation is to extract a number from the nodename. These
|
||||
values are available as n1, n2, etc. So for example attributes for a node named
|
||||
b1o2r3u4 would have {n1} as 1, {n2} as 2, {n3} as 3, and {n4} as 4.
|
||||
|
||||
@@ -4,12 +4,44 @@ nodeboot(8) -- Reboot a confluent node to a specific device
|
||||
## SYNOPSIS
|
||||
|
||||
`nodeboot <noderange>`
|
||||
`nodeboot <noderange>` [net|setup]
|
||||
`nodeboot [options] <noderange>` [default|cd|network|setup|hd]
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
**nodeboot** reboots nodes in a noderange. If an additional argument is given,
|
||||
it sets the node to specifically boot to that as the next boot.
|
||||
it sets the node to specifically boot to that as the next boot. This
|
||||
performs an immediate reboot without waiting for the OS. To set the boot
|
||||
device without inducing a reboot, see the `nodesetboot` command.
|
||||
|
||||
## OPTIONS
|
||||
|
||||
* `-b`, `--bios`:
|
||||
For a system that supports both BIOS and UEFI style boot, request BIOS style
|
||||
boot if supported (some platforms will UEFI boot with this flag anyway).
|
||||
|
||||
* `-u`, `--uefi`:
|
||||
This flag does nothing, it is for command compatibility with xCAT's rsetboot
|
||||
|
||||
* `-p`, `--persist`:
|
||||
For a system that supports it, mark the boot override to persist rather than
|
||||
be a one time change. Many systems do not support this functionality.
|
||||
|
||||
* `default`:
|
||||
Request a normal default boot with no particular device override
|
||||
|
||||
* `cd`:
|
||||
Request boot from media. Note that this can include physical CD,
|
||||
remote media mounted as CD/DVD, and detachable hard disks drives such as usb
|
||||
key devices.
|
||||
|
||||
* `network`:
|
||||
Request boot to network
|
||||
|
||||
* `setup`:
|
||||
Request to enter the firmware configuration menu (e.g. F1 setup) on next boot.
|
||||
|
||||
* `hd`:
|
||||
Boot straight to hard disk drive
|
||||
|
||||
## EXAMPLES
|
||||
* Booting n3 and n4 to the default boot behavior:
|
||||
|
||||
@@ -3,8 +3,8 @@ nodeconfig(8) -- Show or change node configuration
|
||||
|
||||
## SYNOPSIS
|
||||
|
||||
`nodeconfig <noderange> [options] [<configuration>..]`
|
||||
`nodeconfig <noderange> [options] [<configuration=value>..]`
|
||||
`nodeconfig <noderange> [options] [<configuration>..]`
|
||||
`nodeconfig <noderange> [options] [<configuration=value>..]`
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
@@ -30,6 +30,11 @@ given as a node expression, as documented in the man page for nodeattribexpressi
|
||||
Provide detailed data as available. This can include help text and valid
|
||||
values for a setting.
|
||||
|
||||
* `-r`, `--restoredefault`:
|
||||
Request that the specified component of the targeted nodes will have its
|
||||
configuration reset to default. Currently the only component implemented
|
||||
is uefi.
|
||||
|
||||
* `-b`, '--batch':
|
||||
Provide arguments as lines of a file, rather than the command line.
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ nodeconsole(8) -- Open a console to a confluent node
|
||||
=====================================================
|
||||
|
||||
## SYNOPSIS
|
||||
`nodeconsole <node>`
|
||||
`nodeconsole [options] <noderange>`
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
@@ -16,6 +16,12 @@ will initiate an automatic retry interval that is randomized between 2 and 4 min
|
||||
The reopen escape sequence below requests an immediate retry, as does connecting
|
||||
a new session.
|
||||
|
||||
## OPTIONS
|
||||
|
||||
* `-t`, `--tile`:
|
||||
Use tmux to arrange consoles of the given noderange into a tiled layout on
|
||||
the terminal screen
|
||||
|
||||
## ESCAPE SEQUENCE COMMANDS
|
||||
|
||||
While connected to a console, a number of commands may be performed through escape
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
nodedefine(8) -- Define new confluent nodes
|
||||
===================================================================
|
||||
|
||||
## SYNOPSIS
|
||||
|
||||
|
||||
@@ -75,6 +75,20 @@ the nodes.
|
||||
* `-e MAC`, `--ethaddr=MAC`:
|
||||
Operate against the system with the specified MAC
|
||||
address
|
||||
* `-f FIELDS`, `--fields=FIELDS`:
|
||||
Request a custom set of fields. The available fields are:
|
||||
Node: The node name if a correlation has been identified
|
||||
Model: The model number
|
||||
Serial: The serial number
|
||||
UUID: The UUID as it should appear in DMI
|
||||
Type: Device type (e.g. lenovo-xcc, pxe-client, etc)
|
||||
IP: The confirmed working IP addresses associated with the record
|
||||
Mac: Mac address of the relevant network interface
|
||||
Switch: The nearest detected switch to the entry
|
||||
Port: Port of the switch that most closely connects to the network interface
|
||||
Advertised IP: IP addresses that may not have been confirmed, but are advertised
|
||||
* `-o ORDER`, `--order=ORDER`:
|
||||
Order output by given field. Field names are the same as documented in the -f argument.
|
||||
* `-t TYPE`, `--type=TYPE`:
|
||||
Operate against the system of the specified type
|
||||
* `-c`, `--csv`:
|
||||
|
||||
@@ -4,7 +4,7 @@ nodefirmware(8) -- Report firmware information on confluent nodes
|
||||
## SYNOPSIS
|
||||
|
||||
`nodefirmware <noderange>`
|
||||
`nodefirmware <noderange> <components>|core`
|
||||
`nodefirmware <noderange> list|<components>|core`
|
||||
`nodefirmware <noderange> update [--backup] <filename>`
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
@@ -7,6 +7,7 @@ nodegroupattrib(8) -- List or change confluent nodegroup attributes
|
||||
`nodegroupattrib <group> [<nodeattribute>...]`
|
||||
`nodegroupattrib <group> [<nodeattribute1=value1> <nodeattribute2=value2> ...]`
|
||||
`nodegroupattrib <group> [-c] [<nodeattribute1> <nodeattribute2=value2> ...]`
|
||||
`nodeattrib -p <noderange> <nodeattribute1> <nodeattribute2> ...`
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
nodegroupdefine(8) -- Define new confluent node group
|
||||
===================================================================
|
||||
|
||||
## SYNOPSIS
|
||||
|
||||
@@ -6,7 +7,7 @@ nodegroupdefine(8) -- Define new confluent node group
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
`nodegroupdefine` allows the definition of a new node for the confluent management
|
||||
`nodegroupdefine` allows the definition of a new nodegroup for the confluent management
|
||||
service. It may only define a single group name at a time.
|
||||
It has the same syntax as `nodegroupattrib(8)`, and the commands differ in
|
||||
that `nodegroupattrib(8)` will error if a node group does not exist.
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
nodegrouplist(8) -- List the defined confluent nodegroups
|
||||
===================================================================
|
||||
|
||||
## SYNOPSIS
|
||||
|
||||
`nodegrouplist`
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
`nodegrouplist` lists the currently defined groups in confluent.
|
||||
|
||||
## SEE ALSO
|
||||
|
||||
nodeattrib(8), nodeattribexpressions(5), nodegroupattrib(8)
|
||||
@@ -3,7 +3,7 @@ nodeidentify(8) -- Control the identify LED of confluent nodes
|
||||
|
||||
## SYNOPSIS
|
||||
|
||||
`nodidentify <noderange> [on|off]`
|
||||
`nodidentify <noderange> [on|off|blink]`
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
@@ -13,6 +13,7 @@ options are supported:
|
||||
|
||||
* `on`: Turn on the identify LED
|
||||
* `off`: Turn off the identify LED
|
||||
* `blink`: Set the identify LED to blink (when supported by the system)
|
||||
|
||||
## EXAMPLES:
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
nodelicense(8) -- Manage license keys on BMC
|
||||
=================================================================
|
||||
|
||||
## SYNOPSIS
|
||||
|
||||
`nodelicense <noderange> [list|install <filename>|delete <license>|save <directory>]`
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
`nodelicense` manages license keys on supported BMCs. Without an argument, the command
|
||||
lists currently installed license. Using `delete` will remove the specified license name
|
||||
from th eBMC. The `save` subcommand will take the passed directory (which may be in the form
|
||||
of /path/to/{node}/ to have the node name substituted for each node) and back up installed licenses
|
||||
to that directory. The `install` command will take the specified filename and install. The filename
|
||||
argument may be of the form xcc_fod_0034_7X21{id.serial}.key to have the serial number substituted
|
||||
to allow unique licenses to be specified in a single command.
|
||||
|
||||
@@ -17,6 +17,9 @@ displayed. If `-b` is specified, it will also display information on
|
||||
how inherited and expression based attributes are defined. There is more
|
||||
information on node attributes in nodeattributes(5) man page.
|
||||
|
||||
Attributes may be specified by wildcard, for example `net.*switch` will report
|
||||
all attributes that begin with `net.` and end with `switch`.
|
||||
|
||||
## OPTIONS
|
||||
|
||||
* `-b`, `--blame`:
|
||||
|
||||
@@ -36,6 +36,9 @@ Also, regular expressions may be used to indicate nodes with names matching cert
|
||||
The other major noderange primitive is indicating nodes by some attribute value:
|
||||
`location.rack=7`
|
||||
|
||||
The attribute name may use a wildcard:
|
||||
`net.*switch=switch1`
|
||||
|
||||
Commas can be used to indicate multiple nodes, and can mix and match any of the above primitives. The following can be a valid single noderange, combining any and all members of each comma separated component
|
||||
`n1,n2,rack1,storage,location.rack=9,~s1..,n20-n30`
|
||||
|
||||
|
||||
@@ -30,7 +30,10 @@ control.
|
||||
* `-p`, `--persist`:
|
||||
For a system that supports it, mark the boot override to persist rather than
|
||||
be a one time change. Many systems do not support this functionality.
|
||||
|
||||
|
||||
* `-u`, `--uefi`:
|
||||
This flag does nothing, it is for command compatibility with xCAT's rsetboot
|
||||
|
||||
* `default`:
|
||||
Request a normal default boot with no particular device override
|
||||
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
nodestorage(8) -- Examine/Modify storage configuration of a node
|
||||
============================================
|
||||
|
||||
## SYNOPSIS
|
||||
|
||||
`nodestorage <noderange> [show|create|delete] [options]`
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
`nodestorage` provides access to the remote storage configuration of
|
||||
the noderange.
|
||||
|
||||
## OPTIONS
|
||||
* `-r` **RAIDLEVEL**, `--raidlevel`=**RAIDLEVEL**:
|
||||
RAID level to use when creating an array
|
||||
|
||||
* `-d` **DISKS**, `--disks`=**DISKS**:
|
||||
Comma separated list of disks to use, or the word "rest" to
|
||||
indicate use of all available disks
|
||||
|
||||
* `-s` **SIZE**, `--size`=**SIZE**:
|
||||
Comma separated list of sizes to use when creating
|
||||
volumes. The sizes may be absolute size (e.g. 16gb),
|
||||
percentage (10%) or the word "rest" to use remaining
|
||||
capacity, default behavior is to use all capacity to
|
||||
make a volume
|
||||
* `-n` **NAME**, `--name`=**NAME**:
|
||||
Comma separated list of names to use when naming
|
||||
volumes, or selecting a volume for delete. Default
|
||||
behavior is to use implementation provided default
|
||||
|
||||
## EXAMPLES
|
||||
|
||||
* Deleting the volume `somedata`:
|
||||
`$ nodestorage d5 delete somedata`
|
||||
`Deleted: somedata`
|
||||
|
||||
* Creating a raid5 of 4 disks and a volume named `somedata`:
|
||||
`$ nodestorage d5 create -r 5 -d drive0,drive_1,drive_2,drive_3 -n somedata`
|
||||
`d5: Volume somedata: Size: 1.905 TB`
|
||||
`d5: Volume somedata: State: Optimal`
|
||||
`d5: Volume somedata: Array 1-2`
|
||||
|
||||
* Showing current storage configuration of `d3`:
|
||||
`$ nodestorage d3`
|
||||
`d3: Disk m.2-0 Description: 128GB M.2 SATA SSD`
|
||||
`d3: Disk m.2-0 State: online`
|
||||
`d3: Disk m.2-0 FRU: 00LF428`
|
||||
`d3: Disk m.2-0 Serial Number: H6B80054`
|
||||
`d3: Disk m.2-0 Array: 0-0`
|
||||
`d3: Disk m.2-1 Description: 128GB M.2 SATA SSD`
|
||||
`d3: Disk m.2-1 State: online`
|
||||
`d3: Disk m.2-1 FRU: 00LF428`
|
||||
`d3: Disk m.2-1 Serial Number: H6B80059`
|
||||
`d3: Disk m.2-1 Array: 0-0`
|
||||
`d3: Array 0-0 Available Capacity: 0.000 MB`
|
||||
`d3: Array 0-0 Total Capacity: 131.072 GB`
|
||||
`d3: Array 0-0 RAID: RAID 1`
|
||||
`d3: Array 0-0 Disks: m.2-0,m.2-1`
|
||||
`d3: Array 0-0 Volumes: new_vd`
|
||||
`d3: Volume new_vd: Size: 122.040 GB`
|
||||
`d3: Volume new_vd: State: Optimal`
|
||||
`d3: Volume new_vd: Array 0-0`
|
||||
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
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).
|
||||
Note that the file will be downloaded to the confluent server that actually
|
||||
connects to the managed system, so it will download to the remote system if running
|
||||
remotely and will download to the collective.manager indicated system if
|
||||
running in collective mode.
|
||||
|
||||
## 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`
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python
|
||||
#!/usr/bin/python2
|
||||
|
||||
# This is a sample python script for going through all observed mac addresses
|
||||
# and assuming they are BMC related and printing nodeattrib commands
|
||||
|
||||
@@ -22,10 +22,10 @@ This provides the modules common for both client and server
|
||||
%setup -n %{name}-%{version} -n %{name}-%{version}
|
||||
|
||||
%build
|
||||
python setup.py build
|
||||
python2 setup.py build
|
||||
|
||||
%install
|
||||
python setup.py install --single-version-externally-managed -O1 --root=$RPM_BUILD_ROOT --record=INSTALLED_FILES --install-purelib=/opt/confluent/lib/python --install-scripts=/opt/confluent/bin
|
||||
python2 setup.py install --single-version-externally-managed -O1 --root=$RPM_BUILD_ROOT --record=INSTALLED_FILES --install-purelib=/opt/confluent/lib/python --install-scripts=/opt/confluent/bin
|
||||
|
||||
%clean
|
||||
rm -rf $RPM_BUILD_ROOT
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
[metadata]
|
||||
name = confluent_common
|
||||
summary = Confluent Common Libraries
|
||||
description-file =
|
||||
README.txt
|
||||
author = Jarrod Johnson
|
||||
author-email = jjohnson2@lenovo.com
|
||||
home-page = http://xcat.sf.net/
|
||||
classifier =
|
||||
Intended Audience :: Information Technology
|
||||
Intended Audience :: System Administrators
|
||||
License :: OSI Approved :: Apache Software License
|
||||
Operating System :: POSIX :: Linux
|
||||
Programming Language :: Python :: 2.6
|
||||
Programming Language :: Python :: 2.7
|
||||
|
||||
[files]
|
||||
@@ -1,3 +1,4 @@
|
||||
include pam/*
|
||||
include sysvinit/*
|
||||
include systemd/*
|
||||
include sysctl/*
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python
|
||||
#!/usr/bin/python2
|
||||
|
||||
import argparse
|
||||
import errno
|
||||
@@ -66,8 +66,9 @@ def join_collective(server, invitation):
|
||||
'invitation': invitation,
|
||||
'server': server}})
|
||||
res = tlvdata.recv(s)
|
||||
print(res.get('collective',
|
||||
{'status': 'Unknown response: ' + repr(res)})['status'])
|
||||
res = res.get('collective',
|
||||
{'status': 'Unknown response: ' + repr(res)})
|
||||
print(res.get('status', res.get('error', repr(res))))
|
||||
|
||||
|
||||
def show_collective():
|
||||
@@ -100,11 +101,11 @@ def main():
|
||||
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')
|
||||
'collective member. Run collective invite -h for more information')
|
||||
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 = sp.add_parser('join', help='Join a collective. Run collective join -h for more information')
|
||||
jc.add_argument('server', help='Existing collective member that ran invite and generated a token')
|
||||
jc.add_argument('-i', help='Invitation provided by runniing invite on an '
|
||||
'existing collective member')
|
||||
cmdset = a.parse_args()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python
|
||||
#!/usr/bin/python2
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2014 IBM Corporation
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
from os.path import exists
|
||||
import shutil
|
||||
import socket
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
def get_openssl_conf_location():
|
||||
if exists('/etc/pki/tls/openssl.cnf'):
|
||||
return '/etc/pki/tls/openssl.cnf'
|
||||
elif exists('/etc/ssl/openssl.cnf');
|
||||
return '/etc/ssl/openssl.cnf'
|
||||
else:
|
||||
raise Exception("Cannot find openssl config file")
|
||||
|
||||
def get_ip_addresses():
|
||||
lines = subprocess.check_output('ip addr'.split(' '))
|
||||
for line in lines.split('\n'):
|
||||
if line.startswith(' inet6 '):
|
||||
line = line.replace(' inet6 ', '').split('/')[0]
|
||||
if line.startswith('fe80::'):
|
||||
continue
|
||||
if line == '::1':
|
||||
continue
|
||||
elif line.startswith(' inet '):
|
||||
line = line.replace(' inet ', '').split('/')[0]
|
||||
if line == '127.0.0.1':
|
||||
continue
|
||||
if line.startswith('169.254.'):
|
||||
continue
|
||||
else:
|
||||
continue
|
||||
yield line
|
||||
|
||||
def create_certificate():
|
||||
shortname = socket.gethostname().split('.')[0]
|
||||
longname = socket.getfqdn()
|
||||
subprocess.check_call(
|
||||
'openssl ecparam -name secp384r1 -genkey -out privkey.pem'.split(' '))
|
||||
san = ['IP:{0}'.format(x) for x in get_ip_addresses()]
|
||||
san.append('DNS:{0}'.format(shortname))
|
||||
san.append('DNS:{0}'.format(longname))
|
||||
san = ','.join(san)
|
||||
sslcfg = get_openssl_conf_location()
|
||||
tmpconfig = tempfile.mktemp()
|
||||
shutil.copy2(sslcfg, tmpconfig)
|
||||
with open(tmpconfig, 'a') as cfgfile:
|
||||
cfgfile.write('\n[SAN]\nsubjectAltName={0}'.format(san))
|
||||
subprocess.check_call(
|
||||
'openssl req -new -x509 -key privkey.pem -days 7300 -out cert.pem '
|
||||
'-subj /CN={0} -extensions SAN '
|
||||
'-config {1}'.format(longname, tmpconfig).split(' ')
|
||||
)
|
||||
|
||||
if __name__ == '__main__':
|
||||
create_certificate()
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python
|
||||
#!/usr/bin/python2
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2017 Lenovo
|
||||
@@ -82,7 +82,7 @@ elif args[0] == 'dump':
|
||||
"or -s to do encrypted backup that requires keys.json from "
|
||||
"another backup to restore.")
|
||||
sys.exit(1)
|
||||
os.umask(077)
|
||||
os.umask(0o77)
|
||||
main._initsecurity(conf.get_config())
|
||||
if not os.path.exists(dumpdir):
|
||||
os.makedirs(dumpdir)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python
|
||||
#!/usr/bin/python2
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2014 IBM Corporation
|
||||
|
||||
Executable
+64
@@ -0,0 +1,64 @@
|
||||
#!/bin/bash
|
||||
cd `dirname $0`
|
||||
PKGNAME=$(basename $(pwd))
|
||||
DPKGNAME=$(basename $(pwd) | sed -e s/_/-/)
|
||||
OPKGNAME=$(basename $(pwd) | sed -e s/_/-/)
|
||||
if grep wheezy /etc/os-release; then
|
||||
DPKGNAME=python-$DPKGNAME
|
||||
fi
|
||||
cd ..
|
||||
mkdir -p /tmp/confluent # $DPKGNAME
|
||||
cp -a * .git /tmp/confluent # $DPKGNAME
|
||||
cd /tmp/confluent/$PKGNAME
|
||||
if [ -x ./makeman ]; then
|
||||
./makeman
|
||||
fi
|
||||
./makesetup
|
||||
VERSION=`cat VERSION`
|
||||
cat > setup.cfg << EOF
|
||||
[install]
|
||||
install-purelib=/opt/confluent/lib/python
|
||||
install-scripts=/opt/confluent/bin
|
||||
|
||||
[sdist_dsc]
|
||||
package=$DPKGNAME
|
||||
EOF
|
||||
|
||||
python setup.py sdist > /dev/null 2>&1
|
||||
py2dsc dist/*.tar.gz
|
||||
shopt -s extglob
|
||||
cd deb_dist/!(*.orig)/
|
||||
if [ "$OPKGNAME" = "confluent-server" ]; then
|
||||
if grep wheezy /etc/os-release; then
|
||||
sed -i 's/^\(Depends:.*\)/\1, python-confluent-client, python-lxml, python-eficompressor, python-pycryptodomex, python-dateutil, python-pyopenssl/' debian/control
|
||||
else
|
||||
sed -i 's/^\(Depends:.*\)/\1, confluent-client, python-lxml, python-eficompressor, python-pycryptodome, python-dateutil/' debian/control
|
||||
fi
|
||||
if grep wheezy /etc/os-release; then
|
||||
echo 'confluent_client python-confluent-client' >> debian/pydist-overrides
|
||||
else
|
||||
echo 'confluent_client confluent-client' >> debian/pydist-overrides
|
||||
fi
|
||||
fi
|
||||
head -n -1 debian/control > debian/control1
|
||||
mv debian/control1 debian/control
|
||||
echo 'export PYBUILD_INSTALL_ARGS=--install-lib=/opt/confluent/lib/python' >> debian/rules
|
||||
#echo 'Provides: python-'$DPKGNAME >> debian/control
|
||||
#echo 'Conflicts: python-'$DPKGNAME >> debian/control
|
||||
#echo 'Replaces: python-'$DPKGNAME' (<<2)' >> debian/control
|
||||
#echo 'Breaks: python-'$DPKGNAME' (<<2)' >> debian/control
|
||||
|
||||
dpkg-buildpackage -rfakeroot -uc -us -i
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "[ERROR] rpmbuild returned non-zero, run: rpmbuild -ba ~/rpmbuild/SPECS/$PKGNAME.spec"
|
||||
exit 1
|
||||
else
|
||||
cd -
|
||||
# Clean up the generated files in this directory
|
||||
rm -rf $PKGNAME.egg-info dist setup.py
|
||||
rm -rf $(find deb_dist -mindepth 1 -maxdepth 1 -type d)
|
||||
if [ ! -z "$1" ]; then
|
||||
mv deb_dist/* $1/
|
||||
fi
|
||||
fi
|
||||
exit 0
|
||||
@@ -1,6 +1,6 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2016 Lenovo
|
||||
# Copyright 2016-2018 Lenovo
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -83,6 +83,8 @@ class AsyncSession(object):
|
||||
self.reaper = eventlet.spawn_after(15, self.destroy)
|
||||
|
||||
def add(self, requestid, rsp):
|
||||
if self.responses is None:
|
||||
return
|
||||
self.responses.append((requestid, rsp))
|
||||
if self._evt:
|
||||
self._evt.send()
|
||||
@@ -107,6 +109,7 @@ class AsyncSession(object):
|
||||
for console in self.consoles:
|
||||
_consolesessions[console]['session'].destroy()
|
||||
self.consoles = None
|
||||
self.responses = None
|
||||
del _asyncsessions[self.asyncid]
|
||||
|
||||
def run_handler(self, handler, requestid):
|
||||
@@ -166,6 +169,9 @@ def handle_async(env, querydict, threadset):
|
||||
currsess = AsyncSession()
|
||||
yield messages.AsyncSession(currsess.asyncid)
|
||||
return
|
||||
if querydict['asyncid'] not in _asyncsessions:
|
||||
raise exc.InvalidArgumentException(
|
||||
'Invalid or expired async id')
|
||||
mythreadid = greenlet.getcurrent()
|
||||
threadset.add(mythreadid)
|
||||
loggedout = None
|
||||
|
||||
@@ -23,11 +23,14 @@ import confluent.config.configmanager as configmanager
|
||||
import eventlet
|
||||
import eventlet.tpool
|
||||
import Cryptodome.Protocol.KDF as KDF
|
||||
from fnmatch import fnmatch
|
||||
import hashlib
|
||||
import hmac
|
||||
import multiprocessing
|
||||
import confluent.userutil as userutil
|
||||
pam = None
|
||||
try:
|
||||
import PAM
|
||||
import confluent.pam as pam
|
||||
except ImportError:
|
||||
pass
|
||||
import time
|
||||
@@ -39,24 +42,59 @@ _passchecking = {}
|
||||
authworkers = None
|
||||
authcleaner = None
|
||||
|
||||
_allowedbyrole = {
|
||||
'Operator': {
|
||||
'retrieve': ['*'],
|
||||
'create': [
|
||||
'/noderange/',
|
||||
'/nodes/',
|
||||
'/node*/media/uploads/',
|
||||
'/node*/inventory/firmware/updates/*',
|
||||
'/node*/suppport/servicedata*',
|
||||
'/node*/attributes/expression',
|
||||
'/nodes/*/console/session*',
|
||||
'/nodes/*/shell/sessions*',
|
||||
'/node*/configuration/*',
|
||||
],
|
||||
'update': [
|
||||
'/discovery/*',
|
||||
'/networking/macs/rescan',
|
||||
'/node*/power/state',
|
||||
'/node*/power/reseat',
|
||||
'/node*/attributes/*',
|
||||
'/node*/media/*tach',
|
||||
'/node*/boot/nextdevice',
|
||||
'/node*/identify',
|
||||
'/node*/configuration/*',
|
||||
],
|
||||
'start': [
|
||||
'/nodes/*/console/session*',
|
||||
'/nodes/*/shell/sessions*',
|
||||
],
|
||||
'delete': [
|
||||
'/discovery/*',
|
||||
'/node*',
|
||||
],
|
||||
},
|
||||
'Monitor': {
|
||||
'retrieve': [
|
||||
'/node*/health/hardware',
|
||||
'/node*/power/state',
|
||||
'/node*/sensors/*',
|
||||
'/nodes/',
|
||||
'/',
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
class Credentials(object):
|
||||
def __init__(self, username, passphrase):
|
||||
self.username = username
|
||||
self.passphrase = passphrase
|
||||
self.haspam = False
|
||||
|
||||
def pam_conv(self, auth, query_list):
|
||||
# use stored credentials in a pam conversation
|
||||
self.haspam = True
|
||||
resp = []
|
||||
for query_entry in query_list:
|
||||
query, pamtype = query_entry
|
||||
if query.startswith('Password'):
|
||||
resp.append((self.passphrase, 0))
|
||||
else:
|
||||
return None
|
||||
return resp
|
||||
_deniedbyrole = {
|
||||
# This supersedes the above and is only consulted after the allowed has happened
|
||||
'Operator': {
|
||||
'update': [
|
||||
'/node*/configuration/management_controller/users/*',
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def _prune_passcache():
|
||||
@@ -65,9 +103,9 @@ def _prune_passcache():
|
||||
while True:
|
||||
curtime = time.time()
|
||||
for passent in _passcache.iterkeys():
|
||||
if passent[2] < curtime - 10:
|
||||
if passent[2] < curtime - 90:
|
||||
del _passcache[passent]
|
||||
eventlet.sleep(10)
|
||||
eventlet.sleep(90)
|
||||
|
||||
|
||||
def _get_usertenant(name, tenant=False):
|
||||
@@ -112,21 +150,37 @@ def authorize(name, element, tenant=False, operation='create',
|
||||
and the relevant ConfigManager object for the context of the
|
||||
request.
|
||||
"""
|
||||
if operation not in ('create', 'start', 'update', 'retrieve', 'delete'):
|
||||
return None
|
||||
# skipuserobj is a leftover from the now abandoned plan to use pam session
|
||||
# to do authorization and authentication. Now confluent always does authorization
|
||||
# even if pam does authentication.
|
||||
if operation not in ('create', 'start', 'update', 'retrieve', 'delete', None):
|
||||
return False
|
||||
user, tenant = _get_usertenant(name, tenant)
|
||||
if tenant is not None and not configmanager.is_tenant(tenant):
|
||||
return None
|
||||
return False
|
||||
manager = configmanager.ConfigManager(tenant, username=user)
|
||||
if skipuserobj:
|
||||
return None, manager, user, tenant, skipuserobj
|
||||
userobj = manager.get_user(user)
|
||||
if not userobj:
|
||||
for group in userutil.grouplist(user):
|
||||
userobj = manager.get_usergroup(group)
|
||||
if userobj:
|
||||
break
|
||||
if userobj: # returning
|
||||
role = userobj.get('role', 'Administrator')
|
||||
if element and role != 'Administrator':
|
||||
for rule in _allowedbyrole.get(role, {}).get(operation, []):
|
||||
if fnmatch(element, rule):
|
||||
break
|
||||
else:
|
||||
return False
|
||||
for rule in _deniedbyrole.get(role, {}).get(operation, []):
|
||||
if fnmatch(element, rule):
|
||||
return False
|
||||
return userobj, manager, user, tenant, skipuserobj
|
||||
return None
|
||||
return False
|
||||
|
||||
|
||||
def check_user_passphrase(name, passphrase, element=None, tenant=False):
|
||||
def check_user_passphrase(name, passphrase, operation=None, element=None, tenant=False):
|
||||
"""Check a a login name and passphrase for authenticity and authorization
|
||||
|
||||
The function combines authentication and authorization into one function.
|
||||
@@ -157,63 +211,66 @@ def check_user_passphrase(name, passphrase, element=None, tenant=False):
|
||||
# would normally make an event and wait
|
||||
# but here there's no need for that
|
||||
eventlet.sleep(0.5)
|
||||
credobj = Credentials(user, passphrase)
|
||||
try:
|
||||
pammy = PAM.pam()
|
||||
pammy.start(_pamservice, user, credobj.pam_conv)
|
||||
pammy.authenticate()
|
||||
pammy.acct_mgmt()
|
||||
del pammy
|
||||
return authorize(user, element, tenant, skipuserobj=False)
|
||||
except NameError:
|
||||
pass
|
||||
except PAM.error:
|
||||
if credobj.haspam:
|
||||
return None
|
||||
cfm = configmanager.ConfigManager(tenant, username=user)
|
||||
ucfg = cfm.get_user(user)
|
||||
if ucfg is None:
|
||||
try:
|
||||
for group in userutil.grouplist(user):
|
||||
ucfg = cfm.get_usergroup(group)
|
||||
if ucfg:
|
||||
break
|
||||
except KeyError:
|
||||
pass
|
||||
if ucfg is None:
|
||||
eventlet.sleep(0.05)
|
||||
return None
|
||||
if (user, tenant) in _passcache:
|
||||
if passphrase == _passcache[(user, tenant)]:
|
||||
return authorize(user, element, tenant)
|
||||
if hashlib.sha256(passphrase).digest() == _passcache[(user, tenant)]:
|
||||
return authorize(user, element, tenant, operation=operation)
|
||||
else:
|
||||
# In case of someone trying to guess,
|
||||
# while someone is legitimately logged in
|
||||
# invalidate cache and force the slower check
|
||||
del _passcache[(user, tenant)]
|
||||
return None
|
||||
cfm = configmanager.ConfigManager(tenant, username=user)
|
||||
ucfg = cfm.get_user(user)
|
||||
if ucfg is None or 'cryptpass' not in ucfg:
|
||||
eventlet.sleep(0.05) # stall even on test for existence of a username
|
||||
return None
|
||||
_passchecking[(user, tenant)] = True
|
||||
# TODO(jbjohnso): WORKERPOOL
|
||||
# PBKDF2 is, by design, cpu intensive
|
||||
# throw it at the worker pool when implemented
|
||||
# maybe a distinct worker pool, wondering about starving out non-auth stuff
|
||||
salt, crypt = ucfg['cryptpass']
|
||||
# execute inside tpool to get greenthreads to give it a special thread
|
||||
# world
|
||||
#TODO(jbjohnso): util function to generically offload a call
|
||||
#such a beast could be passed into pyghmi as a way for pyghmi to
|
||||
#magically get offload of the crypto functions without having
|
||||
#to explicitly get into the eventlet tpool game
|
||||
global authworkers
|
||||
global authcleaner
|
||||
if authworkers is None:
|
||||
authworkers = multiprocessing.Pool(processes=1)
|
||||
else:
|
||||
authcleaner.cancel()
|
||||
authcleaner = eventlet.spawn_after(30, _clean_authworkers)
|
||||
crypted = eventlet.tpool.execute(_do_pbkdf, passphrase, salt)
|
||||
del _passchecking[(user, tenant)]
|
||||
eventlet.sleep(0.05) # either way, we want to stall so that client can't
|
||||
if 'cryptpass' in ucfg:
|
||||
_passchecking[(user, tenant)] = True
|
||||
# TODO(jbjohnso): WORKERPOOL
|
||||
# PBKDF2 is, by design, cpu intensive
|
||||
# throw it at the worker pool when implemented
|
||||
# maybe a distinct worker pool, wondering about starving out non-auth stuff
|
||||
salt, crypt = ucfg['cryptpass']
|
||||
# execute inside tpool to get greenthreads to give it a special thread
|
||||
# world
|
||||
# TODO(jbjohnso): util function to generically offload a call
|
||||
# such a beast could be passed into pyghmi as a way for pyghmi to
|
||||
# magically get offload of the crypto functions without having
|
||||
# to explicitly get into the eventlet tpool game
|
||||
global authworkers
|
||||
global authcleaner
|
||||
if authworkers is None:
|
||||
authworkers = multiprocessing.Pool(processes=1)
|
||||
else:
|
||||
authcleaner.cancel()
|
||||
authcleaner = eventlet.spawn_after(30, _clean_authworkers)
|
||||
crypted = eventlet.tpool.execute(_do_pbkdf, passphrase, salt)
|
||||
del _passchecking[(user, tenant)]
|
||||
eventlet.sleep(
|
||||
0.05) # either way, we want to stall so that client can't
|
||||
# determine failure because there is a delay, valid response will
|
||||
# delay as well
|
||||
if crypt == crypted:
|
||||
_passcache[(user, tenant)] = passphrase
|
||||
return authorize(user, element, tenant)
|
||||
if crypt == crypted:
|
||||
_passcache[(user, tenant)] = hashlib.sha256(passphrase).digest()
|
||||
return authorize(user, element, tenant, operation)
|
||||
if pam:
|
||||
pammy = pam.pam()
|
||||
usergood = pammy.authenticate(user, passphrase)
|
||||
del pammy
|
||||
if usergood:
|
||||
_passcache[(user, tenant)] = hashlib.sha256(passphrase).digest()
|
||||
return authorize(user, element, tenant, operation, skipuserobj=False)
|
||||
eventlet.sleep(0.05) # stall even on test for existence of a username
|
||||
return None
|
||||
|
||||
|
||||
def _apply_pbkdf(passphrase, salt):
|
||||
return KDF.PBKDF2(passphrase, salt, 32, 10000,
|
||||
lambda p, s: hmac.new(p, s, hashlib.sha256).digest())
|
||||
|
||||
@@ -18,12 +18,14 @@ 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
|
||||
@@ -33,38 +35,41 @@ except ImportError:
|
||||
crypto = None
|
||||
|
||||
currentleader = None
|
||||
cfginitlock = 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 cfginitlock
|
||||
global follower
|
||||
if cfginitlock is None:
|
||||
cfginitlock = threading.RLock()
|
||||
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:
|
||||
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 cfginitlock:
|
||||
with cfm._initlock:
|
||||
tlvdata.recv(remote) # the banner
|
||||
tlvdata.recv(remote) # authpassed... 0..
|
||||
if name is None:
|
||||
@@ -77,10 +82,16 @@ def connect_to_leader(cert=None, name=None, leader=None):
|
||||
return False
|
||||
if 'error' in keydata:
|
||||
if 'backoff' in keydata:
|
||||
eventlet.spawn_after(random.random(), connect_to_leader,
|
||||
cert, name, leader)
|
||||
return True
|
||||
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:
|
||||
@@ -88,13 +99,22 @@ def connect_to_leader(cert=None, name=None, leader=None):
|
||||
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)
|
||||
print(keydata['error'])
|
||||
return False
|
||||
if follower is not None:
|
||||
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)
|
||||
@@ -134,11 +154,22 @@ def connect_to_leader(cert=None, name=None, leader=None):
|
||||
|
||||
def follow_leader(remote):
|
||||
global currentleader
|
||||
cfm.follow_channel(remote)
|
||||
# The leader has folded, time to startup again...
|
||||
cfm.stop_following()
|
||||
currentleader = None
|
||||
eventlet.spawn_n(start_collective)
|
||||
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):
|
||||
@@ -291,6 +322,8 @@ def handle_connection(connection, cert, request, local=False):
|
||||
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:
|
||||
@@ -320,6 +353,11 @@ def handle_connection(connection, cert, request, local=False):
|
||||
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, '
|
||||
@@ -329,8 +367,22 @@ def handle_connection(connection, cert, request, local=False):
|
||||
tlvdata.send(connection,
|
||||
{'error': 'Refusing to be assimilated by inferior'
|
||||
'transaction count',
|
||||
'txcount': cfm._txcount})
|
||||
'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})
|
||||
@@ -359,6 +411,11 @@ def handle_connection(connection, cert, request, local=False):
|
||||
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,
|
||||
@@ -366,17 +423,14 @@ def handle_connection(connection, cert, request, local=False):
|
||||
'in another castle', 'leader': currentleader})
|
||||
connection.close()
|
||||
return
|
||||
if connecting.active:
|
||||
tlvdata.send(connection, {'error': 'Connecting right now',
|
||||
'backoff': True})
|
||||
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()
|
||||
@@ -430,11 +484,36 @@ def try_assimilate(drone):
|
||||
tlvdata.recv(remote) # the banner
|
||||
tlvdata.recv(remote) # authpassed... 0..
|
||||
answer = tlvdata.recv(remote)
|
||||
if answer and 'error' in answer:
|
||||
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
|
||||
|
||||
@@ -446,27 +525,34 @@ def retire_as_leader():
|
||||
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 (currentleader, skipaddr) or member == myname:
|
||||
if dronecandidate in skipem or member == myname:
|
||||
continue
|
||||
eventlet.spawn_n(try_assimilate, dronecandidate)
|
||||
|
||||
|
||||
def startup():
|
||||
global cfginitlock
|
||||
members = list(cfm.list_collective())
|
||||
if len(members) < 2:
|
||||
# Not in collective mode, return
|
||||
return
|
||||
if cfginitlock is None:
|
||||
cfginitlock = threading.RLock()
|
||||
eventlet.spawn_n(start_collective)
|
||||
|
||||
def start_collective():
|
||||
@@ -474,7 +560,15 @@ def start_collective():
|
||||
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
|
||||
@@ -485,6 +579,8 @@ def start_collective():
|
||||
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:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2014 IBM Corporation
|
||||
# Copyright 2015 Lenovo
|
||||
# Copyright 2015-2019 Lenovo
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -93,9 +93,10 @@ node = {
|
||||
'description': ('List of static groups for which this node is '
|
||||
'considered a member'),
|
||||
},
|
||||
#'type': {
|
||||
# 'description': ('Classification of node as system, vm, etc')
|
||||
#},
|
||||
'type': {
|
||||
'description': ('Classification of node as server or switch'),
|
||||
'validvalues': ('switch', 'server'),
|
||||
},
|
||||
#'id': {
|
||||
# 'description': ('Numeric identifier for node')
|
||||
#},
|
||||
@@ -156,6 +157,14 @@ node = {
|
||||
'indicates candidate managers, either for '
|
||||
'high availability or load balancing purposes.')
|
||||
},
|
||||
'discovery.passwordrules': {
|
||||
'description': 'Any specified rules shall be configured on the BMC '
|
||||
'upon discovery. "expiration=no,loginfailures=no,complexity=no,reuse=no" '
|
||||
'would disable password expiration, login failures '
|
||||
'triggering a lockout, password complexity requirements,'
|
||||
'and any restrictions around reusing an old password.',
|
||||
'validlistkeys': ('expiration', 'loginfailures', 'complexity', 'reuse'),
|
||||
},
|
||||
'discovery.policy': {
|
||||
'description': 'Policy to use for auto-configuration of discovered '
|
||||
'and identified nodes. Valid values are "manual", '
|
||||
@@ -165,6 +174,7 @@ node = {
|
||||
'so long as the node has no existing public key. '
|
||||
'"open" allows discovery even if a known public key '
|
||||
'is already stored',
|
||||
'validlist': ('manual', 'permissive', 'pxe', 'open'),
|
||||
},
|
||||
'info.note': {
|
||||
'description': 'A field used for administrators to make arbitrary '
|
||||
@@ -173,6 +183,9 @@ node = {
|
||||
'freeform text data without concern for issues in how '
|
||||
'the server will process it.',
|
||||
},
|
||||
'location.height': {
|
||||
'description': 'Height in RU of the system (defaults to query the systems)',
|
||||
},
|
||||
'location.room': {
|
||||
'description': 'Room description for the node',
|
||||
},
|
||||
@@ -252,11 +265,13 @@ node = {
|
||||
'console.logging': {
|
||||
'description': ('Indicate logging level to apply to console. Valid '
|
||||
'values are currently "full", "interactive", and '
|
||||
'"none". Defaults to "full".')
|
||||
'"none". Defaults to "full".'),
|
||||
'validvalues': ('full', 'interactive', 'none'),
|
||||
},
|
||||
'console.method': {
|
||||
'description': ('Indicate the method used to access the console of '
|
||||
'the managed node.')
|
||||
'the managed node.'),
|
||||
'validvalues': ('ssh', 'ipmi'),
|
||||
},
|
||||
# 'virtualization.host': {
|
||||
# 'description': ('Hypervisor where this node does/should reside'),
|
||||
|
||||
@@ -16,7 +16,10 @@
|
||||
|
||||
|
||||
# This defines config variable to store the global configuration for confluent
|
||||
import ConfigParser
|
||||
try:
|
||||
import ConfigParser
|
||||
except ModuleNotFoundError:
|
||||
import configparser as ConfigParser
|
||||
import os
|
||||
|
||||
_config = None
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
7# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2014 IBM Corporation
|
||||
# Copyright 2015-2018 Lenovo
|
||||
# Copyright 2015-2019 Lenovo
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -46,7 +46,10 @@ import Cryptodome.Protocol.KDF as KDF
|
||||
from Cryptodome.Cipher import AES
|
||||
from Cryptodome.Hash import HMAC
|
||||
from Cryptodome.Hash import SHA256
|
||||
import anydbm as dbm
|
||||
try:
|
||||
import anydbm as dbm
|
||||
except ModuleNotFoundError:
|
||||
import dbm
|
||||
import ast
|
||||
import base64
|
||||
import confluent.config.attributes as allattributes
|
||||
@@ -57,7 +60,10 @@ import confluent.util
|
||||
import confluent.netutil as netutil
|
||||
import confluent.exceptions as exc
|
||||
import copy
|
||||
import cPickle
|
||||
try:
|
||||
import cPickle
|
||||
except ModuleNotFoundError:
|
||||
import pickle as cPickle
|
||||
import errno
|
||||
import eventlet
|
||||
import eventlet.event as event
|
||||
@@ -74,6 +80,10 @@ import struct
|
||||
import sys
|
||||
import threading
|
||||
import traceback
|
||||
try:
|
||||
unicode
|
||||
except NameError:
|
||||
unicode = str
|
||||
|
||||
|
||||
_masterkey = None
|
||||
@@ -82,6 +92,7 @@ _dirtylock = threading.RLock()
|
||||
_leaderlock = gthread.RLock()
|
||||
_synclock = threading.RLock()
|
||||
_rpclock = gthread.RLock()
|
||||
_initlock = gthread.RLock()
|
||||
_followerlocks = {}
|
||||
_config_areas = ('nodegroups', 'nodes', 'usergroups', 'users')
|
||||
tracelog = None
|
||||
@@ -94,8 +105,11 @@ _hasquorum = True
|
||||
_attraliases = {
|
||||
'bmc': 'hardwaremanagement.manager',
|
||||
'bmcuser': 'secret.hardwaremanagementuser',
|
||||
'switchuser': 'secret.hardwaremanagementuser',
|
||||
'bmcpass': 'secret.hardwaremanagementpassword',
|
||||
'switchpass': 'secret.hardwaremanagementpassword',
|
||||
}
|
||||
_validroles = ('Administrator', 'Operator', 'Monitor')
|
||||
|
||||
def _mkpath(pathname):
|
||||
try:
|
||||
@@ -141,10 +155,13 @@ def _parse_key(keydata, password=None):
|
||||
if keydata.startswith('*unencrypted:'):
|
||||
return base64.b64decode(keydata[13:])
|
||||
elif password:
|
||||
salt, iv, crypt, hmac = [base64.b64decode(x)
|
||||
cryptbits = [base64.b64decode(x)
|
||||
for x in keydata.split('!')]
|
||||
salt, iv, crypt, hmac = cryptbits[:4]
|
||||
privkey, integkey = _derive_keys(password, salt)
|
||||
return decrypt_value([iv, crypt, hmac], privkey, integkey)
|
||||
if len(cryptbits) > 4:
|
||||
integkey = None
|
||||
return decrypt_value(cryptbits[1:], privkey, integkey)
|
||||
raise(exc.LockedCredentials(
|
||||
"Passphrase protected secret requires password"))
|
||||
|
||||
@@ -153,7 +170,7 @@ def _format_key(key, password=None):
|
||||
if password is not None:
|
||||
salt = os.urandom(32)
|
||||
privkey, integkey = _derive_keys(password, salt)
|
||||
cval = crypt_value(key, key=privkey, integritykey=integkey)
|
||||
cval = crypt_value(key, key=privkey) # , integritykey=integkey)
|
||||
return {"passphraseprotected": (salt,) + cval}
|
||||
else:
|
||||
return {"unencryptedvalue": key}
|
||||
@@ -166,6 +183,24 @@ def _do_notifier(cfg, watcher, callback):
|
||||
logException()
|
||||
|
||||
|
||||
|
||||
def _rpc_master_del_usergroup(tenant, name):
|
||||
ConfigManager(tenant).del_usergroup(name)
|
||||
|
||||
|
||||
def _rpc_del_usergroup(tenant, name):
|
||||
ConfigManager(tenant)._true_del_usergroup(name)
|
||||
|
||||
|
||||
|
||||
def _rpc_master_set_usergroup(tenant, name, attributemap):
|
||||
ConfigManager(tenant).set_user(name, attributemap)
|
||||
|
||||
|
||||
def _rpc_set_usergroup(tenant, name, attributemap):
|
||||
ConfigManager(tenant)._true_set_user(name, attributemap)
|
||||
|
||||
|
||||
def _rpc_master_set_user(tenant, name, attributemap):
|
||||
ConfigManager(tenant).set_user(name, attributemap)
|
||||
|
||||
@@ -178,6 +213,14 @@ def _rpc_master_set_node_attributes(tenant, attribmap, autocreate):
|
||||
ConfigManager(tenant).set_node_attributes(attribmap, autocreate)
|
||||
|
||||
|
||||
def _rpc_master_rename_nodes(tenant, renamemap):
|
||||
ConfigManager(tenant).rename_nodes(renamemap)
|
||||
|
||||
|
||||
def _rpc_master_rename_nodegroups(tenant, renamemap):
|
||||
ConfigManager(tenant).rename_nodegroups(renamemap)
|
||||
|
||||
|
||||
def _rpc_master_clear_node_attributes(tenant, nodes, attributes):
|
||||
ConfigManager(tenant).clear_node_attributes(nodes, attributes)
|
||||
|
||||
@@ -209,9 +252,19 @@ def _rpc_del_user(tenant, name):
|
||||
def _rpc_master_create_user(tenant, *args):
|
||||
ConfigManager(tenant).create_user(*args)
|
||||
|
||||
|
||||
def _rpc_master_create_usergroup(tenant, *args):
|
||||
ConfigManager(tenant).create_usergroup(*args)
|
||||
|
||||
|
||||
def _rpc_create_user(tenant, *args):
|
||||
ConfigManager(tenant)._true_create_user(*args)
|
||||
|
||||
|
||||
def _rpc_create_usergroup(tenant, *args):
|
||||
ConfigManager(tenant)._true_create_usergroup(*args)
|
||||
|
||||
|
||||
def _rpc_master_del_groups(tenant, groups):
|
||||
ConfigManager(tenant).del_groups(groups)
|
||||
|
||||
@@ -231,6 +284,14 @@ def _rpc_set_node_attributes(tenant, attribmap, autocreate):
|
||||
ConfigManager(tenant)._true_set_node_attributes(attribmap, autocreate)
|
||||
|
||||
|
||||
def _rpc_rename_nodes(tenant, renamemap):
|
||||
ConfigManager(tenant)._true_rename_nodes(renamemap)
|
||||
|
||||
|
||||
def _rpc_rename_nodegroups(tenant, renamemap):
|
||||
ConfigManager(tenant)._true_rename_nodegroups(renamemap)
|
||||
|
||||
|
||||
def _rpc_set_group_attributes(tenant, attribmap, autocreate):
|
||||
ConfigManager(tenant)._true_set_group_attributes(attribmap, autocreate)
|
||||
|
||||
@@ -285,9 +346,9 @@ def logException():
|
||||
event=confluent.log.Events.stacktrace)
|
||||
|
||||
|
||||
def _do_add_watcher(watcher, added, configmanager):
|
||||
def _do_add_watcher(watcher, added, configmanager, renamed=()):
|
||||
try:
|
||||
watcher(added=added, deleting=[], configmanager=configmanager)
|
||||
watcher(added=added, deleting=(), renamed=renamed, configmanager=configmanager)
|
||||
except Exception:
|
||||
logException()
|
||||
|
||||
@@ -308,11 +369,11 @@ def init_masterkey(password=None, autogen=True):
|
||||
if cfgn:
|
||||
_masterintegritykey = _get_protected_key(cfgn, password,
|
||||
'master_integrity_key')
|
||||
elif autogen:
|
||||
_masterintegritykey = os.urandom(64)
|
||||
set_global('master_integrity_key', _format_key(
|
||||
_masterintegritykey,
|
||||
password=password))
|
||||
#elif autogen:
|
||||
# _masterintegritykey = os.urandom(64)
|
||||
# set_global('master_integrity_key', _format_key(
|
||||
# _masterintegritykey,
|
||||
# password=password))
|
||||
|
||||
|
||||
def _push_rpc(stream, payload):
|
||||
@@ -325,25 +386,34 @@ def _push_rpc(stream, payload):
|
||||
def decrypt_value(cryptvalue,
|
||||
key=None,
|
||||
integritykey=None):
|
||||
iv, cipherdata, hmac = cryptvalue
|
||||
# for future reference, if cryptvalue len == 3, then cbc+hmac, 4 includes version
|
||||
iv, cipherdata, hmac = cryptvalue[:3]
|
||||
if key is None and integritykey is None:
|
||||
if _masterkey is None or _masterintegritykey is None:
|
||||
if _masterkey is None:
|
||||
init_masterkey(autogen=False)
|
||||
key = _masterkey
|
||||
integritykey = _masterintegritykey
|
||||
check_hmac = HMAC.new(integritykey, cipherdata, SHA256).digest()
|
||||
if hmac != check_hmac:
|
||||
raise Exception("bad HMAC value on crypted value")
|
||||
decrypter = AES.new(key, AES.MODE_CBC, iv)
|
||||
value = decrypter.decrypt(cipherdata)
|
||||
padsize = ord(value[-1])
|
||||
pad = value[-padsize:]
|
||||
# Note that I cannot grasp what could be done with a subliminal
|
||||
# channel in padding in this case, but check the padding anyway
|
||||
for padbyte in pad:
|
||||
if ord(padbyte) != padsize:
|
||||
raise Exception("bad padding in encrypted value")
|
||||
return value[0:-padsize]
|
||||
if len(cryptvalue) == 3:
|
||||
check_hmac = HMAC.new(integritykey, cipherdata, SHA256).digest()
|
||||
if hmac != check_hmac:
|
||||
check_hmac = HMAC.new(integritykey, cipherdata + iv, SHA256).digest()
|
||||
if hmac != check_hmac:
|
||||
raise Exception("bad HMAC value on crypted value")
|
||||
decrypter = AES.new(key, AES.MODE_CBC, iv)
|
||||
value = decrypter.decrypt(cipherdata)
|
||||
padsize = ord(value[-1])
|
||||
pad = value[-padsize:]
|
||||
# Note that I cannot grasp what could be done with a subliminal
|
||||
# channel in padding in this case, but check the padding anyway
|
||||
for padbyte in pad:
|
||||
if ord(padbyte) != padsize:
|
||||
raise Exception("bad padding in encrypted value")
|
||||
return value[0:-padsize]
|
||||
else:
|
||||
decrypter = AES.new(key, AES.MODE_GCM, nonce=iv)
|
||||
value = decrypter.decrypt(cipherdata)
|
||||
decrypter.verify(hmac)
|
||||
return value
|
||||
|
||||
|
||||
def fixup_attribute(attrname, attrval):
|
||||
@@ -395,22 +465,16 @@ def crypt_value(value,
|
||||
# encrypt given value
|
||||
# PKCS7 is the padding scheme to employ, if no padded needed, pad with 16
|
||||
# check HMAC prior to attempting decrypt
|
||||
if key is None or integritykey is None:
|
||||
if _masterkey is None or _masterintegritykey is None:
|
||||
hmac = None
|
||||
if key is None:
|
||||
if _masterkey is None:
|
||||
init_masterkey()
|
||||
key = _masterkey
|
||||
integritykey = _masterintegritykey
|
||||
iv = os.urandom(16)
|
||||
crypter = AES.new(key, AES.MODE_CBC, iv)
|
||||
neededpad = 16 - (len(value) % 16)
|
||||
pad = chr(neededpad) * neededpad
|
||||
value += pad
|
||||
try:
|
||||
cryptval = crypter.encrypt(value)
|
||||
except TypeError:
|
||||
cryptval = crypter.encrypt(value.encode('utf-8'))
|
||||
hmac = HMAC.new(integritykey, cryptval, SHA256).digest()
|
||||
return iv, cryptval, hmac
|
||||
iv = os.urandom(12)
|
||||
crypter = AES.new(key, AES.MODE_GCM, nonce=iv)
|
||||
value = confluent.util.stringify(value).encode('utf-8')
|
||||
cryptval, hmac = crypter.encrypt_and_digest(value)
|
||||
return iv, cryptval, hmac, '\x02'
|
||||
|
||||
|
||||
def _load_dict_from_dbm(dpath, tdb):
|
||||
@@ -418,16 +482,19 @@ def _load_dict_from_dbm(dpath, tdb):
|
||||
dbe = dbm.open(tdb, 'r')
|
||||
currdict = _cfgstore
|
||||
for elem in dpath:
|
||||
elem = confluent.util.stringify(elem)
|
||||
if elem not in currdict:
|
||||
currdict[elem] = {}
|
||||
currdict = currdict[elem]
|
||||
try:
|
||||
for tk in dbe:
|
||||
currdict[tk] = cPickle.loads(dbe[tk])
|
||||
for tk in dbe.keys():
|
||||
tks = confluent.util.stringify(tk)
|
||||
currdict[tks] = cPickle.loads(dbe[tk])
|
||||
except AttributeError:
|
||||
tk = dbe.firstkey()
|
||||
while tk != None:
|
||||
currdict[tk] = cPickle.loads(dbe[tk])
|
||||
tks = confluent.util.stringify(tk)
|
||||
currdict[tks] = cPickle.loads(dbe[tk])
|
||||
tk = dbe.nextkey(tk)
|
||||
except dbm.error:
|
||||
return
|
||||
@@ -466,13 +533,7 @@ def set_global(globalname, value, sync=True):
|
||||
"""
|
||||
if _cfgstore is None:
|
||||
init(not sync)
|
||||
try:
|
||||
globalname = globalname.encode('utf-8')
|
||||
except AttributeError:
|
||||
# We have to remove the unicode-ness of the string,
|
||||
# but if it is already bytes in python 3, then we will
|
||||
# get an attributeerror, so pass
|
||||
pass
|
||||
globalname = confluent.util.stringify(globalname)
|
||||
with _dirtylock:
|
||||
if 'dirtyglobals' not in _cfgstore:
|
||||
_cfgstore['dirtyglobals'] = set()
|
||||
@@ -623,7 +684,7 @@ def rollback_clear():
|
||||
_cfgstore = _oldcfgstore
|
||||
_oldtxcount = 0
|
||||
_oldcfgstore = None
|
||||
ConfigManager._bg_sync_to_file()
|
||||
ConfigManager.wait_for_sync(True)
|
||||
|
||||
|
||||
def clear_configuration():
|
||||
@@ -643,12 +704,13 @@ def commit_clear():
|
||||
global _oldcfgstore
|
||||
_oldcfgstore = None
|
||||
_oldtxcount = 0
|
||||
todelete = _config_areas + ('globals', 'collective', 'transactioncount')
|
||||
for cfg in todelete:
|
||||
try:
|
||||
os.remove(os.path.join(ConfigManager._cfgdir, cfg))
|
||||
except OSError as oe:
|
||||
pass
|
||||
with _synclock:
|
||||
todelete = ('transactioncount', 'globals', 'collective') + _config_areas
|
||||
for cfg in todelete:
|
||||
try:
|
||||
os.remove(os.path.join(ConfigManager._cfgdir, cfg))
|
||||
except OSError as oe:
|
||||
pass
|
||||
ConfigManager.wait_for_sync(True)
|
||||
ConfigManager._bg_sync_to_file()
|
||||
|
||||
@@ -658,41 +720,43 @@ cfgleader = None
|
||||
def follow_channel(channel):
|
||||
global _txcount
|
||||
global _hasquorum
|
||||
stop_leading()
|
||||
stop_following(channel)
|
||||
lh = StreamHandler(channel)
|
||||
msg = lh.get_next_msg()
|
||||
while msg:
|
||||
sz = struct.unpack('!Q', msg)[0]
|
||||
if sz != 0:
|
||||
rpc = ''
|
||||
while len(rpc) < sz:
|
||||
nrpc = channel.recv(sz - len(rpc))
|
||||
if not nrpc:
|
||||
raise Exception('Truncated message error')
|
||||
rpc += nrpc
|
||||
rpc = cPickle.loads(rpc)
|
||||
if 'txcount' in rpc:
|
||||
_txcount = rpc['txcount']
|
||||
if 'function' in rpc:
|
||||
try:
|
||||
globals()[rpc['function']](*rpc['args'])
|
||||
except Exception as e:
|
||||
print(repr(e))
|
||||
if 'xid' in rpc and rpc['xid']:
|
||||
if rpc.get('exc', None):
|
||||
_pendingchangesets[rpc['xid']].send_exception(rpc['exc'])
|
||||
else:
|
||||
_pendingchangesets[rpc['xid']].send()
|
||||
if 'quorum' in rpc:
|
||||
_hasquorum = rpc['quorum']
|
||||
_push_rpc(channel, b'') # use null as ACK
|
||||
try:
|
||||
stop_leading()
|
||||
stop_following(channel)
|
||||
lh = StreamHandler(channel)
|
||||
msg = lh.get_next_msg()
|
||||
# mark the connection as broken
|
||||
if cfgstreams:
|
||||
stop_following(None)
|
||||
else:
|
||||
stop_following(True)
|
||||
while msg:
|
||||
sz = struct.unpack('!Q', msg)[0]
|
||||
if sz != 0:
|
||||
rpc = ''
|
||||
while len(rpc) < sz:
|
||||
nrpc = channel.recv(sz - len(rpc))
|
||||
if not nrpc:
|
||||
raise Exception('Truncated message error')
|
||||
rpc += nrpc
|
||||
rpc = cPickle.loads(rpc)
|
||||
if 'txcount' in rpc:
|
||||
_txcount = rpc['txcount']
|
||||
if 'function' in rpc:
|
||||
try:
|
||||
globals()[rpc['function']](*rpc['args'])
|
||||
except Exception as e:
|
||||
print(repr(e))
|
||||
if 'xid' in rpc and rpc['xid']:
|
||||
if rpc.get('exc', None):
|
||||
_pendingchangesets[rpc['xid']].send_exception(rpc['exc'])
|
||||
else:
|
||||
_pendingchangesets[rpc['xid']].send()
|
||||
if 'quorum' in rpc:
|
||||
_hasquorum = rpc['quorum']
|
||||
_push_rpc(channel, b'') # use null as ACK
|
||||
msg = lh.get_next_msg()
|
||||
finally:
|
||||
# mark the connection as broken
|
||||
if cfgstreams:
|
||||
stop_following(None)
|
||||
else:
|
||||
stop_following(True)
|
||||
|
||||
|
||||
def add_collective_member(name, address, fingerprint):
|
||||
@@ -726,10 +790,7 @@ def apply_pending_collective_updates():
|
||||
|
||||
|
||||
def _true_add_collective_member(name, address, fingerprint, sync=True):
|
||||
try:
|
||||
name = name.encode('utf-8')
|
||||
except AttributeError:
|
||||
pass
|
||||
name = confluent.util.stringify(name)
|
||||
if _cfgstore is None:
|
||||
init(not sync) # use not sync to avoid read from disk
|
||||
if 'collective' not in _cfgstore:
|
||||
@@ -764,8 +825,7 @@ def get_collective_member_by_address(address):
|
||||
|
||||
|
||||
def _mark_dirtykey(category, key, tenant=None):
|
||||
if type(key) in (str, unicode):
|
||||
key = key.encode('utf-8')
|
||||
key = confluent.util.stringify(key)
|
||||
with _dirtylock:
|
||||
if 'dirtykeys' not in _cfgstore:
|
||||
_cfgstore['dirtykeys'] = {}
|
||||
@@ -947,38 +1007,46 @@ class ConfigManager(object):
|
||||
_nodecollwatchers = {}
|
||||
_notifierids = {}
|
||||
|
||||
@property
|
||||
def _cfgstore(self):
|
||||
if self.tenant is None:
|
||||
return _cfgstore['main']
|
||||
return _cfgstore['tenant'][self.tenant]
|
||||
|
||||
def __init__(self, tenant, decrypt=False, username=None):
|
||||
global _cfgstore
|
||||
if _cfgstore is None:
|
||||
init()
|
||||
self.decrypt = decrypt
|
||||
self.current_user = username
|
||||
if tenant is None:
|
||||
self.tenant = None
|
||||
if 'main' not in _cfgstore:
|
||||
_cfgstore['main'] = {}
|
||||
with _initlock:
|
||||
if _cfgstore is None:
|
||||
init()
|
||||
self.decrypt = decrypt
|
||||
self.current_user = username
|
||||
if tenant is None:
|
||||
self.tenant = None
|
||||
if 'main' not in _cfgstore:
|
||||
_cfgstore['main'] = {}
|
||||
self._bg_sync_to_file()
|
||||
if 'nodegroups' not in self._cfgstore: # This can happen during a clear... it seams... and if so it messes up...
|
||||
self._cfgstore['nodegroups'] = {'everything': {'nodes': set()}}
|
||||
_mark_dirtykey('nodegroups', 'everything', self.tenant)
|
||||
self._bg_sync_to_file()
|
||||
if 'nodes' not in self._cfgstore:
|
||||
self._cfgstore['nodes'] = {}
|
||||
self._bg_sync_to_file()
|
||||
return
|
||||
elif 'tenant' not in _cfgstore:
|
||||
_cfgstore['tenant'] = {tenant: {}}
|
||||
self._bg_sync_to_file()
|
||||
self._cfgstore = _cfgstore['main']
|
||||
elif tenant not in _cfgstore['tenant']:
|
||||
_cfgstore['tenant'][tenant] = {}
|
||||
self._bg_sync_to_file()
|
||||
self.tenant = tenant
|
||||
if 'nodegroups' not in self._cfgstore:
|
||||
self._cfgstore['nodegroups'] = {'everything': {'nodes': set()}}
|
||||
self._bg_sync_to_file()
|
||||
self._cfgstore['nodegroups'] = {'everything': {}}
|
||||
_mark_dirtykey('nodegroups', 'everything', self.tenant)
|
||||
if 'nodes' not in self._cfgstore:
|
||||
self._cfgstore['nodes'] = {}
|
||||
self._bg_sync_to_file()
|
||||
return
|
||||
elif 'tenant' not in _cfgstore:
|
||||
_cfgstore['tenant'] = {tenant: {}}
|
||||
self._bg_sync_to_file()
|
||||
elif tenant not in _cfgstore['tenant']:
|
||||
_cfgstore['tenant'][tenant] = {}
|
||||
self._bg_sync_to_file()
|
||||
self.tenant = tenant
|
||||
self._cfgstore = _cfgstore['tenant'][tenant]
|
||||
if 'nodegroups' not in self._cfgstore:
|
||||
self._cfgstore['nodegroups'] = {'everything': {}}
|
||||
if 'nodes' not in self._cfgstore:
|
||||
self._cfgstore['nodes'] = {}
|
||||
self._bg_sync_to_file()
|
||||
self.wait_for_sync()
|
||||
|
||||
def get_collective_member(self, name):
|
||||
return get_collective_member(name)
|
||||
@@ -1025,24 +1093,32 @@ class ConfigManager(object):
|
||||
raise Exception('Invalid Expression')
|
||||
for node in nodes:
|
||||
try:
|
||||
currval = self._cfgstore['nodes'][node][attribute]['value']
|
||||
currvals = [self._cfgstore['nodes'][node][attribute]['value']]
|
||||
except KeyError:
|
||||
# Let's treat 'not set' as being an empty string for this path
|
||||
currval = ''
|
||||
if exmatch:
|
||||
if yieldmatches:
|
||||
if exmatch.search(currval):
|
||||
yield node
|
||||
currvals = list(
|
||||
[self._cfgstore['nodes'][node][x].get('value', '')
|
||||
for x in fnmatch.filter(self._cfgstore['nodes'][node], attribute)])
|
||||
currvals.append('')
|
||||
for currval in currvals:
|
||||
if exmatch:
|
||||
if yieldmatches:
|
||||
if exmatch.search(currval):
|
||||
yield node
|
||||
break
|
||||
else:
|
||||
if not exmatch.search(currval):
|
||||
yield node
|
||||
break
|
||||
else:
|
||||
if not exmatch.search(currval):
|
||||
yield node
|
||||
else:
|
||||
if yieldmatches:
|
||||
if match == currval:
|
||||
yield node
|
||||
else:
|
||||
if match != currval:
|
||||
yield node
|
||||
if yieldmatches:
|
||||
if match == currval:
|
||||
yield node
|
||||
break
|
||||
else:
|
||||
if match != currval:
|
||||
yield node
|
||||
break
|
||||
|
||||
def filter_nodenames(self, expression, nodes=None):
|
||||
"""Filter nodenames by regular expression
|
||||
@@ -1070,9 +1146,9 @@ class ConfigManager(object):
|
||||
Returns an identifier that can be used to unsubscribe from these
|
||||
notifications using remove_watcher
|
||||
"""
|
||||
notifierid = random.randint(0, sys.maxint)
|
||||
notifierid = random.randint(0, sys.maxsize)
|
||||
while notifierid in self._notifierids:
|
||||
notifierid = random.randint(0, sys.maxint)
|
||||
notifierid = random.randint(0, sys.maxsize)
|
||||
self._notifierids[notifierid] = {'attriblist': []}
|
||||
if self.tenant not in self._attribwatchers:
|
||||
self._attribwatchers[self.tenant] = {}
|
||||
@@ -1111,9 +1187,9 @@ class ConfigManager(object):
|
||||
# use in case of cancellation.
|
||||
# I anticipate no more than a handful of watchers of this sort, so
|
||||
# this loop should not have to iterate too many times
|
||||
notifierid = random.randint(0, sys.maxint)
|
||||
notifierid = random.randint(0, sys.maxsize)
|
||||
while notifierid in self._notifierids:
|
||||
notifierid = random.randint(0, sys.maxint)
|
||||
notifierid = random.randint(0, sys.maxsize)
|
||||
# going to track that this is a nodecollection type watcher,
|
||||
# but there is no additional data associated.
|
||||
self._notifierids[notifierid] = set(['nodecollection'])
|
||||
@@ -1144,6 +1220,12 @@ class ConfigManager(object):
|
||||
except KeyError:
|
||||
return []
|
||||
|
||||
def list_usergroups(self):
|
||||
try:
|
||||
return list(self._cfgstore['usergroups'])
|
||||
except KeyError:
|
||||
return []
|
||||
|
||||
def get_user(self, name):
|
||||
"""Get user information from DB
|
||||
|
||||
@@ -1181,19 +1263,67 @@ class ConfigManager(object):
|
||||
:param groupname: the name of teh group to modify
|
||||
:param attributemap: The mapping of keys to values to set
|
||||
"""
|
||||
if cfgleader:
|
||||
return exec_on_leader('_rpc_master_set_usergroup', self.tenant,
|
||||
groupname, attributemap)
|
||||
if cfgstreams:
|
||||
exec_on_followers('_rpc_set_usergroup', self.tenant, groupname,
|
||||
attributemap)
|
||||
self._true_set_usergroup(groupname, attributemap)
|
||||
|
||||
def _true_set_usergroup(self, groupname, attributemap):
|
||||
for attribute in attributemap:
|
||||
self._cfgstore['usergroups'][attribute] = attributemap[attribute]
|
||||
if attribute == 'role':
|
||||
therole = None
|
||||
for candrole in _validroles:
|
||||
if candrole.lower().startswith(attributemap[attribute].lower()):
|
||||
therole = candrole
|
||||
if therole not in _validroles:
|
||||
raise ValueError(
|
||||
'Unrecognized role "{0}" (valid roles: {1})'.format(attributemap[attribute], ','.join(_validroles)))
|
||||
attributemap[attribute] = therole
|
||||
self._cfgstore['usergroups'][groupname][attribute] = attributemap[attribute]
|
||||
_mark_dirtykey('usergroups', groupname, self.tenant)
|
||||
self._bg_sync_to_file()
|
||||
|
||||
def create_usergroup(self, groupname, role="Administrator"):
|
||||
"""Create a new user
|
||||
|
||||
:param groupname: The name of the user group
|
||||
:param role: The role the user should be considered. Can be
|
||||
"Administrator" or "Technician", defaults to
|
||||
"Administrator"
|
||||
"""
|
||||
if cfgleader:
|
||||
return exec_on_leader('_rpc_master_create_usergroup', self.tenant,
|
||||
groupname, role)
|
||||
if cfgstreams:
|
||||
exec_on_followers('_rpc_create_usergroup', self.tenant, groupname,
|
||||
role)
|
||||
self._true_create_usergroup(groupname, role)
|
||||
|
||||
def _true_create_usergroup(self, groupname, role="Administrator"):
|
||||
if 'usergroups' not in self._cfgstore:
|
||||
self._cfgstore['usergroups'] = {}
|
||||
groupname = groupname.encode('utf-8')
|
||||
groupname = confluent.util.stringify(groupname)
|
||||
if groupname in self._cfgstore['usergroups']:
|
||||
raise Exception("Duplicate groupname requested")
|
||||
self._cfgstore['usergroups'][groupname] = {'role': role}
|
||||
_mark_dirtykey('usergroups', groupname, self.tenant)
|
||||
self._bg_sync_to_file()
|
||||
|
||||
def del_usergroup(self, name):
|
||||
if cfgleader:
|
||||
return exec_on_leader('_rpc_master_del_usergroup', self.tenant, name)
|
||||
if cfgstreams:
|
||||
exec_on_followers('_rpc_del_usergroup', self.tenant, name)
|
||||
self._true_del_usergroup(name)
|
||||
|
||||
def _true_del_usergroup(self, name):
|
||||
if name in self._cfgstore['usergroups']:
|
||||
del self._cfgstore['usergroups'][name]
|
||||
_mark_dirtykey('usergroups', name, self.tenant)
|
||||
self._bg_sync_to_file()
|
||||
|
||||
def set_user(self, name, attributemap):
|
||||
"""Set user attribute(s)
|
||||
@@ -1211,6 +1341,15 @@ class ConfigManager(object):
|
||||
def _true_set_user(self, name, attributemap):
|
||||
user = self._cfgstore['users'][name]
|
||||
for attribute in attributemap:
|
||||
if attribute == 'role':
|
||||
therole = None
|
||||
for candrole in _validroles:
|
||||
if candrole.lower().startswith(attributemap[attribute].lower()):
|
||||
therole = candrole
|
||||
if therole not in _validroles:
|
||||
raise ValueError(
|
||||
'Unrecognized role "{0}" (valid roles: {1})'.format(attributemap[attribute], ','.join(_validroles)))
|
||||
attributemap[attribute] = therole
|
||||
if attribute == 'password':
|
||||
salt = os.urandom(8)
|
||||
#TODO: WORKERPOOL, offload password set to a worker
|
||||
@@ -1226,9 +1365,9 @@ class ConfigManager(object):
|
||||
|
||||
def del_user(self, name):
|
||||
if cfgleader:
|
||||
return exec_on_leader('_rpc_master_del_user', name)
|
||||
return exec_on_leader('_rpc_master_del_user', self.tenant, name)
|
||||
if cfgstreams:
|
||||
exec_on_followers('_rpc_del_user', name)
|
||||
exec_on_followers('_rpc_del_user', self.tenant, name)
|
||||
self._true_del_user(name)
|
||||
|
||||
def _true_del_user(self, name):
|
||||
@@ -1268,7 +1407,7 @@ class ConfigManager(object):
|
||||
raise Exception("Duplicate id requested")
|
||||
if 'users' not in self._cfgstore:
|
||||
self._cfgstore['users'] = {}
|
||||
name = name.encode('utf-8')
|
||||
name = confluent.util.stringify(name)
|
||||
if name in self._cfgstore['users']:
|
||||
raise Exception("Duplicate username requested")
|
||||
self._cfgstore['users'][name] = {'id': uid}
|
||||
@@ -1485,6 +1624,9 @@ class ConfigManager(object):
|
||||
newattr = _attraliases[attr]
|
||||
attribmap[group][newattr] = attribmap[group][attr]
|
||||
del attribmap[group][attr]
|
||||
if 'noderange' in attribmap[group]:
|
||||
if len(attribmap[group]) > 1:
|
||||
raise ValueError('noderange attribute must be set by itself')
|
||||
for attr in attribmap[group]:
|
||||
if attr in _attraliases:
|
||||
newattr = _attraliases[attr]
|
||||
@@ -1501,17 +1643,20 @@ class ConfigManager(object):
|
||||
currnodes = list(self.get_nodegroup_attributes(
|
||||
group, ['nodes']).get('nodes', []))
|
||||
if attribmap[group][attr].get('prepend', False):
|
||||
newnodes = attribmap[group][attr][
|
||||
'prepend'].split(',')
|
||||
attribmap[group][attr] = newnodes + currnodes
|
||||
newnodes = noderange.NodeRange(attribmap[group][attr][
|
||||
'prepend'], config=self).nodes
|
||||
attribmap[group][attr] = list(
|
||||
newnodes) + currnodes
|
||||
elif attribmap[group][attr].get('remove', False):
|
||||
delnodes = attribmap[group][attr][
|
||||
'remove'].split(',')
|
||||
delnodes = noderange.NodeRange(
|
||||
attribmap[group][attr]['remove'],
|
||||
config=self).nodes
|
||||
attribmap[group][attr] = [
|
||||
x for x in currnodes if x not in delnodes]
|
||||
if not isinstance(attribmap[group][attr], list):
|
||||
if type(attribmap[group][attr]) is unicode or type(attribmap[group][attr]) is str:
|
||||
attribmap[group][attr]=attribmap[group][attr].split(",")
|
||||
attribmap[group][attr] = noderange.NodeRange(
|
||||
attribmap[group][attr], config=self).nodes
|
||||
else:
|
||||
raise ValueError("nodes attribute on group must be list")
|
||||
for node in attribmap[group]['nodes']:
|
||||
@@ -1520,10 +1665,17 @@ class ConfigManager(object):
|
||||
"{0} node does not exist to add to {1}".format(
|
||||
node, group))
|
||||
for group in attribmap:
|
||||
group = group.encode('utf-8')
|
||||
group = confluent.util.stringify(group)
|
||||
if group not in self._cfgstore['nodegroups']:
|
||||
self._cfgstore['nodegroups'][group] = {'nodes': set()}
|
||||
cfgobj = self._cfgstore['nodegroups'][group]
|
||||
if 'noderange' in attribmap[group] and attribmap[group]['noderange']:
|
||||
if cfgobj['nodes']:
|
||||
raise ValueError('Cannot set both nodes and noderange on group')
|
||||
if set(cfgobj) - set(['noderange', 'nodes']):
|
||||
raise ValueError('Cannot set noderange on a group with attributes')
|
||||
elif 'noderange' in cfgobj and cfgobj['noderange']:
|
||||
raise ValueError('Attributes cannot be set on a group with a noderange')
|
||||
for attr in attribmap[group]:
|
||||
if attr == 'nodes':
|
||||
newdict = set(attribmap[group][attr])
|
||||
@@ -1571,8 +1723,8 @@ class ConfigManager(object):
|
||||
attributes = realattributes
|
||||
if type(groups) in (str, unicode):
|
||||
groups = (groups,)
|
||||
for group in groups:
|
||||
group = group.encode('utf-8')
|
||||
for group in groups:
|
||||
group = confluent.util.stringify(group)
|
||||
try:
|
||||
groupentry = self._cfgstore['nodegroups'][group]
|
||||
except KeyError:
|
||||
@@ -1663,7 +1815,8 @@ class ConfigManager(object):
|
||||
'nodeattrs': {node: [attrname]},
|
||||
'callback': attribwatcher[watchkey][notifierid]
|
||||
}
|
||||
for watcher in notifdata.itervalues():
|
||||
for watcher in notifdata:
|
||||
watcher = notifdata[watcher]
|
||||
callback = watcher['callback']
|
||||
eventlet.spawn_n(_do_notifier, self, watcher, callback)
|
||||
|
||||
@@ -1677,14 +1830,15 @@ class ConfigManager(object):
|
||||
|
||||
def _true_del_nodes(self, nodes):
|
||||
if self.tenant in self._nodecollwatchers:
|
||||
for watcher in self._nodecollwatchers[self.tenant].itervalues():
|
||||
watcher(added=[], deleting=nodes, configmanager=self)
|
||||
for watcher in self._nodecollwatchers[self.tenant]:
|
||||
watcher = self._nodecollwatchers[self.tenant][watcher]
|
||||
watcher(added=(), deleting=nodes, renamed=(), configmanager=self)
|
||||
changeset = {}
|
||||
for node in nodes:
|
||||
# set a reserved attribute for the sake of the change notification
|
||||
# framework to trigger on
|
||||
changeset[node] = {'_nodedeleted': 1}
|
||||
node = node.encode('utf-8')
|
||||
node = confluent.util.stringify(node)
|
||||
if node in self._cfgstore['nodes']:
|
||||
self._sync_groups_to_node(node=node, groups=[],
|
||||
changeset=changeset)
|
||||
@@ -1732,7 +1886,7 @@ class ConfigManager(object):
|
||||
realattributes.append(attrname)
|
||||
attributes = realattributes
|
||||
for node in nodes:
|
||||
node = node.encode('utf-8')
|
||||
node = confluent.util.stringify(node)
|
||||
try:
|
||||
nodek = self._cfgstore['nodes'][node]
|
||||
except KeyError:
|
||||
@@ -1762,6 +1916,87 @@ class ConfigManager(object):
|
||||
attribmap[node]['groups'] = []
|
||||
self.set_node_attributes(attribmap, autocreate=True)
|
||||
|
||||
def rename_nodes(self, renamemap):
|
||||
if cfgleader:
|
||||
return exec_on_leader('_rpc_master_rename_nodes', self.tenant,
|
||||
renamemap)
|
||||
if cfgstreams:
|
||||
exec_on_followers('_rpc_rename_nodes', self.tenant, renamemap)
|
||||
self._true_rename_nodes(renamemap)
|
||||
|
||||
def _true_rename_nodes(self, renamemap):
|
||||
oldnames = set(renamemap)
|
||||
exprmgr = None
|
||||
currnodes = set(self._cfgstore['nodes'])
|
||||
missingnodes = oldnames - currnodes
|
||||
if missingnodes:
|
||||
raise ValueError(
|
||||
'The following nodes to rename do not exist: {0}'.format(
|
||||
','.join(missingnodes)))
|
||||
newnames = set([])
|
||||
for name in renamemap:
|
||||
newnames.add(renamemap[name])
|
||||
if newnames & currnodes:
|
||||
raise ValueError(
|
||||
'The following requested new names conflict with existing nodes: {0}'.format(
|
||||
','.join(newnames & currnodes)))
|
||||
for name in renamemap:
|
||||
self._cfgstore['nodes'][renamemap[name]] = self._cfgstore['nodes'][name]
|
||||
del self._cfgstore['nodes'][name]
|
||||
_mark_dirtykey('nodes', name, self.tenant)
|
||||
_mark_dirtykey('nodes', renamemap[name], self.tenant)
|
||||
for group in self._cfgstore['nodes'][renamemap[name]].get('groups', []):
|
||||
self._cfgstore['nodegroups'][group]['nodes'].discard(name)
|
||||
self._cfgstore['nodegroups'][group]['nodes'].add(renamemap[name])
|
||||
_mark_dirtykey('nodegroups', group, self.tenant)
|
||||
cfgobj = self._cfgstore['nodes'][renamemap[name]]
|
||||
node = renamemap[name]
|
||||
changeset = {}
|
||||
if exprmgr is None:
|
||||
exprmgr = _ExpressionFormat(cfgobj, node)
|
||||
self._recalculate_expressions(cfgobj, formatter=exprmgr, node=renamemap[name], changeset=changeset)
|
||||
if self.tenant in self._nodecollwatchers:
|
||||
nodecollwatchers = self._nodecollwatchers[self.tenant]
|
||||
for watcher in nodecollwatchers:
|
||||
watcher = nodecollwatchers[watcher]
|
||||
eventlet.spawn_n(_do_add_watcher, watcher, (), self, renamemap)
|
||||
self._bg_sync_to_file()
|
||||
|
||||
def rename_nodegroups(self, renamemap):
|
||||
if cfgleader:
|
||||
return exec_on_leader('_rpc_master_rename_nodegroups', self.tenant, renamemap)
|
||||
if cfgstreams:
|
||||
exec_on_followers('_rpc_rename_nodegroups', self.tenant, renamemap)
|
||||
self._true_rename_groups(renamemap)
|
||||
|
||||
def _true_rename_groups(self, renamemap):
|
||||
oldnames = set(renamemap)
|
||||
currgroups = set(self._cfgstore['nodegroups'])
|
||||
missinggroups = oldnames - currgroups
|
||||
if missinggroups:
|
||||
raise ValueError(
|
||||
'The following groups to rename do not exist: {0}'.format(
|
||||
','.join(missinggroups)))
|
||||
newnames = set([])
|
||||
for name in renamemap:
|
||||
newnames.add(renamemap[name])
|
||||
if newnames & currgroups:
|
||||
raise ValueError(
|
||||
'The following requested new names conflict with existing groups: {0}'.format(
|
||||
','.join(newnames & currgroups)))
|
||||
for name in renamemap:
|
||||
self._cfgstore['nodegroups'][renamemap[name]] = self._cfgstore['nodegroups'][name]
|
||||
del self._cfgstore['nodegroups'][name]
|
||||
_mark_dirtykey('nodegroups', name, self.tenant)
|
||||
_mark_dirtykey('nodegroups', renamemap[name], self.tenant)
|
||||
for node in self._cfgstore['nodegroups'][renamemap[name]].get('nodes', []):
|
||||
lidx = self._cfgstore['nodes'][node]['groups'].index(name)
|
||||
self._cfgstore['nodes'][node]['groups'][lidx] = renamemap[name]
|
||||
_mark_dirtykey('nodes', node, self.tenant)
|
||||
self._bg_sync_to_file()
|
||||
|
||||
|
||||
|
||||
def set_node_attributes(self, attribmap, autocreate=False):
|
||||
if cfgleader: # currently config slave to another
|
||||
return exec_on_leader('_rpc_master_set_node_attributes',
|
||||
@@ -1780,7 +2015,7 @@ class ConfigManager(object):
|
||||
# first do a sanity check of the input upfront
|
||||
# this mitigates risk of arguments being partially applied
|
||||
for node in attribmap:
|
||||
node = node.encode('utf-8')
|
||||
node = confluent.util.stringify(node)
|
||||
if node == '':
|
||||
raise ValueError('"{0}" is not a valid node name'.format(node))
|
||||
if autocreate:
|
||||
@@ -1835,8 +2070,8 @@ class ConfigManager(object):
|
||||
attrname, node)
|
||||
raise ValueError(errstr)
|
||||
attribmap[node][attrname] = attrval
|
||||
for node in attribmap:
|
||||
node = node.encode('utf-8')
|
||||
for node in attribmap:
|
||||
node = confluent.util.stringify(node)
|
||||
exprmgr = None
|
||||
if node not in self._cfgstore['nodes']:
|
||||
newnodes.append(node)
|
||||
@@ -1879,7 +2114,8 @@ class ConfigManager(object):
|
||||
if newnodes:
|
||||
if self.tenant in self._nodecollwatchers:
|
||||
nodecollwatchers = self._nodecollwatchers[self.tenant]
|
||||
for watcher in nodecollwatchers.itervalues():
|
||||
for watcher in nodecollwatchers:
|
||||
watcher = nodecollwatchers[watcher]
|
||||
eventlet.spawn_n(_do_add_watcher, watcher, newnodes, self)
|
||||
self._bg_sync_to_file()
|
||||
#TODO: wait for synchronization to suceed/fail??)
|
||||
@@ -1946,6 +2182,9 @@ class ConfigManager(object):
|
||||
self.set_node_attributes(tmpconfig[confarea], True)
|
||||
elif confarea == 'nodegroups':
|
||||
self.set_group_attributes(tmpconfig[confarea], True)
|
||||
elif confarea == 'usergroups':
|
||||
for usergroup in tmpconfig[confarea]:
|
||||
self.create_usergroup(usergroup)
|
||||
elif confarea == 'users':
|
||||
for user in tmpconfig[confarea]:
|
||||
uid = tmpconfig[confarea].get('id', None)
|
||||
@@ -2014,8 +2253,10 @@ class ConfigManager(object):
|
||||
_cfgstore = {}
|
||||
rootpath = cls._cfgdir
|
||||
try:
|
||||
with open(os.path.join(rootpath, 'transactioncount'), 'r') as f:
|
||||
_txcount = struct.unpack('!Q', f.read())[0]
|
||||
with open(os.path.join(rootpath, 'transactioncount'), 'rb') as f:
|
||||
txbytes = f.read()
|
||||
if len(txbytes) == 8:
|
||||
_txcount = struct.unpack('!Q', txbytes)[0]
|
||||
except IOError:
|
||||
pass
|
||||
_load_dict_from_dbm(['collective'], os.path.join(rootpath,
|
||||
@@ -2050,9 +2291,13 @@ class ConfigManager(object):
|
||||
if statelessmode:
|
||||
return
|
||||
with cls._syncstate:
|
||||
if cls._syncrunning:
|
||||
if (cls._syncrunning and cls._cfgwriter is not None and
|
||||
cls._cfgwriter.isAlive()):
|
||||
cls._writepending = True
|
||||
return
|
||||
if cls._syncrunning: # This suggests an unclean write attempt,
|
||||
# do a fullsync as a recovery
|
||||
fullsync = True
|
||||
cls._syncrunning = True
|
||||
# if the thread is exiting, join it to let it close, just in case
|
||||
if cls._cfgwriter is not None:
|
||||
@@ -2065,16 +2310,17 @@ class ConfigManager(object):
|
||||
with _synclock:
|
||||
if statelessmode:
|
||||
return
|
||||
with open(os.path.join(cls._cfgdir, 'transactioncount'), 'w') as f:
|
||||
_mkpath(cls._cfgdir)
|
||||
with open(os.path.join(cls._cfgdir, 'transactioncount'), 'wb') as f:
|
||||
f.write(struct.pack('!Q', _txcount))
|
||||
if fullsync or 'dirtyglobals' in _cfgstore:
|
||||
if fullsync:
|
||||
if (fullsync or 'dirtyglobals' in _cfgstore and
|
||||
'globals' in _cfgstore):
|
||||
if fullsync: # globals is not a given to be set..
|
||||
dirtyglobals = _cfgstore['globals']
|
||||
else:
|
||||
with _dirtylock:
|
||||
dirtyglobals = copy.deepcopy(_cfgstore['dirtyglobals'])
|
||||
del _cfgstore['dirtyglobals']
|
||||
_mkpath(cls._cfgdir)
|
||||
globalf = dbm.open(os.path.join(cls._cfgdir, "globals"), 'c', 384) # 0600
|
||||
try:
|
||||
for globalkey in dirtyglobals:
|
||||
@@ -2176,7 +2422,9 @@ def _restore_keys(jsond, password, newpassword=None, sync=True):
|
||||
else:
|
||||
keydata = json.loads(jsond)
|
||||
cryptkey = _parse_key(keydata['cryptkey'], password)
|
||||
integritykey = _parse_key(keydata['integritykey'], password)
|
||||
integritykey = None
|
||||
if 'integritykey' in keydata:
|
||||
integritykey = _parse_key(keydata['integritykey'], password)
|
||||
conf.init_config()
|
||||
cfg = conf.get_config()
|
||||
if cfg.has_option('security', 'externalcfgkey'):
|
||||
@@ -2185,8 +2433,9 @@ def _restore_keys(jsond, password, newpassword=None, sync=True):
|
||||
newpassword = keyfile.read()
|
||||
set_global('master_privacy_key', _format_key(cryptkey,
|
||||
password=newpassword), sync)
|
||||
set_global('master_integrity_key', _format_key(integritykey,
|
||||
password=newpassword), sync)
|
||||
if integritykey:
|
||||
set_global('master_integrity_key', _format_key(integritykey,
|
||||
password=newpassword), sync)
|
||||
_masterkey = cryptkey
|
||||
_masterintegritykey = integritykey
|
||||
if sync:
|
||||
@@ -2194,7 +2443,7 @@ def _restore_keys(jsond, password, newpassword=None, sync=True):
|
||||
|
||||
|
||||
def _dump_keys(password, dojson=True):
|
||||
if _masterkey is None or _masterintegritykey is None:
|
||||
if _masterkey is None:
|
||||
init_masterkey()
|
||||
cryptkey = _format_key(_masterkey, password=password)
|
||||
if 'passphraseprotected' in cryptkey:
|
||||
@@ -2203,14 +2452,16 @@ def _dump_keys(password, dojson=True):
|
||||
else:
|
||||
cryptkey = '*unencrypted:{0}'.format(base64.b64encode(
|
||||
cryptkey['unencryptedvalue']))
|
||||
integritykey = _format_key(_masterintegritykey, password=password)
|
||||
if 'passphraseprotected' in integritykey:
|
||||
integritykey = '!'.join(map(base64.b64encode,
|
||||
integritykey['passphraseprotected']))
|
||||
else:
|
||||
integritykey = '*unencrypted:{0}'.format(base64.b64encode(
|
||||
integritykey['unencryptedvalue']))
|
||||
keydata = {'cryptkey': cryptkey, 'integritykey': integritykey}
|
||||
keydata = {'cryptkey': cryptkey}
|
||||
if _masterintegritykey is not None:
|
||||
integritykey = _format_key(_masterintegritykey, password=password)
|
||||
if 'passphraseprotected' in integritykey:
|
||||
integritykey = '!'.join(map(base64.b64encode,
|
||||
integritykey['passphraseprotected']))
|
||||
else:
|
||||
integritykey = '*unencrypted:{0}'.format(base64.b64encode(
|
||||
integritykey['unencryptedvalue']))
|
||||
keydata['integritykey'] = integritykey
|
||||
if dojson:
|
||||
return json.dumps(keydata, sort_keys=True, indent=4, separators=(',', ': '))
|
||||
return keydata
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2014 IBM Corporation
|
||||
# Copyright 2017 Lenovo
|
||||
# Copyright 2017-2018 Lenovo
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -94,7 +94,7 @@ def _utf8_normalize(data, shiftin, decoder):
|
||||
|
||||
|
||||
def pytechars2line(chars, maxlen=None):
|
||||
line = '\x1b[m' # start at default params
|
||||
line = b'\x1b[m' # start at default params
|
||||
lb = False # last bold
|
||||
li = False # last italic
|
||||
lu = False # last underline
|
||||
@@ -130,9 +130,12 @@ def pytechars2line(chars, maxlen=None):
|
||||
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():
|
||||
if not hasdata and char.data.rstrip():
|
||||
hasdata = True
|
||||
line += char.data.encode('utf-8')
|
||||
chardata = char.data
|
||||
if not isinstance(chardata, bytes):
|
||||
chardata = chardata.encode('utf-8')
|
||||
line += chardata
|
||||
if maxlen and len >= maxlen:
|
||||
break
|
||||
len += 1
|
||||
@@ -145,8 +148,10 @@ class ConsoleHandler(object):
|
||||
_genwatchattribs = frozenset(('console.method', 'console.logging',
|
||||
'collective.manager'))
|
||||
|
||||
def __init__(self, node, configmanager):
|
||||
def __init__(self, node, configmanager, width=80, height=24):
|
||||
self.clearpending = False
|
||||
self.clearerror = False
|
||||
self.initsize = (width, height)
|
||||
self._dologging = True
|
||||
self._is_local = True
|
||||
self._isondemand = False
|
||||
@@ -183,7 +188,7 @@ class ConsoleHandler(object):
|
||||
if termstate & 1:
|
||||
self.appmodedetected = True
|
||||
if termstate & 2:
|
||||
self.shiftin = '0'
|
||||
self.shiftin = b'0'
|
||||
self.users = {}
|
||||
self._attribwatcher = None
|
||||
self._console = None
|
||||
@@ -208,6 +213,8 @@ class ConsoleHandler(object):
|
||||
return retrytime + (retrytime * random.random())
|
||||
|
||||
def feedbuffer(self, data):
|
||||
if not isinstance(data, bytes):
|
||||
data = data.encode('utf-8')
|
||||
try:
|
||||
self.termstream.feed(data)
|
||||
except StopIteration: # corrupt parser state, start over
|
||||
@@ -307,8 +314,11 @@ class ConsoleHandler(object):
|
||||
|
||||
def clearbuffer(self):
|
||||
self.feedbuffer(
|
||||
'\x1bc[no replay buffer due to console.logging attribute set to '
|
||||
'none or interactive,\r\nconnection loss, or service restart]')
|
||||
'\x1bc[No data has been received from the remote console since ' \
|
||||
'connecting. This could\r\nbe due to having the console.logging ' \
|
||||
'attribute set to none or interactive,\r\nserial console not ' \
|
||||
'being enabled or incorrectly configured in the OS or\r\nfirmware, ' \
|
||||
'or the console simply not having any output since last connection]')
|
||||
self.clearpending = True
|
||||
|
||||
def _detach(self):
|
||||
@@ -359,16 +369,33 @@ class ConsoleHandler(object):
|
||||
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,
|
||||
'error': self.error})
|
||||
self.feedbuffer(
|
||||
'\x1bc\x1b[2J\x1b[1;1H[The console.method attribute for this node is '
|
||||
'not configured,\r\nset it to a valid value for console '
|
||||
'function]')
|
||||
self._send_rcpts(
|
||||
'\x1bc\x1b[2J\x1b[1;1H[The console.method attribute for this node is '
|
||||
'not configured,\r\nset it to a valid value for console '
|
||||
'function]')
|
||||
self.clearerror = True
|
||||
return
|
||||
if self.clearerror:
|
||||
self.clearerror = False
|
||||
self.clearbuffer()
|
||||
self._send_rcpts(b'\x1bc\x1b[2J\x1b[1;1H')
|
||||
self.send_break = self._console.send_break
|
||||
self.resize = self._console.resize
|
||||
if self._attribwatcher:
|
||||
self.cfgmgr.remove_watcher(self._attribwatcher)
|
||||
self._attribwatcher = None
|
||||
@@ -380,6 +407,7 @@ class ConsoleHandler(object):
|
||||
self._attribwatcher = self.cfgmgr.watch_attributes(
|
||||
(self.node,), attribstowatch, self._attribschanged)
|
||||
try:
|
||||
self.resize(width=self.initsize[0], height=self.initsize[1])
|
||||
self._console.connect(self.get_console_output)
|
||||
except exc.TargetEndpointBadCredentials:
|
||||
self.clearbuffer()
|
||||
@@ -425,6 +453,7 @@ class ConsoleHandler(object):
|
||||
|
||||
def _got_disconnected(self):
|
||||
if self.connectstate != 'unconnected':
|
||||
self._console.close()
|
||||
self.connectstate = 'unconnected'
|
||||
self.log(
|
||||
logdata='console disconnected', ltype=log.DataTypes.event,
|
||||
@@ -505,14 +534,14 @@ class ConsoleHandler(object):
|
||||
elif data == '':
|
||||
# ignore empty strings from a cconsole provider
|
||||
return
|
||||
if '\x1b[?1l' in data: # request for ansi mode cursor keys
|
||||
if b'\x1b[?1l' in data: # request for ansi mode cursor keys
|
||||
self.appmodedetected = False
|
||||
if '\x1b[?1h' in data: # remember the session wants the client to use
|
||||
if b'\x1b[?1h' in data: # remember the session wants the client to use
|
||||
# 'application mode' Thus far only observed on esxi
|
||||
self.appmodedetected = True
|
||||
if '\x1b)0' in data:
|
||||
if b'\x1b)0' in data:
|
||||
# console indicates it wants access to special drawing characters
|
||||
self.shiftin = '0'
|
||||
self.shiftin = b'0'
|
||||
eventdata = 0
|
||||
if self.appmodedetected:
|
||||
eventdata |= 1
|
||||
@@ -521,10 +550,11 @@ class ConsoleHandler(object):
|
||||
# TODO: analyze buffer for registered events, examples:
|
||||
# panics
|
||||
# certificate signing request
|
||||
if self.clearpending:
|
||||
if self.clearpending or self.clearerror:
|
||||
self.clearpending = False
|
||||
self.feedbuffer(b'\x1bc')
|
||||
self._send_rcpts(b'\x1bc')
|
||||
self.clearerror = False
|
||||
self.feedbuffer(b'\x1bc\x1b[2J\x1b[1;1H')
|
||||
self._send_rcpts(b'\x1bc\x1b[2J\x1b[1;1H')
|
||||
self._send_rcpts(_utf8_normalize(data, self.shiftin, self.utf8decoder))
|
||||
self.log(data, eventdata=eventdata)
|
||||
self.lasttime = util.monotonic_time()
|
||||
@@ -564,20 +594,23 @@ class ConsoleHandler(object):
|
||||
if pendingbl:
|
||||
retdata += pendingbl
|
||||
pendingbl = b''
|
||||
retdata += nline + '\r\n'
|
||||
retdata += nline + b'\r\n'
|
||||
else:
|
||||
pendingbl += nline + '\r\n'
|
||||
pendingbl += nline + b'\r\n'
|
||||
if len(retdata) > 6:
|
||||
retdata = retdata[:-2] # remove the last \r\n
|
||||
retdata += b'\x1b[{0};{1}H'.format(self.buffer.cursor.y + 1,
|
||||
self.buffer.cursor.x + 1)
|
||||
cursordata = '\x1b[{0};{1}H'.format(self.buffer.cursor.y + 1,
|
||||
self.buffer.cursor.x + 1)
|
||||
if not isinstance(cursordata, bytes):
|
||||
cursordata = cursordata.encode('utf-8')
|
||||
retdata += cursordata
|
||||
if self.shiftin is not None: # detected that terminal requested a
|
||||
# shiftin character set, relay that to the terminal that cannected
|
||||
retdata += '\x1b)' + self.shiftin
|
||||
retdata += b'\x1b)' + self.shiftin
|
||||
if self.appmodedetected:
|
||||
retdata += '\x1b[?1h'
|
||||
retdata += b'\x1b[?1h'
|
||||
else:
|
||||
retdata += '\x1b[?1l'
|
||||
retdata += b'\x1b[?1l'
|
||||
return retdata, connstate
|
||||
|
||||
def write(self, data):
|
||||
@@ -597,11 +630,14 @@ def disconnect_node(node, configmanager):
|
||||
del _handled_consoles[consk]
|
||||
|
||||
|
||||
def _nodechange(added, deleting, configmanager):
|
||||
for node in added:
|
||||
connect_node(node, configmanager)
|
||||
def _nodechange(added, deleting, renamed, configmanager):
|
||||
for node in deleting:
|
||||
disconnect_node(node, configmanager)
|
||||
for node in renamed:
|
||||
disconnect_node(node, configmanager)
|
||||
connect_node(renamed[node], configmanager)
|
||||
for node in added:
|
||||
connect_node(node, configmanager)
|
||||
|
||||
|
||||
def _start_tenant_sessions(cfm):
|
||||
@@ -625,17 +661,20 @@ def start_console_sessions():
|
||||
configmodule.hook_new_configmanagers(_start_tenant_sessions)
|
||||
|
||||
|
||||
def connect_node(node, configmanager, username=None):
|
||||
def connect_node(node, configmanager, username=None, direct=True, width=80,
|
||||
height=24):
|
||||
attrval = configmanager.get_node_attributes(node, 'collective.manager')
|
||||
myc = attrval.get(node, {}).get('collective.manager', {}).get(
|
||||
'value', None)
|
||||
myname = collective.get_myname()
|
||||
if myc and myc != collective.get_myname():
|
||||
if myc and myc != collective.get_myname() and direct:
|
||||
minfo = configmodule.get_collective_member(myc)
|
||||
return ProxyConsole(node, minfo, myname, configmanager, username)
|
||||
return ProxyConsole(node, minfo, myname, configmanager, username,
|
||||
width, height)
|
||||
consk = (node, configmanager.tenant)
|
||||
if consk not in _handled_consoles:
|
||||
_handled_consoles[consk] = ConsoleHandler(node, configmanager)
|
||||
_handled_consoles[consk] = ConsoleHandler(node, configmanager, width,
|
||||
height)
|
||||
return _handled_consoles[consk]
|
||||
|
||||
# A stub console handler that just passes through to a remote confluent
|
||||
@@ -644,8 +683,10 @@ def connect_node(node, configmanager, username=None):
|
||||
class ProxyConsole(object):
|
||||
_genwatchattribs = frozenset(('collective.manager',))
|
||||
|
||||
def __init__(self, node, managerinfo, myname, configmanager, user):
|
||||
def __init__(self, node, managerinfo, myname, configmanager, user, width,
|
||||
height):
|
||||
self.skipreplay = True
|
||||
self.initsize = (width, height)
|
||||
self.managerinfo = managerinfo
|
||||
self.myname = myname
|
||||
self.cfm = configmanager
|
||||
@@ -656,6 +697,7 @@ class ProxyConsole(object):
|
||||
self._attribwatcher = configmanager.watch_attributes(
|
||||
(self.node,), self._genwatchattribs, self._attribschanged)
|
||||
|
||||
|
||||
def _attribschanged(self, nodeattribs, configmanager, **kwargs):
|
||||
if self.clisession:
|
||||
self.clisession.detach()
|
||||
@@ -697,6 +739,8 @@ class ProxyConsole(object):
|
||||
'tenant': self.cfm.tenant,
|
||||
'node': self.node,
|
||||
'skipreplay': self.skipreplay,
|
||||
'width': self.initsize[0],
|
||||
'height': self.initsize[1],
|
||||
#TODO(jjohnson2): declare myself as a proxy,
|
||||
#facilitate redirect rather than relay on manager change
|
||||
},
|
||||
@@ -736,6 +780,10 @@ class ProxyConsole(object):
|
||||
def reopen(self):
|
||||
tlvdata.send(self.remote, {'operation': 'reopen'})
|
||||
|
||||
def resize(self, width, height):
|
||||
tlvdata.send(self.remote, {'operation': 'resize', 'width': width,
|
||||
'height': height})
|
||||
|
||||
|
||||
# this represents some api view of a console handler. This handles things like
|
||||
# holding the caller specific queue data, for example, when http api should be
|
||||
@@ -758,10 +806,9 @@ class ConsoleSession(object):
|
||||
'get_next_output' non-functional
|
||||
:param skipreplay: If true, will skip the attempt to redraw the screen
|
||||
"""
|
||||
connector = connect_node
|
||||
|
||||
def __init__(self, node, configmanager, username, datacallback=None,
|
||||
skipreplay=False):
|
||||
skipreplay=False, direct=True, width=80, height=24):
|
||||
self.registered = False
|
||||
self.tenant = configmanager.tenant
|
||||
if not configmanager.is_node(node):
|
||||
@@ -769,6 +816,10 @@ class ConsoleSession(object):
|
||||
self.username = username
|
||||
self.node = node
|
||||
self.configmanager = configmanager
|
||||
self.direct = direct # true if client is directly connected versus
|
||||
# relay
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.connect_session()
|
||||
self.registered = True
|
||||
self._evt = None
|
||||
@@ -797,12 +848,16 @@ class ConsoleSession(object):
|
||||
between console and shell.
|
||||
"""
|
||||
self.conshdl = connect_node(self.node, self.configmanager,
|
||||
self.username)
|
||||
self.username, self.direct, self.width,
|
||||
self.height)
|
||||
def send_break(self):
|
||||
"""Send break to remote system
|
||||
"""
|
||||
self.conshdl.send_break()
|
||||
|
||||
def resize(self, width, height):
|
||||
self.conshdl.resize(width, height)
|
||||
|
||||
def get_buffer_age(self):
|
||||
"""Get the age in seconds of the buffered data
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2014 IBM Corporation
|
||||
# Copyright 2015-2017 Lenovo
|
||||
# Copyright 2015-2018 Lenovo
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -35,6 +35,7 @@
|
||||
|
||||
import confluent
|
||||
import confluent.alerts as alerts
|
||||
import confluent.log as log
|
||||
import confluent.tlvdata as tlvdata
|
||||
import confluent.config.attributes as attrscheme
|
||||
import confluent.config.configmanager as cfm
|
||||
@@ -85,7 +86,10 @@ def seek_element(currplace, currkey):
|
||||
|
||||
def nested_lookup(nestdict, key):
|
||||
try:
|
||||
return reduce(seek_element, key, nestdict)
|
||||
currloc = nestdict
|
||||
for currk in key:
|
||||
currloc = seek_element(currloc, currk)
|
||||
return currloc
|
||||
except TypeError:
|
||||
raise exc.NotFoundException("Invalid element requested")
|
||||
|
||||
@@ -105,6 +109,8 @@ def load_plugins():
|
||||
for plugin in os.listdir(plugindir):
|
||||
if plugin.startswith('.'):
|
||||
continue
|
||||
if '__pycache__' in plugin:
|
||||
continue
|
||||
(plugin, plugtype) = os.path.splitext(plugin)
|
||||
if plugtype == '.sh':
|
||||
pluginmap[plugin] = shellmodule.Plugin(
|
||||
@@ -123,7 +129,7 @@ def load_plugins():
|
||||
|
||||
|
||||
rootcollections = ['discovery/', 'events/', 'networking/',
|
||||
'noderange/', 'nodes/', 'nodegroups/', 'users/', 'version']
|
||||
'noderange/', 'nodes/', 'nodegroups/', 'usergroups/' , 'users/', 'version']
|
||||
|
||||
|
||||
class PluginRoute(object):
|
||||
@@ -143,6 +149,7 @@ def _init_core():
|
||||
# be enumerated in any collection
|
||||
noderesources = {
|
||||
'attributes': {
|
||||
'rename': PluginRoute({'handler': 'attributes'}),
|
||||
'all': PluginRoute({'handler': 'attributes'}),
|
||||
'current': PluginRoute({'handler': 'attributes'}),
|
||||
'expression': PluginRoute({'handler': 'attributes'}),
|
||||
@@ -161,10 +168,22 @@ def _init_core():
|
||||
'default': 'ipmi',
|
||||
}),
|
||||
},
|
||||
'clear': PluginRoute({
|
||||
'pluginattrs': ['hardwaremanagement.method'],
|
||||
'default': 'ipmi',
|
||||
}),
|
||||
'users': PluginCollection({
|
||||
'pluginattrs': ['hardwaremanagement.method'],
|
||||
'default': 'ipmi',
|
||||
}),
|
||||
'licenses': PluginCollection({
|
||||
'pluginattrs': ['hardwaremanagement.method'],
|
||||
'default': 'ipmi',
|
||||
}),
|
||||
'save_licenses': PluginRoute({
|
||||
'pluginattrs': ['hardwaremanagement.method'],
|
||||
'default': 'ipmi',
|
||||
}),
|
||||
'net_interfaces': PluginCollection({
|
||||
'pluginattrs': ['hardwaremanagement.method'],
|
||||
'default': 'ipmi',
|
||||
@@ -195,12 +214,48 @@ def _init_core():
|
||||
'default': 'ipmi',
|
||||
}),
|
||||
},
|
||||
'extended': {
|
||||
'all': PluginRoute({
|
||||
'pluginattrs': ['hardwaremanagement.method'],
|
||||
'default': 'ipmi',
|
||||
}),
|
||||
'advanced': PluginRoute({
|
||||
'pluginattrs': ['hardwaremanagement.method'],
|
||||
'default': 'ipmi',
|
||||
}),
|
||||
},
|
||||
},
|
||||
'storage': {
|
||||
'all': PluginRoute({
|
||||
'pluginattrs': ['hardwaremanagement.method'],
|
||||
'default': 'ipmi',
|
||||
}),
|
||||
'arrays': PluginCollection({
|
||||
'pluginattrs': ['hardwaremanagement.method'],
|
||||
'default': 'ipmi',
|
||||
}),
|
||||
'disks': PluginCollection({
|
||||
'pluginattrs': ['hardwaremanagement.method'],
|
||||
'default': 'ipmi',
|
||||
}),
|
||||
'volumes': PluginCollection({
|
||||
'pluginattrs': ['hardwaremanagement.method'],
|
||||
'default': 'ipmi',
|
||||
})
|
||||
},
|
||||
'system': {
|
||||
'all': PluginRoute({
|
||||
'pluginattrs': ['hardwaremanagement.method'],
|
||||
'default': 'ipmi',
|
||||
}),
|
||||
'advanced': PluginRoute({
|
||||
'pluginattrs': ['hardwaremanagement.method'],
|
||||
'default': 'ipmi',
|
||||
}),
|
||||
'clear': PluginRoute({
|
||||
'pluginattrs': ['hardwaremanagement.method'],
|
||||
'default': 'ipmi',
|
||||
})
|
||||
},
|
||||
},
|
||||
'_console': {
|
||||
@@ -233,6 +288,10 @@ def _init_core():
|
||||
'default': 'ipmi',
|
||||
}),
|
||||
},
|
||||
'description': PluginRoute({
|
||||
'pluginattrs': ['hardwaremanagement.method'],
|
||||
'default': 'ipmi',
|
||||
}),
|
||||
'events': {
|
||||
'hardware': {
|
||||
'log': PluginRoute({
|
||||
@@ -334,10 +393,18 @@ def _init_core():
|
||||
},
|
||||
|
||||
},
|
||||
'support': {
|
||||
'servicedata': PluginCollection({
|
||||
'pluginattrs': ['hardwaremanagement.method'],
|
||||
'default': 'ipmi',
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
nodegroupresources = {
|
||||
'attributes': {
|
||||
'check': PluginRoute({'handler': 'attributes'}),
|
||||
'rename': PluginRoute({'handler': 'attributes'}),
|
||||
'all': PluginRoute({'handler': 'attributes'}),
|
||||
'current': PluginRoute({'handler': 'attributes'}),
|
||||
},
|
||||
@@ -353,13 +420,33 @@ def create_user(inputdata, configmanager):
|
||||
configmanager.create_user(username, attributemap=inputdata)
|
||||
|
||||
|
||||
def create_usergroup(inputdata, configmanager):
|
||||
try:
|
||||
groupname = inputdata['name']
|
||||
del inputdata['name']
|
||||
except (KeyError, ValueError):
|
||||
raise exc.InvalidArgumentException()
|
||||
configmanager.create_usergroup(groupname)
|
||||
|
||||
|
||||
def update_usergroup(groupname, attribmap, configmanager):
|
||||
try:
|
||||
configmanager.set_usergroup(groupname, attribmap)
|
||||
except ValueError as e:
|
||||
raise exc.InvalidArgumentException(str(e))
|
||||
|
||||
def update_user(name, attribmap, configmanager):
|
||||
try:
|
||||
configmanager.set_user(name, attribmap)
|
||||
except ValueError:
|
||||
raise exc.InvalidArgumentException()
|
||||
except ValueError as e:
|
||||
raise exc.InvalidArgumentException(str(e))
|
||||
|
||||
|
||||
def show_usergroup(groupname, configmanager):
|
||||
groupinfo = configmanager.get_usergroup(groupname)
|
||||
for attr in groupinfo:
|
||||
yield msg.Attributes(kv={attr: groupinfo[attr]})
|
||||
|
||||
def show_user(name, configmanager):
|
||||
userobj = configmanager.get_user(name)
|
||||
rv = {}
|
||||
@@ -376,6 +463,10 @@ def show_user(name, configmanager):
|
||||
rv[attr] = userobj[attr]
|
||||
yield msg.Attributes(kv={attr: rv[attr]},
|
||||
desc=attrscheme.user[attr]['description'])
|
||||
if 'role' in userobj:
|
||||
yield msg.Attributes(kv={'role': userobj['role']})
|
||||
|
||||
|
||||
|
||||
|
||||
def stripnode(iterablersp, node):
|
||||
@@ -408,6 +499,10 @@ def delete_user(user, configmanager):
|
||||
configmanager.del_user(user)
|
||||
yield msg.DeletedResource(user)
|
||||
|
||||
def delete_usergroup(usergroup, configmanager):
|
||||
configmanager.del_usergroup(usergroup)
|
||||
yield msg.DeletedResource(usergroup)
|
||||
|
||||
|
||||
def delete_nodegroup_collection(collectionpath, configmanager):
|
||||
if len(collectionpath) == 2: # just the nodegroup
|
||||
@@ -567,6 +662,15 @@ class BadPlugin(object):
|
||||
self.node, self.plugin + ' is not a supported plugin')
|
||||
|
||||
|
||||
class BadCollective(object):
|
||||
def __init__(self, node):
|
||||
self.node = node
|
||||
|
||||
def error(self, *args, **kwargs):
|
||||
yield msg.ConfluentNodeError(
|
||||
self.node, 'collective mode is active, but collective.manager '
|
||||
'is not set for this node')
|
||||
|
||||
def abbreviate_noderange(configmanager, inputdata, operation):
|
||||
if operation != 'create':
|
||||
raise exc.InvalidArgumentException('Must be a create with nodes in list')
|
||||
@@ -602,7 +706,7 @@ def handle_dispatch(connection, cert, dispatch, peername):
|
||||
plugpath = nodeattr[node][attrname]['value']
|
||||
elif 'default' in plugroute:
|
||||
plugpath = plugroute['default']
|
||||
if plugpath is not None:
|
||||
if plugpath:
|
||||
try:
|
||||
hfunc = getattr(pluginmap[plugpath], operation)
|
||||
except KeyError:
|
||||
@@ -626,7 +730,11 @@ def handle_dispatch(connection, cert, dispatch, peername):
|
||||
|
||||
|
||||
def _forward_rsp(connection, res):
|
||||
r = pickle.dumps(res)
|
||||
try:
|
||||
r = pickle.dumps(res)
|
||||
except TypeError:
|
||||
r = pickle.dumps(Exception(
|
||||
'Cannot serialize error, check collective.manager error logs for details' + str(res)))
|
||||
rlen = len(r)
|
||||
if not rlen:
|
||||
return
|
||||
@@ -636,6 +744,8 @@ def _forward_rsp(connection, res):
|
||||
|
||||
def handle_node_request(configmanager, inputdata, operation,
|
||||
pathcomponents, autostrip=True):
|
||||
if log.logfull:
|
||||
raise exc.TargetResourceUnavailable('Filesystem full, free up space and restart confluent service')
|
||||
iscollection = False
|
||||
routespec = None
|
||||
if pathcomponents[0] == 'noderange':
|
||||
@@ -759,7 +869,8 @@ def handle_node_request(configmanager, inputdata, operation,
|
||||
continue
|
||||
elif list(cfm.list_collective()):
|
||||
badcollnodes.append(node)
|
||||
if plugpath is not None:
|
||||
continue
|
||||
if plugpath:
|
||||
try:
|
||||
hfunc = getattr(pluginmap[plugpath], operation)
|
||||
except KeyError:
|
||||
@@ -769,11 +880,8 @@ def handle_node_request(configmanager, inputdata, operation,
|
||||
nodesbyhandler[hfunc].append(node)
|
||||
else:
|
||||
nodesbyhandler[hfunc] = [node]
|
||||
if badcollnodes:
|
||||
raise exc.ConfluentException(
|
||||
'collective management active, '
|
||||
'collective.manager must be set for {0}'.format(
|
||||
','.join(badcollnodes)))
|
||||
for bn in badcollnodes:
|
||||
nodesbyhandler[BadCollective(bn).error] = [bn]
|
||||
workers = greenpool.GreenPool()
|
||||
numworkers = 0
|
||||
for hfunc in nodesbyhandler:
|
||||
@@ -809,6 +917,8 @@ def iterate_queue(numworkers, passvalues, strip=False):
|
||||
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
|
||||
@@ -822,6 +932,8 @@ def addtoqueue(theq, fun, kwargs):
|
||||
else:
|
||||
for pv in result:
|
||||
theq.put(pv)
|
||||
except Exception as e:
|
||||
theq.put(e)
|
||||
finally:
|
||||
theq.put('theend')
|
||||
|
||||
@@ -831,14 +943,22 @@ def dispatch_request(nodes, manager, element, configmanager, inputdata,
|
||||
a = configmanager.get_collective_member(manager)
|
||||
try:
|
||||
remote = socket.create_connection((a['address'], 13001))
|
||||
remote.settimeout(90)
|
||||
remote.settimeout(180)
|
||||
remote = ssl.wrap_socket(remote, cert_reqs=ssl.CERT_NONE,
|
||||
keyfile='/etc/confluent/privkey.pem',
|
||||
certfile='/etc/confluent/srvcert.pem')
|
||||
except Exception:
|
||||
for node in nodes:
|
||||
yield msg.ConfluentResourceUnavailable(
|
||||
node, 'Collective member {0} is unreachable'.format(a['name']))
|
||||
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)):
|
||||
@@ -941,6 +1061,31 @@ def handle_path(path, operation, configmanager, inputdata=None, autostrip=True):
|
||||
configmanager, inputdata, operation, pathcomponents)
|
||||
elif pathcomponents[0] == 'version':
|
||||
return (msg.Attributes(kv={'version': confluent.__version__}),)
|
||||
elif pathcomponents[0] == 'usergroups':
|
||||
# TODO: when non-administrator accounts exist,
|
||||
# they must only be allowed to see their own user
|
||||
try:
|
||||
usergroup = pathcomponents[1]
|
||||
except IndexError: # it's just users/
|
||||
if operation == 'create':
|
||||
inputdata = msg.get_input_message(
|
||||
pathcomponents, operation, inputdata,
|
||||
configmanager=configmanager)
|
||||
create_usergroup(inputdata.attribs, configmanager)
|
||||
return iterate_collections(configmanager.list_usergroups(),
|
||||
forcecollection=False)
|
||||
if usergroup not in configmanager.list_usergroups():
|
||||
raise exc.NotFoundException("Invalid usergroup %s" % usergroup)
|
||||
if operation == 'retrieve':
|
||||
return show_usergroup(usergroup, configmanager)
|
||||
elif operation == 'delete':
|
||||
return delete_usergroup(usergroup, configmanager)
|
||||
elif operation == 'update':
|
||||
inputdata = msg.get_input_message(
|
||||
pathcomponents, operation, inputdata,
|
||||
configmanager=configmanager)
|
||||
update_usergroup(usergroup, inputdata.attribs, configmanager)
|
||||
return show_usergroup(usergroup, configmanager)
|
||||
elif pathcomponents[0] == 'users':
|
||||
# TODO: when non-administrator accounts exist,
|
||||
# they must only be allowed to see their own user
|
||||
|
||||
@@ -63,10 +63,13 @@
|
||||
|
||||
import base64
|
||||
import confluent.config.configmanager as cfm
|
||||
import confluent.collective.manager as collective
|
||||
import confluent.discovery.protocols.pxe as pxe
|
||||
#import confluent.discovery.protocols.ssdp as ssdp
|
||||
import confluent.discovery.protocols.slp as slp
|
||||
import confluent.discovery.handlers.imm as imm
|
||||
import confluent.discovery.handlers.cpstorage as cpstorage
|
||||
import confluent.discovery.handlers.tsm as tsm
|
||||
import confluent.discovery.handlers.pxe as pxeh
|
||||
import confluent.discovery.handlers.smm as smm
|
||||
import confluent.discovery.handlers.xcc as xcc
|
||||
@@ -86,6 +89,8 @@ import eventlet.greenpool
|
||||
import eventlet.semaphore
|
||||
|
||||
autosensors = set()
|
||||
scanner = None
|
||||
|
||||
class nesteddict(dict):
|
||||
|
||||
def __missing__(self, key):
|
||||
@@ -98,6 +103,8 @@ nodehandlers = {
|
||||
'service:management-hardware.IBM:integrated-management-module2': imm,
|
||||
'pxe-client': pxeh,
|
||||
'service:io-device.Lenovo:management-module': None,
|
||||
'service:thinkagile-storage': cpstorage,
|
||||
'service:lenovo-tsm': tsm,
|
||||
}
|
||||
|
||||
servicenames = {
|
||||
@@ -106,6 +113,8 @@ servicenames = {
|
||||
'service:management-hardware.Lenovo:lenovo-xclarity-controller': 'lenovo-xcc',
|
||||
'service:management-hardware.IBM:integrated-management-module2': 'lenovo-imm2',
|
||||
'service:io-device.Lenovo:management-module': 'lenovo-switch',
|
||||
'service:thinkagile-storage': 'thinkagile-storagebmc',
|
||||
'service:lenovo-tsm': 'lenovo-tsm',
|
||||
}
|
||||
|
||||
servicebyname = {
|
||||
@@ -114,6 +123,8 @@ servicebyname = {
|
||||
'lenovo-xcc': 'service:management-hardware.Lenovo:lenovo-xclarity-controller',
|
||||
'lenovo-imm2': 'service:management-hardware.IBM:integrated-management-module2',
|
||||
'lenovo-switch': 'service:io-device.Lenovo:management-module',
|
||||
'thinkagile-storage': 'service:thinkagile-storagebmc',
|
||||
'lenovo-tsm': 'service:lenovo-tsm',
|
||||
}
|
||||
|
||||
discopool = eventlet.greenpool.GreenPool(500)
|
||||
@@ -209,6 +220,8 @@ def send_discovery_datum(info):
|
||||
if infotype in servicenames:
|
||||
types.append(servicenames[infotype])
|
||||
yield msg.KeyValueData({'types': types})
|
||||
if 'otheraddresses' in info:
|
||||
yield msg.KeyValueData({'otheripaddrs': list(info['otheraddresses'])})
|
||||
|
||||
|
||||
def _info_matches(info, criteria):
|
||||
@@ -381,6 +394,7 @@ def handle_api_request(configmanager, inputdata, operation, pathcomponents):
|
||||
raise exc.InvalidArgumentException()
|
||||
rescan()
|
||||
return (msg.KeyValueData({'rescan': 'started'}),)
|
||||
|
||||
elif operation in ('update', 'create'):
|
||||
if 'node' not in inputdata:
|
||||
raise exc.InvalidArgumentException('Missing node name in input')
|
||||
@@ -391,8 +405,15 @@ def handle_api_request(configmanager, inputdata, operation, pathcomponents):
|
||||
'Unable to {0} to {1}'.format(operation,
|
||||
'/'.join(pathcomponents)))
|
||||
handler = info['handler'].NodeHandler(info, configmanager)
|
||||
eval_node(configmanager, handler, info, inputdata['node'],
|
||||
manual=True)
|
||||
try:
|
||||
eval_node(configmanager, handler, info, inputdata['node'],
|
||||
manual=True)
|
||||
except Exception as e:
|
||||
# or... incorrect passworod provided..
|
||||
if 'Incorrect password' in str(e) or 'Unauthorized name' in str(e):
|
||||
return [msg.ConfluentTargetInvalidCredentials(
|
||||
inputdata['node'])]
|
||||
raise
|
||||
return [msg.AssignedResource(inputdata['node'])]
|
||||
elif operation == 'delete':
|
||||
mac = _get_mac_from_query(pathcomponents)
|
||||
@@ -416,6 +437,8 @@ def handle_read_api_request(pathcomponents):
|
||||
# TODO(jjohnson2): This should be more generalized...
|
||||
# odd indexes into components are 'by-'*, even indexes
|
||||
# starting at 2 are parameters to previous index
|
||||
if pathcomponents == ['discovery', 'rescan']:
|
||||
return (msg.KeyValueData({'scanning': bool(scanner)}),)
|
||||
subcats, queryparms, indexof, coll = _parameterize_path(pathcomponents[1:])
|
||||
if len(pathcomponents) == 1:
|
||||
dirlist = [msg.ChildCollection(x + '/') for x in sorted(list(subcats))]
|
||||
@@ -458,6 +481,9 @@ def _recheck_nodes_backend(nodeattribs, configmanager):
|
||||
_map_unique_ids(nodeattribs)
|
||||
# for the nodes whose attributes have changed, consider them as potential
|
||||
# strangers
|
||||
if nodeattribs:
|
||||
macmap.vintage = 0 # expire current mac map data, in case
|
||||
# the attributes changed impacted the result
|
||||
for node in nodeattribs:
|
||||
if node in known_nodes:
|
||||
for somemac in known_nodes[node]:
|
||||
@@ -571,6 +597,11 @@ def detected(info):
|
||||
break
|
||||
else: # no nodehandler, ignore for now
|
||||
return
|
||||
if (handler and not handler.NodeHandler.adequate(info) and
|
||||
info.get('protocol', None)):
|
||||
eventlet.spawn_after(10, info['protocol'].fix_info, info,
|
||||
safe_detected)
|
||||
return
|
||||
try:
|
||||
snum = info['attributes']['enclosure-serial-number'][0].strip()
|
||||
if snum:
|
||||
@@ -617,6 +648,9 @@ def detected(info):
|
||||
uuid = info.get('uuid', None)
|
||||
if uuid_is_valid(uuid):
|
||||
known_uuids[uuid][info['hwaddr']] = info
|
||||
info['otheraddresses'] = set([])
|
||||
for i4addr in info.get('attributes', {}).get('ipv4-address', []):
|
||||
info['otheraddresses'].add(i4addr)
|
||||
if handler and handler.https_supported and not handler.https_cert:
|
||||
if handler.cert_fail_reason == 'unreachable':
|
||||
log.log(
|
||||
@@ -686,6 +720,10 @@ def get_enclosure_chain_head(nodename, cfg):
|
||||
if ne in members:
|
||||
raise exc.InvalidArgumentException(
|
||||
'Circular chain that includes ' + nodename)
|
||||
if not cfg.is_node(ne):
|
||||
raise exc.InvalidArgumentException(
|
||||
'{0} is chained to nonexistent node {1} '.format(
|
||||
nodename, ne))
|
||||
nodename = ne
|
||||
members.append(nodename)
|
||||
return nodename
|
||||
@@ -851,7 +889,7 @@ def eval_node(cfg, handler, info, nodename, manual=False):
|
||||
handler.probe() # unicast interrogation as possible to get more data
|
||||
# switch concurrently
|
||||
# do some preconfig, for example, to bring a SMM online if applicable
|
||||
handler.preconfig()
|
||||
handler.preconfig(nodename)
|
||||
except Exception as e:
|
||||
unknown_info[info['hwaddr']] = info
|
||||
info['discostatus'] = 'unidentified'
|
||||
@@ -1026,6 +1064,14 @@ def discover_node(cfg, handler, info, nodename, manual):
|
||||
traceback.print_exc()
|
||||
return False
|
||||
newnodeattribs = {}
|
||||
if cfm.list_collective():
|
||||
# We are in a collective, check collective.manager
|
||||
cmc = cfg.get_node_attributes(nodename, 'collective.manager')
|
||||
cm = cmc.get(nodename, {}).get('collective.manager', {}).get('value', None)
|
||||
if not cm:
|
||||
# Node is being discovered in collective, but no collective.manager, default
|
||||
# to the collective member actually able to execute the discovery
|
||||
newnodeattribs['collective.manager'] = collective.get_myname()
|
||||
if 'uuid' in info:
|
||||
newnodeattribs['id.uuid'] = info['uuid']
|
||||
if 'serialnumber' in info:
|
||||
@@ -1091,6 +1137,7 @@ needaddhandled = False
|
||||
def _handle_nodelist_change(configmanager):
|
||||
global needaddhandled
|
||||
global nodeaddhandler
|
||||
macmap.vintage = 0 # the current mac map is probably inaccurate
|
||||
_recheck_nodes((), configmanager)
|
||||
if needaddhandled:
|
||||
needaddhandled = False
|
||||
@@ -1099,10 +1146,18 @@ def _handle_nodelist_change(configmanager):
|
||||
nodeaddhandler = None
|
||||
|
||||
|
||||
def newnodes(added, deleting, configmanager):
|
||||
def newnodes(added, deleting, renamed, configmanager):
|
||||
global attribwatcher
|
||||
global needaddhandled
|
||||
global nodeaddhandler
|
||||
alldeleting = set(deleting) | set(renamed)
|
||||
for node in alldeleting:
|
||||
if node not in known_nodes:
|
||||
continue
|
||||
for mac in known_nodes[node]:
|
||||
if mac in known_info:
|
||||
del known_info[mac]
|
||||
del known_nodes[node]
|
||||
_map_unique_ids()
|
||||
configmanager.remove_watcher(attribwatcher)
|
||||
allnodes = configmanager.list_nodes()
|
||||
@@ -1142,7 +1197,11 @@ def _periodic_recheck(configmanager):
|
||||
|
||||
def rescan():
|
||||
_map_unique_ids()
|
||||
eventlet.spawn_n(slp.active_scan, safe_detected)
|
||||
global scanner
|
||||
if scanner:
|
||||
return
|
||||
else:
|
||||
scanner = eventlet.spawn(slp.active_scan, safe_detected, slp)
|
||||
|
||||
|
||||
def start_detection():
|
||||
@@ -1171,8 +1230,8 @@ def stop_autosense():
|
||||
autosensors.discard(watcher)
|
||||
|
||||
def start_autosense():
|
||||
autosensors.add(eventlet.spawn(slp.snoop, safe_detected))
|
||||
autosensors.add(eventlet.spawn(pxe.snoop, safe_detected))
|
||||
autosensors.add(eventlet.spawn(slp.snoop, safe_detected, slp))
|
||||
autosensors.add(eventlet.spawn(pxe.snoop, safe_detected, pxe))
|
||||
|
||||
|
||||
nodes_by_fprint = {}
|
||||
@@ -1194,7 +1253,7 @@ def _map_unique_ids(nodes=None):
|
||||
uuid_by_nodes = {}
|
||||
fprint_by_nodes = {}
|
||||
for uuid in nodes_by_uuid:
|
||||
if not uuid_is_valid():
|
||||
if not uuid_is_valid(uuid):
|
||||
continue
|
||||
node = nodes_by_uuid[uuid]
|
||||
if node in bigmap:
|
||||
|
||||
@@ -26,16 +26,27 @@ ipmicommand.session.threading = eventlet.green.threading
|
||||
ipmicommand.session.socket.getaddrinfo = eventlet.support.greendns.getaddrinfo
|
||||
getaddrinfo = eventlet.support.greendns.getaddrinfo
|
||||
|
||||
DEFAULT_USER = 'USERID'
|
||||
DEFAULT_PASS = 'PASSW0RD'
|
||||
|
||||
|
||||
class NodeHandler(generic.NodeHandler):
|
||||
DEFAULT_USER = 'USERID'
|
||||
DEFAULT_PASS = 'PASSW0RD'
|
||||
|
||||
def _get_ipmicmd(self, user=DEFAULT_USER, password=DEFAULT_PASS):
|
||||
return ipmicommand.Command(self.ipaddr, user, password)
|
||||
def _get_ipmicmd(self, user=None, password=None):
|
||||
priv = None
|
||||
if user is None or password is None:
|
||||
if self.trieddefault:
|
||||
raise pygexc.IpmiException()
|
||||
priv = 4 # manually indicate priv to avoid double-attempt
|
||||
if user is None:
|
||||
user = self.DEFAULT_USER
|
||||
if password is None:
|
||||
password = self.DEFAULT_PASS
|
||||
return ipmicommand.Command(self.ipaddr, user, password,
|
||||
privlevel=priv, keepalive=False)
|
||||
|
||||
def __init__(self, info, configmanager):
|
||||
self.trieddefault = None
|
||||
super(NodeHandler, self).__init__(info, configmanager)
|
||||
|
||||
def probe(self):
|
||||
@@ -45,35 +56,37 @@ class NodeHandler(generic.NodeHandler):
|
||||
def config(self, nodename, reset=False):
|
||||
self._bmcconfig(nodename, reset)
|
||||
|
||||
def _bmcconfig(self, nodename, reset=False):
|
||||
def _bmcconfig(self, nodename, reset=False, customconfig=None, vc=None):
|
||||
# TODO(jjohnson2): set ip parameters, user/pass, alert cfg maybe
|
||||
# In general, try to use https automation, to make it consistent
|
||||
# between hypothetical secure path and today.
|
||||
creds = self.configmanager.get_node_attributes(
|
||||
nodename,
|
||||
['secret.hardwaremanagementuser',
|
||||
'secret.hardwaremanagementpassword'], decrypt=True)
|
||||
user = creds.get(nodename, {}).get(
|
||||
'secret.hardwaremanagementuser', {}).get('value', None)
|
||||
passwd = creds.get(nodename, {}).get(
|
||||
'secret.hardwaremanagementpassword', {}).get('value', None)
|
||||
try:
|
||||
ic = self._get_ipmicmd()
|
||||
passwd = DEFAULT_PASS
|
||||
passwd = self.DEFAULT_PASS
|
||||
except pygexc.IpmiException as pi:
|
||||
creds = self.configmanager.get_node_attributes(
|
||||
nodename,
|
||||
['secret.hardwaremanagementuser',
|
||||
'secret.hardwaremanagementpassword'], decrypt=True)
|
||||
user = creds.get(nodename, {}).get(
|
||||
'secret.hardwaremanagementuser', {}).get('value', None)
|
||||
havecustomcreds = False
|
||||
if user is not None and user != DEFAULT_USER:
|
||||
if user is not None and user != self.DEFAULT_USER:
|
||||
havecustomcreds = True
|
||||
else:
|
||||
user = DEFAULT_USER
|
||||
passwd = creds.get(nodename, {}).get(
|
||||
'secret.hardwaremanagementpassword', {}).get('value', None)
|
||||
if passwd is not None and passwd != DEFAULT_PASS:
|
||||
user = self.DEFAULT_USER
|
||||
if passwd is not None and passwd != self.DEFAULT_PASS:
|
||||
havecustomcreds = True
|
||||
else:
|
||||
passwd = DEFAULT_PASS
|
||||
passwd = self.DEFAULT_PASS
|
||||
if havecustomcreds:
|
||||
ic = self._get_ipmicmd(user, passwd)
|
||||
else:
|
||||
raise
|
||||
if vc:
|
||||
ic.register_key_handler(vc)
|
||||
currusers = ic.get_users()
|
||||
lanchan = ic.get_network_channel()
|
||||
userdata = ic.xraw_command(netfn=6, command=0x44, data=(lanchan,
|
||||
@@ -93,45 +106,33 @@ class NodeHandler(generic.NodeHandler):
|
||||
raise exc.TargetEndpointBadCredentials(
|
||||
'secret.hardwaremanagementuser and/or '
|
||||
'secret.hardwaremanagementpassword was not configured')
|
||||
if ('hardwaremanagement.manager' in cd and
|
||||
cd['hardwaremanagement.manager']['value'] and
|
||||
not cd['hardwaremanagement.manager']['value'].startswith(
|
||||
'fe80::')):
|
||||
newip = cd['hardwaremanagement.manager']['value']
|
||||
newipinfo = getaddrinfo(newip, 0)[0]
|
||||
# This getaddrinfo is repeated in get_nic_config, could be
|
||||
# optimized, albeit with a more convoluted api..
|
||||
newip = newipinfo[-1][0]
|
||||
if ':' in newip:
|
||||
raise exc.NotImplementedException('IPv6 remote config TODO')
|
||||
netconfig = netutil.get_nic_config(cfg, nodename, ip=newip)
|
||||
plen = netconfig['prefix']
|
||||
newip = '{0}/{1}'.format(newip, plen)
|
||||
ic.set_net_configuration(ipv4_address=newip,
|
||||
ipv4_configuration='static',
|
||||
ipv4_gateway=netconfig['ipv4_gateway'])
|
||||
elif self.ipaddr.startswith('fe80::'):
|
||||
cfg.set_node_attributes(
|
||||
{nodename: {'hardwaremanagement.manager': self.ipaddr}})
|
||||
else:
|
||||
raise exc.TargetEndpointUnreachable(
|
||||
'hardwaremanagement.manager must be set to desired address')
|
||||
newuser = cd['secret.hardwaremanagementuser']['value']
|
||||
newpass = cd['secret.hardwaremanagementpassword']['value']
|
||||
for uid in currusers:
|
||||
if currusers[uid]['name'] == newuser:
|
||||
# Use existing account that has been created
|
||||
newuserslot = uid
|
||||
if newpass != passwd: # don't mess with existing if no change
|
||||
ic.set_user_password(newuserslot, password=newpass)
|
||||
ic = self._get_ipmicmd(user, passwd)
|
||||
if vc:
|
||||
ic.register_key_handler(vc)
|
||||
break
|
||||
else:
|
||||
newuserslot = lockedusers + 1
|
||||
if newuserslot < 2:
|
||||
newuserslot = 2
|
||||
if newpass != passwd: # don't mess with existing if no change
|
||||
ic.set_user_password(newuserslot, password=newpass)
|
||||
ic.set_user_name(newuserslot, newuser)
|
||||
ic.set_user_access(newuserslot, lanchan,
|
||||
privilege_level='administrator')
|
||||
if newpass != passwd: # don't mess with existing if no change
|
||||
ic.set_user_password(newuserslot, password=newpass)
|
||||
if havecustomcreds:
|
||||
ic = self._get_ipmicmd(user, passwd)
|
||||
if vc:
|
||||
ic.register_key_handler(vc)
|
||||
#We are remote operating on the account we are
|
||||
#using, no need to try to set user access
|
||||
#ic.set_user_access(newuserslot, lanchan,
|
||||
# privilege_level='administrator')
|
||||
# Now to zap others
|
||||
for uid in currusers:
|
||||
if uid != newuserslot:
|
||||
@@ -152,6 +153,36 @@ class NodeHandler(generic.NodeHandler):
|
||||
# name...
|
||||
# the user will remain, but that is life
|
||||
raise
|
||||
if customconfig:
|
||||
customconfig(ic)
|
||||
if ('hardwaremanagement.manager' in cd and
|
||||
cd['hardwaremanagement.manager']['value'] and
|
||||
not cd['hardwaremanagement.manager']['value'].startswith(
|
||||
'fe80::')):
|
||||
newip = cd['hardwaremanagement.manager']['value']
|
||||
newipinfo = getaddrinfo(newip, 0)[0]
|
||||
# This getaddrinfo is repeated in get_nic_config, could be
|
||||
# optimized, albeit with a more convoluted api..
|
||||
newip = newipinfo[-1][0]
|
||||
if ':' in newip:
|
||||
raise exc.NotImplementedException('IPv6 remote config TODO')
|
||||
netconfig = netutil.get_nic_config(cfg, nodename, ip=newip)
|
||||
plen = netconfig['prefix']
|
||||
newip = '{0}/{1}'.format(newip, plen)
|
||||
currcfg = ic.get_net_configuration()
|
||||
if currcfg['ipv4_address'] != newip:
|
||||
# do not change the ipv4_config if the current config looks
|
||||
# like it is already accurate
|
||||
ic.set_net_configuration(ipv4_address=newip,
|
||||
ipv4_configuration='static',
|
||||
ipv4_gateway=netconfig[
|
||||
'ipv4_gateway'])
|
||||
elif self.ipaddr.startswith('fe80::'):
|
||||
cfg.set_node_attributes(
|
||||
{nodename: {'hardwaremanagement.manager': self.ipaddr}})
|
||||
else:
|
||||
raise exc.TargetEndpointUnreachable(
|
||||
'hardwaremanagement.manager must be set to desired address')
|
||||
if reset:
|
||||
ic.reset_bmc()
|
||||
return ic
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
# Copyright 2019 Lenovo
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import confluent.discovery.handlers.bmc as bmchandler
|
||||
|
||||
class NodeHandler(bmchandler.NodeHandler):
|
||||
DEFAULT_USER = 'admin'
|
||||
DEFAULT_PASS = 'admin'
|
||||
devname = 'BMC'
|
||||
maxmacs = 2
|
||||
@@ -40,6 +40,12 @@ class NodeHandler(object):
|
||||
targsa = info['addresses'][0]
|
||||
self.ipaddr = targsa[0]
|
||||
|
||||
@classmethod
|
||||
def adequate(cls, info):
|
||||
# Check if the referenced info is really enough, if false, a rescan
|
||||
# may occur against the target in a short while
|
||||
return True
|
||||
|
||||
def scan(self):
|
||||
# Do completely passive things to enhance data.
|
||||
# Probe is permitted to for example attempt a login
|
||||
@@ -51,7 +57,7 @@ class NodeHandler(object):
|
||||
# serial number and uuid to flesh out data as needed
|
||||
return
|
||||
|
||||
def preconfig(self):
|
||||
def preconfig(self, possiblenode):
|
||||
return
|
||||
|
||||
def discoverable_by_switch(self, macs):
|
||||
@@ -62,6 +68,23 @@ class NodeHandler(object):
|
||||
def _savecert(self, certificate):
|
||||
self._fp = certificate
|
||||
return True
|
||||
|
||||
def get_node_credentials(self, nodename, creds, defuser, defpass):
|
||||
user = creds.get(nodename, {}).get(
|
||||
'secret.hardwaremanagementuser', {}).get('value', None)
|
||||
havecustomcreds = False
|
||||
if user is not None and user != defuser:
|
||||
havecustomcreds = True
|
||||
else:
|
||||
user = defuser
|
||||
passwd = creds.get(nodename, {}).get(
|
||||
'secret.hardwaremanagementpassword', {}).get('value', None)
|
||||
if passwd is not None and passwd != defpass:
|
||||
havecustomcreds = True
|
||||
else:
|
||||
passwd = defpass
|
||||
return user, passwd, not havecustomcreds
|
||||
|
||||
|
||||
@property
|
||||
def cert_fail_reason(self):
|
||||
|
||||
@@ -15,12 +15,17 @@
|
||||
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'
|
||||
|
||||
@classmethod
|
||||
def adequate(cls, info):
|
||||
# We can sometimes receive a partially initialized SLP packet
|
||||
# This is not adequate for being satisfied
|
||||
return bool(info.get('attributes', {}))
|
||||
|
||||
def scan(self):
|
||||
slpattrs = self.info.get('attributes', {})
|
||||
self.isdense = False
|
||||
@@ -38,7 +43,7 @@ class NodeHandler(bmchandler.NodeHandler):
|
||||
uuidprefix[12:16]
|
||||
self.info['uuid'] = uuidprefix + '-' + '-'.join(
|
||||
wronguuid.split('-')[3:])
|
||||
self.info['uuid'] = string.lower(self.info['uuid'])
|
||||
self.info['uuid'] = self.info['uuid'].lower()
|
||||
if ff not in ('dense-computing', 'BC2'):
|
||||
# do not probe unless it's a dense platform
|
||||
return
|
||||
|
||||
@@ -13,7 +13,16 @@
|
||||
# limitations under the License.
|
||||
|
||||
import confluent.discovery.handlers.bmc as bmchandler
|
||||
import confluent.exceptions as exc
|
||||
import eventlet
|
||||
webclient = eventlet.import_patched('pyghmi.util.webclient')
|
||||
import struct
|
||||
import urllib
|
||||
import eventlet.support.greendns
|
||||
import confluent.netutil as netutil
|
||||
getaddrinfo = eventlet.support.greendns.getaddrinfo
|
||||
|
||||
from xml.etree.ElementTree import fromstring
|
||||
|
||||
def fixuuid(baduuid):
|
||||
# SMM dumps it out in hex
|
||||
@@ -26,7 +35,7 @@ def fixuuid(baduuid):
|
||||
class NodeHandler(bmchandler.NodeHandler):
|
||||
is_enclosure = True
|
||||
devname = 'SMM'
|
||||
maxmacs = 5 # support an enclosure, but try to avoid catching daisy chain
|
||||
maxmacs = 6 # support an enclosure, but try to avoid catching daisy chain
|
||||
|
||||
def scan(self):
|
||||
# the UUID is in a weird order, fix it up to match
|
||||
@@ -36,9 +45,147 @@ class NodeHandler(bmchandler.NodeHandler):
|
||||
uuid = fixuuid(uuid[0])
|
||||
self.info['uuid'] = uuid
|
||||
|
||||
def _validate_cert(self, certificate):
|
||||
# Assumption is by the time we call config, that discovery core has
|
||||
# vetted self._fp. Our job here then is just to make sure that
|
||||
# the currect connection matches the previously saved cert
|
||||
if not self._fp: # circumstances are that we haven't validated yet
|
||||
self._fp = certificate
|
||||
return certificate == self._fp
|
||||
|
||||
def _webconfigrules(self, wc):
|
||||
rules = []
|
||||
for rule in self.ruleset.split(','):
|
||||
if '=' not in rule:
|
||||
continue
|
||||
name, value = rule.split('=')
|
||||
if value.lower() in ('no', 'none', 'disable', 'disabled'):
|
||||
value = '0'
|
||||
if name.lower() in ('expiry', 'expiration'):
|
||||
rules.append('passwordDurationDays:' + value)
|
||||
warndays = '5' if int(value) > 5 else value
|
||||
rules.append('passwordExpireWarningDays:' + warndays)
|
||||
if name.lower() in ('lockout', 'loginfailures'):
|
||||
rules.append('passwordFailAllowdNum:' + value)
|
||||
if name.lower() == 'reuse':
|
||||
rules.append('passwordReuseCheckNum:' + value)
|
||||
if rules:
|
||||
apirequest = 'set={0}'.format(','.join(rules))
|
||||
wc.request('POST', '/data', apirequest)
|
||||
wc.getresponse().read()
|
||||
|
||||
def _webconfignet(self, wc, nodename):
|
||||
cfg = self.configmanager
|
||||
cd = cfg.get_node_attributes(
|
||||
nodename, ['hardwaremanagement.manager'])
|
||||
smmip = cd.get(nodename, {}).get('hardwaremanagement.manager', {}).get('value', None)
|
||||
if smmip and ':' not in smmip:
|
||||
smmip = getaddrinfo(smmip, 0)[0]
|
||||
smmip = smmip[-1][0]
|
||||
if smmip and ':' in smmip:
|
||||
raise exc.NotImplementedException('IPv6 not supported')
|
||||
netconfig = netutil.get_nic_config(cfg, nodename, ip=smmip)
|
||||
netmask = netutil.cidr_to_mask(netconfig['prefix'])
|
||||
setdata = 'set=ifIndex:0,v4DHCPEnabled:0,v4IPAddr:{0},v4NetMask:{1}'.format(smmip, netmask)
|
||||
gateway = netconfig.get('ipv4_gateway', None)
|
||||
if gateway:
|
||||
setdata += ',v4Gateway:{0}'.format(gateway)
|
||||
wc.request('POST', '/data', setdata)
|
||||
rsp = wc.getresponse()
|
||||
rspdata = rsp.read()
|
||||
if '<statusCode>0' not in rspdata:
|
||||
raise Exception("Error configuring SMM Network")
|
||||
return
|
||||
if smmip and ':' in smmip and not smmip.startswith('fe80::'):
|
||||
raise exc.NotImplementedException('IPv6 configuration TODO')
|
||||
if self.ipaddr.startswith('fe80::'):
|
||||
cfg.set_node_attributes(
|
||||
{nodename: {'hardwaremanagement.manager': self.ipaddr}})
|
||||
|
||||
def _webconfigcreds(self, username, password):
|
||||
wc = webclient.SecureHTTPConnection(self.ipaddr, 443, verifycallback=self._validate_cert)
|
||||
wc.connect()
|
||||
authdata = { # start by trying factory defaults
|
||||
'user': 'USERID',
|
||||
'password': 'PASSW0RD',
|
||||
}
|
||||
headers = {'Connection': 'keep-alive', 'Content-Type': 'application/x-www-form-urlencoded'}
|
||||
wc.request('POST', '/data/login', urllib.urlencode(authdata), headers)
|
||||
rsp = wc.getresponse()
|
||||
rspdata = rsp.read()
|
||||
if 'authResult>0' not in rspdata:
|
||||
# default credentials are refused, try with the actual
|
||||
authdata['user'] = username
|
||||
authdata['password'] = password
|
||||
wc.request('POST', '/data/login', urllib.urlencode(authdata), headers)
|
||||
rsp = wc.getresponse()
|
||||
rspdata = rsp.read()
|
||||
if 'renew_account' in rspdata:
|
||||
raise Exception('Configured password has expired')
|
||||
if 'authResult>0' not in rspdata:
|
||||
raise Exception('Unknown username/password on SMM')
|
||||
tokens = fromstring(rspdata)
|
||||
st2 = tokens.findall('st2')[0].text
|
||||
wc.set_header('ST2', st2)
|
||||
return wc
|
||||
if 'renew_account' in rspdata:
|
||||
passwdchange = {'oripwd': 'PASSW0RD', 'newpwd': password}
|
||||
tokens = fromstring(rspdata)
|
||||
st2 = tokens.findall('st2')[0].text
|
||||
wc.set_header('ST2', st2)
|
||||
wc.request('POST', '/data/changepwd', urllib.urlencode(passwdchange))
|
||||
rsp = wc.getresponse()
|
||||
rspdata = rsp.read()
|
||||
authdata['password'] = password
|
||||
wc.request('POST', '/data/login', urllib.urlencode(authdata), headers)
|
||||
rsp = wc.getresponse()
|
||||
rspdata = rsp.read()
|
||||
if 'authResult>0' in rspdata:
|
||||
tokens = fromstring(rspdata)
|
||||
st2 = tokens.findall('st2')[0].text
|
||||
wc.set_header('ST2', st2)
|
||||
if username == 'USERID':
|
||||
return wc
|
||||
wc.request('POST', '/data', 'set=user(2,1,{0},511,,4,15,0)'.format(username))
|
||||
rsp = wc.getresponse()
|
||||
rspdata = rsp.read()
|
||||
wc.request('POST', '/data/logout')
|
||||
rsp = wc.getresponse()
|
||||
rspdata = rsp.read()
|
||||
authdata['user'] = username
|
||||
wc.request('POST', '/data/login', urllib.urlencode(authdata, headers))
|
||||
rsp = wc.getresponse()
|
||||
rspdata = rsp.read()
|
||||
tokens = fromstring(rspdata)
|
||||
st2 = tokens.findall('st2')[0].text
|
||||
wc.set_header('ST2', st2)
|
||||
return wc
|
||||
|
||||
def config(self, nodename):
|
||||
# SMM for now has to reset to assure configuration applies
|
||||
super(NodeHandler, self).config(nodename)
|
||||
dpp = self.configmanager.get_node_attributes(
|
||||
nodename, 'discovery.passwordrules')
|
||||
self.ruleset = dpp.get(nodename, {}).get(
|
||||
'discovery.passwordrules', {}).get('value', '')
|
||||
creds = self.configmanager.get_node_attributes(
|
||||
nodename,
|
||||
['secret.hardwaremanagementuser',
|
||||
'secret.hardwaremanagementpassword'], decrypt=True)
|
||||
username = creds.get(nodename, {}).get(
|
||||
'secret.hardwaremanagementuser', {}).get('value', 'USERID')
|
||||
passwd = creds.get(nodename, {}).get(
|
||||
'secret.hardwaremanagementpassword', {}).get('value', 'PASSW0RD')
|
||||
if passwd == 'PASSW0RD' and self.ruleset:
|
||||
raise Exception('Cannot support default password and setting password rules at same time')
|
||||
if passwd == 'PASSW0RD':
|
||||
# We must avoid hitting the web interface due to forced password change, best effert
|
||||
self._bmcconfig(nodename)
|
||||
else:
|
||||
# Switch to full web based configuration, to mitigate risks with the SMM
|
||||
wc = self._webconfigcreds(username, passwd)
|
||||
self._webconfigrules(wc)
|
||||
self._webconfignet(wc, nodename)
|
||||
|
||||
|
||||
# notes for smm:
|
||||
# POST to:
|
||||
@@ -53,4 +200,4 @@ class NodeHandler(bmchandler.NodeHandler):
|
||||
# with body user=USERID&password=Passw0rd!4321
|
||||
# yields:
|
||||
# <?xml version="1.0" encoding="UTF-8"?><root> <status>ok</status> <authResult>0</authResult> <forwardUrl>index.html</forwardUrl> </root>
|
||||
# note forwardUrl, if password change needed, will indicate something else
|
||||
# note forwardUrl, if password change needed, will indicate something else
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
# Copyright 2019 Lenovo
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import confluent.discovery.handlers.generic as generic
|
||||
import confluent.exceptions as exc
|
||||
import confluent.netutil as netutil
|
||||
import confluent.util as util
|
||||
import eventlet.support.greendns
|
||||
import json
|
||||
import urllib
|
||||
getaddrinfo = eventlet.support.greendns.getaddrinfo
|
||||
|
||||
webclient = eventlet.import_patched('pyghmi.util.webclient')
|
||||
|
||||
class NodeHandler(generic.NodeHandler):
|
||||
devname = 'TSM'
|
||||
DEFAULT_USER = 'USERID'
|
||||
DEFAULT_PASS = 'PASSW0RD'
|
||||
|
||||
def __init__(self, info, configmanager):
|
||||
self.trieddefault = None
|
||||
self.targuser = None
|
||||
self.curruser = None
|
||||
self.currpass = None
|
||||
self.targpass = None
|
||||
self.nodename = None
|
||||
self.csrftok = None
|
||||
self.channel = None
|
||||
self.atdefault = True
|
||||
super(NodeHandler, self).__init__(info, configmanager)
|
||||
|
||||
def validate_cert(self, certificate):
|
||||
# broadly speaking, merely checks consistency moment to moment,
|
||||
# but if https_cert gets stricter, this check means something
|
||||
fprint = util.get_fingerprint(self.https_cert)
|
||||
return util.cert_matches(fprint, certificate)
|
||||
|
||||
def _get_wc(self):
|
||||
authdata = { # start by trying factory defaults
|
||||
'username': self.DEFAULT_USER,
|
||||
'password': self.DEFAULT_PASS,
|
||||
}
|
||||
if not self.trieddefault:
|
||||
wc = webclient.SecureHTTPConnection(self.ipaddr, 443, verifycallback=self.validate_cert)
|
||||
rsp, status = wc.grab_json_response_with_status('/api/session', urllib.urlencode(authdata))
|
||||
if status > 400:
|
||||
self.trieddefault = True
|
||||
if '555' in rsp:
|
||||
passchange = {
|
||||
'Password': self.targpass,
|
||||
'RetypePassword': self.targpass,
|
||||
'param': 4,
|
||||
'default_password': self.DEFAULT_PASS,
|
||||
'username': self.DEFAULT_USER
|
||||
}
|
||||
rsp, status = wc.grab_json_response_with_status('/api/reset-pass', urllib.urlencode(passchange))
|
||||
authdata['password'] = self.targpass
|
||||
rsp, status = wc.grab_json_response_with_status('/api/session', urllib.urlencode(authdata))
|
||||
self.csrftok = rsp['CSRFToken']
|
||||
self.channel = rsp['channel']
|
||||
self.curruser = self.DEFAULT_USER
|
||||
self.currpass = self.targpass
|
||||
return wc
|
||||
else:
|
||||
self.curruser = self.DEFAULT_USER
|
||||
self.currpass = self.DEFAULT_PASS
|
||||
self.csrftok = rsp['CSRFToken']
|
||||
self.channel = rsp['channel']
|
||||
return wc
|
||||
if self.curruser:
|
||||
authdata['username'] = self.curruser
|
||||
authdata['password'] = self.currpass
|
||||
rsp, status = wc.grab_json_response_with_status('/api/session', urllib.urlencode(authdata))
|
||||
if rsp.status != 200:
|
||||
return None
|
||||
self.csrftok = rsp['CSRFToken']
|
||||
self.channel = rsp['channel']
|
||||
return wc
|
||||
authdata['username'] = self.targuser
|
||||
authdata['password'] = self.targpass
|
||||
rsp, status = wc.grab_json_response_with_status('/api/session', urllib.urlencode(authdata))
|
||||
if status != 200:
|
||||
return None
|
||||
self.curruser = self.targuser
|
||||
self.currpass = self.targpass
|
||||
self.csrftok = rsp['CSRFToken']
|
||||
self.channel = rsp['channel']
|
||||
return wc
|
||||
|
||||
def config(self, nodename):
|
||||
self.nodename = nodename
|
||||
creds = self.configmanager.get_node_attributes(
|
||||
nodename, ['secret.hardwaremanagementuser',
|
||||
'secret.hardwaremanagementpassword',
|
||||
'hardwaremanagement.manager', 'hardwaremanagement.method', 'console.method'],
|
||||
True)
|
||||
cd = creds.get(nodename, {})
|
||||
user, passwd, _ = self.get_node_credentials(
|
||||
nodename, creds, self.DEFAULT_USER, self.DEFAULT_PASS)
|
||||
self.targuser = user
|
||||
self.targpass = passwd
|
||||
wc = self._get_wc()
|
||||
wc.set_header('X-CSRFTOKEN', self.csrftok)
|
||||
curruserinfo = {}
|
||||
authupdate = False
|
||||
wc.set_header('Content-Type', 'application/json')
|
||||
if user != self.curruser:
|
||||
authupdate = True
|
||||
if not curruserinfo:
|
||||
curruserinfo = wc.grab_json_response('/api/settings/users')
|
||||
authchg = curruserinfo[1]
|
||||
authchg['name'] = user
|
||||
if passwd != self.currpass:
|
||||
authupdate = True
|
||||
if not curruserinfo:
|
||||
curruserinfo = wc.grab_json_response('/api/settings/users')
|
||||
authchg = curruserinfo[1]
|
||||
authchg['changepassword'] = 0
|
||||
authchg['password_size'] = 'bytes_20'
|
||||
authchg['password'] = passwd
|
||||
authchg['confirm_password'] = passwd
|
||||
if authupdate:
|
||||
rsp, status = wc.grab_json_response_with_status('/api/settings/users/2', authchg, method='PUT')
|
||||
if (cd.get('hardwaremanagement.method', {}).get('value', 'ipmi') != 'redfish'
|
||||
or cd.get('console.method', {}).get('value', None) == 'ipmi'):
|
||||
# IPMI must be enabled per user config
|
||||
wc.grab_json_response('/api/settings/ipmilanconfig', {
|
||||
'ipv4_enable': 1, 'ipv6_enable': 1,
|
||||
'uncheckedipv4lanEnable': 0, 'uncheckedipv6lanEnable': 0,
|
||||
'checkedipv4lanEnable': 1, 'checkedipv6lanEnable': 1})
|
||||
if ('hardwaremanagement.manager' in cd and
|
||||
cd['hardwaremanagement.manager']['value'] and
|
||||
not cd['hardwaremanagement.manager']['value'].startswith(
|
||||
'fe80::')):
|
||||
newip = cd['hardwaremanagement.manager']['value']
|
||||
newipinfo = getaddrinfo(newip, 0)[0]
|
||||
newip = newipinfo[-1][0]
|
||||
if ':' in newip:
|
||||
raise exc.NotImplementedException('IPv6 remote config TODO')
|
||||
currnet = wc.grab_json_response('/api/settings/network')
|
||||
for net in currnet:
|
||||
if net['channel_number'] == self.channel and net['lan_enable'] == 0:
|
||||
# ignore false indication and switch to 8 (dedicated)
|
||||
self.channel = 8
|
||||
if net['channel_number'] == self.channel:
|
||||
# we have found the interface to potentially manipulate
|
||||
if net['ipv4_address'] != newip:
|
||||
netconfig = netutil.get_nic_config(self.configmanager, nodename, ip=newip)
|
||||
newmask = netutil.cidr_to_mask(netconfig['prefix'])
|
||||
net['ipv4_address'] = newip
|
||||
net['ipv4_subnet'] = newmask
|
||||
if netconfig['ipv4_gateway']:
|
||||
net['ipv4_gateway'] = netconfig['ipv4_gateway']
|
||||
net['ipv4_dhcp_enable'] = 0
|
||||
rsp, status = wc.grab_json_response_with_status(
|
||||
'/api/settings/network/{0}'.format(net['id']), net, method='PUT')
|
||||
break
|
||||
elif self.ipaddr.startswith('fe80::'):
|
||||
self.configmanager.set_node_attributes(
|
||||
{nodename: {'hardwaremanagement.manager': self.ipaddr}})
|
||||
else:
|
||||
raise exc.TargetEndpointUnreachable(
|
||||
'hardwaremanagement.manager must be set to desired address (No IPv6 Link Local detected)')
|
||||
rsp, status = wc.grab_json_response_with_status('/api/session', method='DELETE')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
import confluent.config.configmanager as cfm
|
||||
c = cfm.ConfigManager(None)
|
||||
import sys
|
||||
info = {'addresses': [[sys.argv[1]]] }
|
||||
print(repr(info))
|
||||
testr = NodeHandler(info, c)
|
||||
testr.config(sys.argv[2])
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright 2017 Lenovo
|
||||
# Copyright 2017-2019 Lenovo
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -12,51 +12,404 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import base64
|
||||
import confluent.discovery.handlers.imm as immhandler
|
||||
import confluent.netutil as netutil
|
||||
import confluent.util as util
|
||||
import errno
|
||||
import eventlet
|
||||
import eventlet.support.greendns
|
||||
import json
|
||||
import os
|
||||
import pyghmi.exceptions as pygexc
|
||||
import pyghmi.ipmi.oem.lenovo.imm as imm
|
||||
import eventlet.green.socket as socket
|
||||
webclient = eventlet.import_patched('pyghmi.util.webclient')
|
||||
import struct
|
||||
getaddrinfo = eventlet.support.greendns.getaddrinfo
|
||||
|
||||
|
||||
def fixup_uuid(uuidprop):
|
||||
baduuid = ''.join(uuidprop.split())
|
||||
uuidprefix = (baduuid[:8], baduuid[8:12], baduuid[12:16])
|
||||
a = 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).upper()
|
||||
|
||||
|
||||
|
||||
|
||||
class NodeHandler(immhandler.NodeHandler):
|
||||
devname = 'XCC'
|
||||
|
||||
def preconfig(self):
|
||||
def __init__(self, info, configmanager):
|
||||
self._wc = None
|
||||
self.nodename = None
|
||||
self.tmpnodename = None
|
||||
self.tmppasswd = None
|
||||
self._atdefaultcreds = True
|
||||
self._needpasswordchange = True
|
||||
self._currcreds = (None, None)
|
||||
super(NodeHandler, self).__init__(info, configmanager)
|
||||
|
||||
@classmethod
|
||||
def adequate(cls, info):
|
||||
# We can sometimes receive a partially initialized SLP packet
|
||||
# This is not adequate for being satisfied
|
||||
return bool(info.get('attributes', {}))
|
||||
|
||||
def preconfig(self, possiblenode):
|
||||
self.tmpnodename = possiblenode
|
||||
ff = self.info.get('attributes', {}).get('enclosure-form-factor', '')
|
||||
if ff not in ('dense-computing', [u'dense-computing']):
|
||||
# skip preconfig for non-SD530 servers
|
||||
return
|
||||
self.trieddefault = None # Reset state on a preconfig attempt
|
||||
# attempt to enable SMM
|
||||
#it's normal to get a 'not supported' (193) for systems without an SMM
|
||||
# need to branch on 3.00+ firmware
|
||||
currfirm = self.info.get('attributes', {}).get('firmware-image-info', [''])[0]
|
||||
currfirm = currfirm.split(':')
|
||||
if len(currfirm) > 1:
|
||||
currfirm = float(currfirm[1])
|
||||
disableipmi = False
|
||||
if currfirm >= 3:
|
||||
# IPMI is disabled and we need it, also we need to go to *some* password
|
||||
wc = self.wc
|
||||
if not wc:
|
||||
# We cannot try to enable SMM here without risking real credentials
|
||||
# on the wire to untrusted parties
|
||||
return
|
||||
wc.grab_json_response('/api/providers/logout')
|
||||
wc.set_basic_credentials(self._currcreds[0], self._currcreds[1])
|
||||
rsp = wc.grab_json_response('/redfish/v1/Managers/1/NetworkProtocol')
|
||||
if not rsp.get('IPMI', {}).get('ProtocolEnabled', True):
|
||||
disableipmi = True
|
||||
_, _ = wc.grab_json_response_with_status(
|
||||
'/redfish/v1/Managers/1/NetworkProtocol',
|
||||
{'IPMI': {'ProtocolEnabled': True}}, method='PATCH')
|
||||
ipmicmd = None
|
||||
try:
|
||||
ipmicmd = self._get_ipmicmd()
|
||||
ipmicmd = self._get_ipmicmd(self._currcreds[0], self._currcreds[1])
|
||||
ipmicmd.xraw_command(netfn=0x3a, command=0xf1, data=(1,))
|
||||
except pygexc.IpmiException as e:
|
||||
if (e.ipmicode != 193 and 'Unauthorized name' not in str(e) and
|
||||
'Incorrect password' not in str(e)):
|
||||
# raise an issue if anything other than to be expected
|
||||
if disableipmi:
|
||||
_, _ = wc.grab_json_response_with_status(
|
||||
'/redfish/v1/Managers/1/NetworkProtocol',
|
||||
{'IPMI': {'ProtocolEnabled': False}}, method='PATCH')
|
||||
raise
|
||||
self.trieddefault = True
|
||||
if disableipmi:
|
||||
_, _ = wc.grab_json_response_with_status(
|
||||
'/redfish/v1/Managers/1/NetworkProtocol',
|
||||
{'IPMI': {'ProtocolEnabled': False}}, method='PATCH')
|
||||
#TODO: decide how to clean out if important
|
||||
#as it stands, this can step on itself
|
||||
#if ipmicmd:
|
||||
# ipmicmd.ipmi_session.logout()
|
||||
|
||||
def validate_cert(self, certificate):
|
||||
# broadly speaking, merely checks consistency moment to moment,
|
||||
# but if https_cert gets stricter, this check means something
|
||||
fprint = util.get_fingerprint(self.https_cert)
|
||||
return util.cert_matches(fprint, certificate)
|
||||
|
||||
def get_webclient(self, username, password, newpassword):
|
||||
wc = self._wc.dupe()
|
||||
try:
|
||||
wc.connect()
|
||||
except socket.error as se:
|
||||
if se.errno != errno.ECONNREFUSED:
|
||||
raise
|
||||
return (None, None)
|
||||
pwdchanged = False
|
||||
adata = json.dumps({'username': username,
|
||||
'password': password
|
||||
})
|
||||
headers = {'Connection': 'keep-alive',
|
||||
'Content-Type': 'application/json'}
|
||||
wc.request('POST', '/api/login', adata, headers)
|
||||
rsp = wc.getresponse()
|
||||
if rsp.status != 200 and password == 'PASSW0RD':
|
||||
rsp.read()
|
||||
adata = json.dumps({
|
||||
'username': username,
|
||||
'password': newpassword,
|
||||
})
|
||||
headers = {'Connection': 'keep-alive',
|
||||
'Content-Type': 'application/json'}
|
||||
wc.request('POST', '/api/login', adata, headers)
|
||||
rsp = wc.getresponse()
|
||||
if rsp.status == 200:
|
||||
pwdchanged = True
|
||||
password = newpassword
|
||||
else:
|
||||
rsp.read()
|
||||
return (None, None)
|
||||
if rsp.status == 200:
|
||||
self._currcreds = (username, password)
|
||||
wc.set_basic_credentials(username, password)
|
||||
rspdata = json.loads(rsp.read())
|
||||
wc.set_header('Content-Type', 'application/json')
|
||||
wc.set_header('Authorization', 'Bearer ' + rspdata['access_token'])
|
||||
if '_csrf_token' in wc.cookies:
|
||||
wc.set_header('X-XSRF-TOKEN', wc.cookies['_csrf_token'])
|
||||
if rspdata.get('pwchg_required', None) == 'true':
|
||||
wc.request('POST', '/api/function', json.dumps(
|
||||
{'USER_UserPassChange': '1,{0}'.format(newpassword)}))
|
||||
rsp = wc.getresponse()
|
||||
rsp.read()
|
||||
if rsp.status != 200:
|
||||
return (None, None)
|
||||
self._currcreds = (username, newpassword)
|
||||
wc.set_basic_credentials(username, newpassword)
|
||||
pwdchanged = True
|
||||
if '_csrf_token' in wc.cookies:
|
||||
wc.set_header('X-XSRF-TOKEN', wc.cookies['_csrf_token'])
|
||||
return (wc, pwdchanged)
|
||||
|
||||
@property
|
||||
def wc(self):
|
||||
passwd = None
|
||||
isdefault = True
|
||||
if self._wc is None:
|
||||
self._wc = webclient.SecureHTTPConnection(
|
||||
self.ipaddr, 443, verifycallback=self.validate_cert)
|
||||
self._wc.connect()
|
||||
nodename = None
|
||||
if self.nodename:
|
||||
nodename = self.nodename
|
||||
inpreconfig = False
|
||||
elif self.tmpnodename:
|
||||
nodename = None
|
||||
inpreconfig = True
|
||||
if self._currcreds[0] is not None:
|
||||
wc, pwdchanged = self.get_webclient(self._currcreds[0], self._currcreds[1], None)
|
||||
if wc:
|
||||
return wc
|
||||
if nodename:
|
||||
creds = self.configmanager.get_node_attributes(
|
||||
nodename, ['secret.hardwaremanagementuser',
|
||||
'secret.hardwaremanagementpassword'], decrypt=True)
|
||||
user, passwd, isdefault = self.get_node_credentials(
|
||||
nodename, creds, 'USERID', 'PASSW0RD')
|
||||
if not self.trieddefault:
|
||||
if not passwd:
|
||||
# So in preconfig context, we don't have admin permission to
|
||||
# actually divulge anything to the target
|
||||
# however the target *will* demand a new password... if it's currently
|
||||
# PASSW0RD
|
||||
# use TempW0rd42 to avoid divulging a real password on the line
|
||||
# This is replacing one well known password (PASSW0RD) with another
|
||||
# (TempW0rd42)
|
||||
passwd = 'TempW0rd42'
|
||||
wc, pwdchanged = self.get_webclient('USERID', 'PASSW0RD', passwd)
|
||||
if wc:
|
||||
if pwdchanged:
|
||||
if inpreconfig:
|
||||
self.tmppasswd = passwd
|
||||
else:
|
||||
self._needpasswordchange = False
|
||||
return wc
|
||||
self.trieddefault = True
|
||||
if isdefault:
|
||||
return
|
||||
self._atdefaultcreds = False
|
||||
if self.tmppasswd:
|
||||
wc, _ = self.get_webclient('USERID', self.tmppasswd, passwd)
|
||||
else:
|
||||
wc, _ = self.get_webclient(user, passwd, None)
|
||||
if wc:
|
||||
return wc
|
||||
|
||||
def set_password_policy(self):
|
||||
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
|
||||
if name.lower() == 'complexity':
|
||||
ruleset['USER_GlobalPassComplexRequired'] = value
|
||||
if name.lower() == 'reuse':
|
||||
ruleset['USER_GlobalMinPassReuseCycle'] = value
|
||||
try:
|
||||
wc = self.wc
|
||||
wc.grab_json_response('/api/dataset', ruleset)
|
||||
wc.grab_json_response('/api/providers/logout')
|
||||
except Exception as e:
|
||||
print(repr(e))
|
||||
pass
|
||||
|
||||
def _get_next_userid(self, wc):
|
||||
userinfo = wc.grab_json_response('/api/dataset/imm_users')
|
||||
userinfo = userinfo['items'][0]['users']
|
||||
for user in userinfo:
|
||||
if user['users_user_name'] == '':
|
||||
return user['users_user_id']
|
||||
|
||||
def _setup_xcc_account(self, username, passwd, wc):
|
||||
userinfo = wc.grab_json_response('/api/dataset/imm_users')
|
||||
uid = None
|
||||
for user in userinfo['items'][0]['users']:
|
||||
if user['users_user_name'] == username:
|
||||
uid = user['users_user_id']
|
||||
break
|
||||
else:
|
||||
for user in userinfo['items'][0]['users']:
|
||||
if user['users_user_name'] == 'USERID':
|
||||
uid = user['users_user_id']
|
||||
break
|
||||
if not uid:
|
||||
raise Exception("XCC has neither the default user nor configured user")
|
||||
# The following will work if the password is force change or normal..
|
||||
if self._needpasswordchange and self.tmppasswd != passwd:
|
||||
wc.grab_json_response('/api/function',
|
||||
{'USER_UserPassChange': '{0},{1}'.format(uid, passwd)})
|
||||
if username != 'USERID':
|
||||
wc.grab_json_response(
|
||||
'/api/function',
|
||||
{'USER_UserModify': '{0},{1},,1,4,0,0,0,0,,8,'.format(uid, username)})
|
||||
self.tmppasswd = None
|
||||
self._currcreds = (username, passwd)
|
||||
|
||||
def _convert_sha256account(self, user, passwd, wc):
|
||||
# First check if the specified user is sha256...
|
||||
userinfo = wc.grab_json_response('/api/dataset/imm_users')
|
||||
curruser = None
|
||||
uid = None
|
||||
for userent in userinfo['items'][0]['users']:
|
||||
if userent['users_user_name'] == user:
|
||||
curruser = userent
|
||||
break
|
||||
if curruser.get('users_pass_is_sha256', 0):
|
||||
self._wc = None
|
||||
wc = self.wc
|
||||
nwc = wc.dupe()
|
||||
# Have to convert it for being useful with most Lenovo automation tools
|
||||
# This requires deleting the account entirely and trying again
|
||||
tmpuid = self._get_next_userid(wc)
|
||||
try:
|
||||
tpass = base64.b64encode(os.urandom(9)) + 'Iw47$'
|
||||
userparams = "{0},6pmu0ezczzcp,{1},1,4,0,0,0,0,,8,".format(tmpuid, tpass)
|
||||
result = wc.grab_json_response('/api/function', {'USER_UserCreate': userparams})
|
||||
wc.grab_json_response('/api/providers/logout')
|
||||
adata = json.dumps({
|
||||
'username': '6pmu0ezczzcp',
|
||||
'password': tpass,
|
||||
})
|
||||
headers = {'Connection': 'keep-alive', 'Content-Type': 'application/json'}
|
||||
nwc.request('POST', '/api/login', adata, headers)
|
||||
rsp = nwc.getresponse()
|
||||
if rsp.status == 200:
|
||||
rspdata = json.loads(rsp.read())
|
||||
nwc.set_header('Content-Type', 'application/json')
|
||||
nwc.set_header('Authorization', 'Bearer ' + rspdata['access_token'])
|
||||
if '_csrf_token' in wc.cookies:
|
||||
nwc.set_header('X-XSRF-TOKEN', wc.cookies['_csrf_token'])
|
||||
if rspdata.get('reason', False):
|
||||
newpass = base64.b64encode(os.urandom(9)) + 'q4J$'
|
||||
nwc.grab_json_response(
|
||||
'/api/function',
|
||||
{'USER_UserPassChange': '{0},{1}'.format(tmpuid, newpass)})
|
||||
nwc.grab_json_response('/api/function', {'USER_UserDelete': "{0},{1}".format(curruser['users_user_id'], user)})
|
||||
userparams = "{0},{1},{2},1,4,0,0,0,0,,8,".format(curruser['users_user_id'], user, tpass)
|
||||
nwc.grab_json_response('/api/function', {'USER_UserCreate': userparams})
|
||||
nwc.grab_json_response('/api/providers/logout')
|
||||
nwc, pwdchanged = self.get_webclient(user, tpass, passwd)
|
||||
if not pwdchanged:
|
||||
nwc.grab_json_response(
|
||||
'/api/function',
|
||||
{'USER_UserPassChange': '{0},{1}'.format(curruser['users_user_id'], passwd)})
|
||||
nwc.grab_json_response('/api/providers/logout')
|
||||
finally:
|
||||
self._wc = None
|
||||
wc = self.wc
|
||||
wc.grab_json_response('/api/function', {'USER_UserDelete': "{0},{1}".format(tmpuid, '6pmu0ezczzcp')})
|
||||
wc.grab_json_response('/api/providers/logout')
|
||||
|
||||
def config(self, nodename, reset=False):
|
||||
self.nodename = nodename
|
||||
# TODO(jjohnson2): set ip parameters, user/pass, alert cfg maybe
|
||||
# In general, try to use https automation, to make it consistent
|
||||
# between hypothetical secure path and today.
|
||||
ic = self._bmcconfig(nodename)
|
||||
dpp = self.configmanager.get_node_attributes(
|
||||
nodename, 'discovery.passwordrules')
|
||||
self.ruleset = dpp.get(nodename, {}).get(
|
||||
'discovery.passwordrules', {}).get('value', '')
|
||||
wc = self.wc
|
||||
creds = self.configmanager.get_node_attributes(
|
||||
self.nodename, ['secret.hardwaremanagementuser',
|
||||
'secret.hardwaremanagementpassword'], decrypt=True)
|
||||
user, passwd, isdefault = self.get_node_credentials(nodename, creds, 'USERID', 'PASSW0RD')
|
||||
self.set_password_policy()
|
||||
if self._atdefaultcreds:
|
||||
if not isdefault:
|
||||
self._setup_xcc_account(user, passwd, wc)
|
||||
self._convert_sha256account(user, passwd, wc)
|
||||
cd = self.configmanager.get_node_attributes(
|
||||
nodename, ['secret.hardwaremanagementuser',
|
||||
'secret.hardwaremanagementpassword',
|
||||
'hardwaremanagement.manager', 'hardwaremanagement.method', 'console.method'],
|
||||
True)
|
||||
cd = cd.get(nodename, {})
|
||||
if (cd.get('hardwaremanagement.method', {}).get('value', 'ipmi') != 'redfish'
|
||||
or cd.get('console.method', {}).get('value', None) == 'ipmi'):
|
||||
nwc = wc.dupe()
|
||||
nwc.set_basic_credentials(self._currcreds[0], self._currcreds[1])
|
||||
rsp = nwc.grab_json_response('/redfish/v1/Managers/1/NetworkProtocol')
|
||||
if not rsp.get('IPMI', {}).get('ProtocolEnabled', True):
|
||||
# User has indicated IPMI support, but XCC is currently disabled
|
||||
# change XCC to be consistent
|
||||
_, _ = nwc.grab_json_response_with_status(
|
||||
'/redfish/v1/Managers/1/NetworkProtocol',
|
||||
{'IPMI': {'ProtocolEnabled': True}}, method='PATCH')
|
||||
if ('hardwaremanagement.manager' in cd and
|
||||
cd['hardwaremanagement.manager']['value'] and
|
||||
not cd['hardwaremanagement.manager']['value'].startswith(
|
||||
'fe80::')):
|
||||
newip = cd['hardwaremanagement.manager']['value']
|
||||
newipinfo = getaddrinfo(newip, 0)[0]
|
||||
newip = newipinfo[-1][0]
|
||||
if ':' in newip:
|
||||
raise exc.NotImplementedException('IPv6 remote config TODO')
|
||||
netconfig = netutil.get_nic_config(self.configmanager, nodename, ip=newip)
|
||||
newmask = netutil.cidr_to_mask(netconfig['prefix'])
|
||||
currinfo = wc.grab_json_response('/api/providers/logoninfo')
|
||||
currip = currinfo.get('items', [{}])[0].get('ipv4_address', '')
|
||||
# do not change the ipv4_config if the current config looks right already
|
||||
if currip != newip:
|
||||
statargs = {
|
||||
'ENET_IPv4Ena': '1', 'ENET_IPv4AddrSource': '0',
|
||||
'ENET_IPv4StaticIPAddr': newip, 'ENET_IPv4StaticIPNetMask': newmask
|
||||
}
|
||||
if netconfig['ipv4_gateway']:
|
||||
statargs['ENET_IPv4GatewayIPAddr'] = netconfig['ipv4_gateway']
|
||||
wc.grab_json_response('/api/dataset', statargs)
|
||||
elif self.ipaddr.startswith('fe80::'):
|
||||
self.configmanager.set_node_attributes(
|
||||
{nodename: {'hardwaremanagement.manager': self.ipaddr}})
|
||||
else:
|
||||
raise exc.TargetEndpointUnreachable(
|
||||
'hardwaremanagement.manager must be set to desired address (No IPv6 Link Local detected)')
|
||||
wc.grab_json_response('/api/providers/logout')
|
||||
ff = self.info.get('attributes', {}).get('enclosure-form-factor', '')
|
||||
if ff not in ('dense-computing', [u'dense-computing']):
|
||||
return
|
||||
# Ok, we can get the enclosure uuid now..
|
||||
ic.oem_init()
|
||||
enclosureuuid = ic._oem.immhandler.get_property(
|
||||
'/v2/ibmc/smm/chassis/uuid')
|
||||
enclosureuuid = ic._oem.immhandler.get_property(
|
||||
'/v2/ibmc/smm/chassis/uuid')
|
||||
enclosureuuid = self.info.get('attributes', {}).get('chassis-uuid', [None])[0]
|
||||
if enclosureuuid:
|
||||
enclosureuuid = imm.fixup_uuid(enclosureuuid).lower()
|
||||
enclosureuuid = enclosureuuid.lower()
|
||||
em = self.configmanager.get_node_attributes(nodename,
|
||||
'enclosure.manager')
|
||||
em = em.get(nodename, {}).get('enclosure.manager', {}).get(
|
||||
@@ -65,8 +418,3 @@ class NodeHandler(immhandler.NodeHandler):
|
||||
if em:
|
||||
self.configmanager.set_node_attributes(
|
||||
{em: {'id.uuid': enclosureuuid}})
|
||||
|
||||
# TODO(jjohnson2): web based init config for future prevalidated cert scheme
|
||||
# def config(self, nodename):
|
||||
# return
|
||||
|
||||
|
||||
@@ -72,7 +72,7 @@ def find_info_in_options(rq, optidx):
|
||||
return uuid, arch
|
||||
return uuid, arch
|
||||
|
||||
def snoop(handler):
|
||||
def snoop(handler, protocol=None):
|
||||
#TODO(jjohnson2): ipv6 socket and multicast for DHCPv6, should that be
|
||||
#prominent
|
||||
#TODO(jjohnson2): IP_PKTINFO, recvmsg to get the destination ip, per
|
||||
@@ -98,7 +98,7 @@ def snoop(handler):
|
||||
netaddr = ':'.join(['{0:02x}'.format(x) for x in netaddr])
|
||||
optidx = 0
|
||||
try:
|
||||
optidx = rq.index('\x63\x82\x53\x63') + 4
|
||||
optidx = rq.index(b'\x63\x82\x53\x63') + 4
|
||||
except ValueError:
|
||||
continue
|
||||
uuid, arch = find_info_in_options(rq, optidx)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2017 Lenovo
|
||||
# Copyright 2017-2019 Lenovo
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -27,6 +27,8 @@ import traceback
|
||||
_slp_services = set([
|
||||
'service:management-hardware.IBM:integrated-management-module2',
|
||||
'service:lenovo-smm',
|
||||
'service:ipmi',
|
||||
'service:lighttpd',
|
||||
'service:management-hardware.Lenovo:lenovo-xclarity-controller',
|
||||
'service:management-hardware.IBM:chassis-management-module',
|
||||
'service:management-hardware.Lenovo:chassis-management-module',
|
||||
@@ -47,7 +49,6 @@ 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:
|
||||
@@ -245,20 +246,24 @@ def _grab_rsps(socks, rsps, interval, xidmap):
|
||||
|
||||
def _parse_attrlist(attrstr):
|
||||
attribs = {}
|
||||
previousattrlen = None
|
||||
while attrstr:
|
||||
if len(attrstr) == previousattrlen:
|
||||
raise Exception('Looping in attrstr parsing')
|
||||
previousattrlen = len(attrstr)
|
||||
if attrstr[0] == '(':
|
||||
if ')' not in attrstr:
|
||||
if b')' not in attrstr:
|
||||
attribs['INCOMPLETE'] = True
|
||||
return attribs
|
||||
currattr = attrstr[1:attrstr.index(')')]
|
||||
if '=' not in currattr: # Not allegedly kosher, but still..
|
||||
currattr = attrstr[1:attrstr.index(b')')]
|
||||
if b'=' not in currattr: # Not allegedly kosher, but still..
|
||||
currattr = currattr.decode('utf-8')
|
||||
attribs[currattr] = None
|
||||
else:
|
||||
attrname, attrval = currattr.split('=')
|
||||
attrname, attrval = currattr.split('=', 1)
|
||||
attrname = attrname.decode('utf-8')
|
||||
attribs[attrname] = []
|
||||
for val in attrval.split(','):
|
||||
for val in attrval.split(b','):
|
||||
try:
|
||||
val = val.decode('utf-8')
|
||||
except UnicodeDecodeError:
|
||||
@@ -272,9 +277,9 @@ def _parse_attrlist(attrstr):
|
||||
val = finalval
|
||||
if 'uuid' in attrname and len(val) == 16:
|
||||
lebytes = struct.unpack_from(
|
||||
'<IHH', buffer(val[:8]))
|
||||
'<IHH', memoryview(val[:8]))
|
||||
bebytes = struct.unpack_from(
|
||||
'>HHI', buffer(val[8:]))
|
||||
'>HHI', memoryview(val[8:]))
|
||||
val = '{0:08X}-{1:04X}-{2:04X}-{3:04X}-' \
|
||||
'{4:04X}{5:08X}'.format(
|
||||
lebytes[0], lebytes[1], lebytes[2], bebytes[0],
|
||||
@@ -282,12 +287,12 @@ def _parse_attrlist(attrstr):
|
||||
).lower()
|
||||
attribs[attrname].append(val)
|
||||
attrstr = attrstr[attrstr.index(')'):]
|
||||
elif attrstr[0] == ',':
|
||||
elif attrstr[0] == b','[0]:
|
||||
attrstr = attrstr[1:]
|
||||
elif ',' in attrstr:
|
||||
currattr = attrstr[:attrstr.index(',')]
|
||||
elif b',' in attrstr:
|
||||
currattr = attrstr[:attrstr.index(b',')]
|
||||
attribs[currattr] = None
|
||||
attrstr = attrstr[attrstr.index(','):]
|
||||
attrstr = attrstr[attrstr.index(b','):]
|
||||
else:
|
||||
currattr = attrstr
|
||||
attribs[currattr] = None
|
||||
@@ -295,9 +300,11 @@ def _parse_attrlist(attrstr):
|
||||
return attribs
|
||||
|
||||
|
||||
def _parse_attrs(data, parsed):
|
||||
def _parse_attrs(data, parsed, xid=None):
|
||||
headinfo = _parse_slp_header(data)
|
||||
if headinfo['function'] != 7 or headinfo['xid'] != parsed['xid']:
|
||||
if xid is None:
|
||||
xid = parsed['xid']
|
||||
if headinfo['function'] != 7 or headinfo['xid'] != xid:
|
||||
return
|
||||
payload = headinfo['payload']
|
||||
if struct.unpack('!H', bytes(payload[:2]))[0] != 0:
|
||||
@@ -307,8 +314,18 @@ def _parse_attrs(data, parsed):
|
||||
parsed['attributes'] = _parse_attrlist(attrstr)
|
||||
|
||||
|
||||
def fix_info(info, handler):
|
||||
if '_attempts' not in info:
|
||||
info['_attempts'] = 10
|
||||
if info['_attempts'] == 0:
|
||||
return
|
||||
info['_attempts'] -= 1
|
||||
_add_attributes(info)
|
||||
handler(info)
|
||||
|
||||
def _add_attributes(parsed):
|
||||
attrq = _generate_attr_request(parsed['services'][0], parsed['xid'])
|
||||
xid = parsed.get('xid', 42)
|
||||
attrq = _generate_attr_request(parsed['services'][0], xid)
|
||||
target = None
|
||||
# prefer reaching out to an fe80 if present, to be highly robust
|
||||
# in face of network changes
|
||||
@@ -323,13 +340,14 @@ def _add_attributes(parsed):
|
||||
else:
|
||||
net = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
try:
|
||||
net.settimeout(1.0)
|
||||
net.connect(target)
|
||||
except socket.error:
|
||||
return
|
||||
net.sendall(attrq)
|
||||
rsp = net.recv(8192)
|
||||
net.close()
|
||||
_parse_attrs(rsp, parsed)
|
||||
_parse_attrs(rsp, parsed, xid)
|
||||
|
||||
|
||||
def query_srvtypes(target):
|
||||
@@ -351,6 +369,7 @@ def query_srvtypes(target):
|
||||
while tries and not connected:
|
||||
tries -= 1
|
||||
try:
|
||||
net.settimeout(1.0)
|
||||
net.connect(target)
|
||||
connected = True
|
||||
except socket.error:
|
||||
@@ -383,7 +402,7 @@ def rescan(handler):
|
||||
handler(scanned)
|
||||
|
||||
|
||||
def snoop(handler):
|
||||
def snoop(handler, protocol=None):
|
||||
"""Watch for SLP activity
|
||||
|
||||
handler will be called with a dictionary of relevant attributes
|
||||
@@ -392,7 +411,7 @@ def snoop(handler):
|
||||
:return:
|
||||
"""
|
||||
tracelog = log.Logger('trace')
|
||||
active_scan(handler)
|
||||
active_scan(handler, protocol)
|
||||
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')
|
||||
@@ -469,13 +488,26 @@ def snoop(handler):
|
||||
peerbymacaddress[mac]['xid'] = 1
|
||||
_add_attributes(peerbymacaddress[mac])
|
||||
peerbymacaddress[mac]['hwaddr'] = mac
|
||||
peerbymacaddress[mac]['protocol'] = protocol
|
||||
if 'service:ipmi' in peerbymacaddress[mac]['services']:
|
||||
if 'service:ipmi//Athena:623' in peerbymacaddress[mac].get('urls', ()):
|
||||
peerbymacaddress[mac]['services'] = ['service:thinkagile-storage']
|
||||
else:
|
||||
continue
|
||||
if 'service:lightttpd' in peerbymacaddress[mac]['services']:
|
||||
currinf = peerbymacaddress[mac]
|
||||
curratt = currinf.get('attributes', {})
|
||||
if curratt.get('System-Manufacturing', [None])[0] == 'Lenovo' and curratt.get('type', [None])[0] == 'LenovoThinkServer':
|
||||
peerbymacaddress[mac]['services'] = ['service:lenovo-tsm']
|
||||
else:
|
||||
continue
|
||||
handler(peerbymacaddress[mac])
|
||||
except Exception as e:
|
||||
tracelog.log(traceback.format_exc(), ltype=log.DataTypes.event,
|
||||
event=log.Events.stacktrace)
|
||||
|
||||
|
||||
def active_scan(handler):
|
||||
def active_scan(handler, protocol=None):
|
||||
known_peers = set([])
|
||||
for scanned in scan():
|
||||
for addr in scanned['addresses']:
|
||||
@@ -486,6 +518,7 @@ def active_scan(handler):
|
||||
break
|
||||
known_peers.add(addr)
|
||||
else:
|
||||
scanned['protocol'] = protocol
|
||||
handler(scanned)
|
||||
|
||||
|
||||
@@ -531,6 +564,11 @@ def scan(srvtypes=_slp_services, addresses=None, localonly=False):
|
||||
_grab_rsps((net, net4), rsps, 1, xidmap)
|
||||
# now to analyze and flesh out the responses
|
||||
for id in rsps:
|
||||
if 'service:ipmi' in rsps[id]['services']:
|
||||
if 'service:ipmi://Athena:623' in rsps[id]['urls']:
|
||||
rsps[id]['services'] = ['service:thinkagile-storage']
|
||||
else:
|
||||
continue
|
||||
if localonly:
|
||||
for addr in rsps[id]['addresses']:
|
||||
if 'fe80' in addr[0]:
|
||||
@@ -538,6 +576,15 @@ def scan(srvtypes=_slp_services, addresses=None, localonly=False):
|
||||
else:
|
||||
continue
|
||||
_add_attributes(rsps[id])
|
||||
if 'service:lighttpd' in rsps[id]['services']:
|
||||
currinf = rsps[id]
|
||||
curratt = currinf.get('attributes', {})
|
||||
if curratt.get('System-Manufacturing', [None])[0] == 'Lenovo' and curratt.get('type', [None])[0] == 'LenovoThinkServer':
|
||||
currinf['services'] = ['service:lenovo-tsm']
|
||||
curratt['enclosure-serial-number'] = curratt['Product-Serial']
|
||||
curratt['enclosure-machinetype-model'] = curratt['Machine-Type']
|
||||
else:
|
||||
continue
|
||||
del rsps[id]['payload']
|
||||
del rsps[id]['function']
|
||||
del rsps[id]['xid']
|
||||
|
||||
@@ -78,8 +78,13 @@ def snoop(handler, byehandler=None):
|
||||
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)
|
||||
try:
|
||||
net4.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP,
|
||||
ssdp4mcast)
|
||||
except socket.error as e:
|
||||
if e.errno != 98:
|
||||
# errno 98 can happen if aliased, skip for now
|
||||
raise
|
||||
net4.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
net4.bind(('', 1900))
|
||||
net6.bind(('', 1900))
|
||||
@@ -167,6 +172,7 @@ def _find_service(service, target):
|
||||
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
|
||||
deadline = util.monotonic_time() + 4
|
||||
r, _, _ = select.select((net4, net6), (), (), 4)
|
||||
peerdata = {}
|
||||
while r:
|
||||
@@ -174,7 +180,10 @@ def _find_service(service, target):
|
||||
(rsp, peer) = s.recvfrom(9000)
|
||||
neighutil.refresh_neigh()
|
||||
_parse_ssdp(peer, rsp, peerdata)
|
||||
r, _, _ = select.select((net4, net6), (), (), 4)
|
||||
timeout = deadline - util.monotonic_time()
|
||||
if timeout < 0:
|
||||
timeout = 0
|
||||
r, _, _ = select.select((net4, net6), (), (), timeout)
|
||||
for nid in peerdata:
|
||||
yield peerdata[nid]
|
||||
|
||||
@@ -194,6 +203,8 @@ def _parse_ssdp(peer, rsp, peerdata):
|
||||
if code == '200':
|
||||
if nid in peerdata:
|
||||
peerdatum = peerdata[nid]
|
||||
if peer not in peerdatum['peers']:
|
||||
peerdatum['peers'].append(peer)
|
||||
else:
|
||||
peerdatum = {
|
||||
'peers': [peer],
|
||||
|
||||
@@ -106,6 +106,7 @@ class PubkeyInvalid(ConfluentException):
|
||||
super(PubkeyInvalid, self).__init__(self, text)
|
||||
self.fingerprint = fingerprint
|
||||
self.attrname = attribname
|
||||
self.message = text
|
||||
bodydata = {'message': text,
|
||||
'event': event,
|
||||
'fingerprint': fingerprint,
|
||||
|
||||
@@ -19,22 +19,31 @@
|
||||
# the time comes
|
||||
|
||||
import confluent.exceptions as exc
|
||||
import confluent.log as log
|
||||
import confluent.messages as msg
|
||||
import eventlet
|
||||
import os
|
||||
import pwd
|
||||
import socket
|
||||
import traceback
|
||||
|
||||
updatesbytarget = {}
|
||||
uploadsbytarget = {}
|
||||
downloadsbytarget = {}
|
||||
updatepool = eventlet.greenpool.GreenPool(256)
|
||||
_tracelog = None
|
||||
|
||||
|
||||
def execupdate(handler, filename, updateobj, type):
|
||||
if not os.path.exists(filename):
|
||||
def execupdate(handler, filename, updateobj, type, owner, node):
|
||||
global _tracelog
|
||||
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,
|
||||
@@ -43,6 +52,9 @@ def execupdate(handler, filename, updateobj, type):
|
||||
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 ' \
|
||||
@@ -50,23 +62,28 @@ def execupdate(handler, filename, updateobj, type):
|
||||
updateobj.handle_progress({'phase': 'error', 'progress': 0.0,
|
||||
'detail': errstr})
|
||||
except Exception as e:
|
||||
if _tracelog is None:
|
||||
_tracelog = log.Logger('trace')
|
||||
_tracelog.log(traceback.format_exc(), ltype=log.DataTypes.event, event=log.Events.stacktrace)
|
||||
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'):
|
||||
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)
|
||||
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:
|
||||
@@ -96,6 +113,8 @@ def remove_updates(nodes, tenant, element, type='firmware'):
|
||||
upid = element[-1]
|
||||
if type == 'firmware':
|
||||
myparty = updatesbytarget
|
||||
elif type == 'ffdc':
|
||||
myparty = downloadsbytarget
|
||||
else:
|
||||
myparty = uploadsbytarget
|
||||
for node in nodes:
|
||||
@@ -115,6 +134,9 @@ def list_updates(nodes, tenant, element, type='firmware'):
|
||||
if type == 'mediaupload':
|
||||
myparty = uploadsbytarget
|
||||
verb = 'upload'
|
||||
elif type == 'ffdc':
|
||||
verb = 'download'
|
||||
myparty = downloadsbytarget
|
||||
else:
|
||||
myparty = updatesbytarget
|
||||
verb = 'update'
|
||||
|
||||
@@ -49,6 +49,7 @@ def forward_port(sock, target, clientip, sessionid):
|
||||
continue
|
||||
try:
|
||||
client = socket.create_connection((target, 443))
|
||||
client.setsockopt(socket.IPPROTO_TCP, socket.TCP_MAXSEG, 1456)
|
||||
except Exception:
|
||||
conn.close()
|
||||
continue
|
||||
@@ -68,6 +69,7 @@ def forward_video():
|
||||
try:
|
||||
vidclient = socket.create_connection((vidtargetbypeer[cli[0]],
|
||||
3900))
|
||||
vidclient.setsockopt(socket.IPPROTO_TCP, socket.TCP_MAXSEG, 1456)
|
||||
except Exception:
|
||||
conn.close()
|
||||
continue
|
||||
|
||||
@@ -17,7 +17,10 @@
|
||||
# This SCGI server provides a http wrap to confluent api
|
||||
# It additionally manages httprequest console sessions
|
||||
import base64
|
||||
import Cookie
|
||||
try:
|
||||
import Cookie
|
||||
except ModuleNotFoundError:
|
||||
import http.cookies as Cookie
|
||||
import confluent.auth as auth
|
||||
import confluent.config.attributes as attribs
|
||||
import confluent.consoleserver as consoleserver
|
||||
@@ -39,7 +42,10 @@ import socket
|
||||
import sys
|
||||
import traceback
|
||||
import time
|
||||
import urlparse
|
||||
try:
|
||||
import urlparse
|
||||
except ModuleNotFoundError:
|
||||
import urllib.parse as urlparse
|
||||
import eventlet.wsgi
|
||||
#scgi = eventlet.import_patched('flup.server.scgi')
|
||||
tlvdata = confluent.tlvdata
|
||||
@@ -269,6 +275,9 @@ def _authorize_request(env, operation):
|
||||
name = ''
|
||||
sessionid = None
|
||||
cookie = Cookie.SimpleCookie()
|
||||
element = env['PATH_INFO']
|
||||
if element.startswith('/sessions/current/'):
|
||||
element = None
|
||||
if 'HTTP_COOKIE' in env:
|
||||
#attempt to use the cookie. If it matches
|
||||
cc = RobustCookie()
|
||||
@@ -290,7 +299,7 @@ def _authorize_request(env, operation):
|
||||
httpsessions[sessionid]['expiry'] = time.time() + 90
|
||||
name = httpsessions[sessionid]['name']
|
||||
authdata = auth.authorize(
|
||||
name, element=None,
|
||||
name, element=element, operation=operation,
|
||||
skipuserobj=httpsessions[sessionid]['skipuserobject'])
|
||||
if (not authdata) and 'HTTP_AUTHORIZATION' in env:
|
||||
if env['PATH_INFO'] == '/sessions/current/logout':
|
||||
@@ -303,8 +312,10 @@ def _authorize_request(env, operation):
|
||||
return ('logout',)
|
||||
name, passphrase = base64.b64decode(
|
||||
env['HTTP_AUTHORIZATION'].replace('Basic ', '')).split(':', 1)
|
||||
authdata = auth.check_user_passphrase(name, passphrase, element=None)
|
||||
if not authdata:
|
||||
authdata = auth.check_user_passphrase(name, passphrase, operation=operation, element=element)
|
||||
if authdata is False:
|
||||
return {'code': 403}
|
||||
elif not authdata:
|
||||
return {'code': 401}
|
||||
sessid = util.randomstring(32)
|
||||
while sessid in httpsessions:
|
||||
@@ -341,15 +352,10 @@ def _authorize_request(env, operation):
|
||||
if 'csrftoken' in httpsessions[sessid]:
|
||||
authinfo['authtoken'] = httpsessions[sessid]['csrftoken']
|
||||
return authinfo
|
||||
else:
|
||||
elif authdata is None:
|
||||
return {'code': 401}
|
||||
# TODO(jbjohnso): actually evaluate the request for authorization
|
||||
# In theory, the x509 or http auth stuff will get translated and then
|
||||
# passed on to the core authorization function in an appropriate form
|
||||
# expresses return in the form of http code
|
||||
# 401 if there is no known identity
|
||||
# 403 if valid identity, but no access
|
||||
# going to run 200 just to get going for now
|
||||
else:
|
||||
return {'code': 403}
|
||||
|
||||
|
||||
def _pick_mimetype(env):
|
||||
@@ -384,11 +390,11 @@ def resourcehandler(env, start_response):
|
||||
try:
|
||||
for rsp in resourcehandler_backend(env, start_response):
|
||||
yield rsp
|
||||
except:
|
||||
except Exception as e:
|
||||
tracelog.log(traceback.format_exc(), ltype=log.DataTypes.event,
|
||||
event=log.Events.stacktrace)
|
||||
start_response('500 - Internal Server Error', [])
|
||||
yield '500 - Internal Server Error'
|
||||
start_response('500 - ' + str(e), [])
|
||||
yield '500 - ' + str(e)
|
||||
return
|
||||
|
||||
|
||||
@@ -429,7 +435,7 @@ def resourcehandler_backend(env, start_response):
|
||||
return
|
||||
if authorized['code'] == 403:
|
||||
start_response('403 Forbidden', badauth)
|
||||
yield 'authorization failed'
|
||||
yield 'Forbidden'
|
||||
return
|
||||
if authorized['code'] != 200:
|
||||
raise Exception("Unrecognized code from auth engine")
|
||||
@@ -469,6 +475,10 @@ def resourcehandler_backend(env, start_response):
|
||||
funport = forwarder.get_port(targip, env['HTTP_X_FORWARDED_FOR'],
|
||||
authorized['sessionid'])
|
||||
host = env['HTTP_X_FORWARDED_HOST']
|
||||
if ']' in host:
|
||||
host = host.split(']')[0] + ']'
|
||||
elif ':' in host:
|
||||
host = host.rsplit(':', 1)[0]
|
||||
url = 'https://{0}:{1}/'.format(host, funport)
|
||||
start_response('302', [('Location', url)])
|
||||
yield 'Our princess is in another castle!'
|
||||
@@ -496,6 +506,8 @@ def resourcehandler_backend(env, start_response):
|
||||
skipreplay = False
|
||||
if 'skipreplay' in querydict and querydict['skipreplay']:
|
||||
skipreplay = True
|
||||
width = querydict.get('width', 80)
|
||||
height = querydict.get('height', 24)
|
||||
datacallback = None
|
||||
async = None
|
||||
if 'HTTP_CONFLUENTASYNCID' in env:
|
||||
@@ -507,13 +519,13 @@ def resourcehandler_backend(env, start_response):
|
||||
consession = shellserver.ShellSession(
|
||||
node=nodename, configmanager=cfgmgr,
|
||||
username=authorized['username'], skipreplay=skipreplay,
|
||||
datacallback=datacallback
|
||||
datacallback=datacallback, width=width, height=height
|
||||
)
|
||||
else:
|
||||
consession = consoleserver.ConsoleSession(
|
||||
node=nodename, configmanager=cfgmgr,
|
||||
username=authorized['username'], skipreplay=skipreplay,
|
||||
datacallback=datacallback
|
||||
datacallback=datacallback, width=width, height=height
|
||||
)
|
||||
except exc.NotFoundException:
|
||||
start_response("404 Not found", headers)
|
||||
@@ -548,6 +560,9 @@ def resourcehandler_backend(env, start_response):
|
||||
elif 'action' in querydict:
|
||||
if querydict['action'] == 'break':
|
||||
consolesessions[querydict['session']]['session'].send_break()
|
||||
elif querydict['action'] == 'resize':
|
||||
consolesessions[querydict['session']]['session'].resize(
|
||||
width=querydict['width'], height=querydict['height'])
|
||||
elif querydict['action'] == 'reopen':
|
||||
consolesessions[querydict['session']]['session'].reopen()
|
||||
else:
|
||||
@@ -784,8 +799,17 @@ def serve(bind_host, bind_port):
|
||||
'Failed to open HTTP due to busy port, trying again in'
|
||||
' a second\n')
|
||||
eventlet.sleep(1)
|
||||
eventlet.wsgi.server(sock, resourcehandler, log=False, log_output=False,
|
||||
debug=False, socket_timeout=60)
|
||||
# TCP_FASTOPEN
|
||||
try:
|
||||
sock.setsockopt(socket.SOL_TCP, 23, 5)
|
||||
except Exception:
|
||||
pass # we gave it our best shot there
|
||||
try:
|
||||
eventlet.wsgi.server(sock, resourcehandler, log=False, log_output=False,
|
||||
debug=False, socket_timeout=60)
|
||||
except TypeError:
|
||||
# Older eventlet in place, skip arguments it does not understand
|
||||
eventlet.wsgi.server(sock, resourcehandler, log=False, debug=False)
|
||||
|
||||
|
||||
class HttpApi(object):
|
||||
|
||||
@@ -41,6 +41,11 @@ class Console(object):
|
||||
the remote console"""
|
||||
return
|
||||
|
||||
def resize(self, width, height):
|
||||
"""This function is responsible for relaying resize request from
|
||||
client terminal to remote console"""
|
||||
return
|
||||
|
||||
def ping(self):
|
||||
"""This function is a hint to the console plugin that now would be a
|
||||
nice time to assess health of console connection. Plugins that see
|
||||
|
||||
@@ -51,11 +51,15 @@
|
||||
# - leading bit reserved, 0 for now
|
||||
# - length of metadata record 7 bits
|
||||
# - type of data referenced by this entry (one byte), currently:
|
||||
# 0=text event, 1=json, 2=console data
|
||||
# 0=text event, 1=json, 2=console data, 3=event
|
||||
# - offset into the text log to begin (4 bytes)
|
||||
# - length of data referenced by this entry (2 bytes)
|
||||
# - UTC timestamp of this entry in seconds since epoch (unsigned 32 bit?)
|
||||
# - CRC32 over the record
|
||||
# - UTC timestamp of this entry in seconds since epoch (unsigned 32 bit)
|
||||
# - Event type (per 'Events' class below)
|
||||
# - Event data (per event, currently used by connect/disconnect to represent
|
||||
# single or multiple connections by user and for 'appmode' and 'shiftin'
|
||||
# status for console
|
||||
# - 2 reserved bytes
|
||||
# (a future extended version might include suport for Forward Secure Sealing
|
||||
# or other fields)
|
||||
|
||||
@@ -72,7 +76,13 @@ import stat
|
||||
import struct
|
||||
import time
|
||||
import traceback
|
||||
try:
|
||||
unicode
|
||||
except NameError:
|
||||
unicode = str
|
||||
|
||||
daemonized = False
|
||||
logfull = False
|
||||
try:
|
||||
from fcntl import flock, LOCK_EX, LOCK_UN, LOCK_SH
|
||||
except ImportError:
|
||||
@@ -150,21 +160,36 @@ class BaseRotatingHandler(object):
|
||||
Output the record to the file, catering for rollover as described
|
||||
in doRollover().
|
||||
"""
|
||||
rolling_type = self.shouldRollover(binrecord, textrecord)
|
||||
if rolling_type:
|
||||
flock(self.textfile, LOCK_UN)
|
||||
return self.doRollover(rolling_type)
|
||||
return None
|
||||
global logfull
|
||||
try:
|
||||
rolling_type = self.shouldRollover(binrecord, textrecord)
|
||||
if rolling_type:
|
||||
flock(self.textfile, LOCK_UN)
|
||||
return self.doRollover(rolling_type)
|
||||
return None
|
||||
except (IOError, OSError) as e:
|
||||
if not daemonized:
|
||||
raise
|
||||
logfull = True
|
||||
|
||||
|
||||
def emit(self, binrecord, textrecord):
|
||||
if self.textfile is None:
|
||||
self.textfile = open(self.textpath, mode='ab')
|
||||
if self.binfile is None:
|
||||
self.binfile = open(self.binpath, mode='ab')
|
||||
self.textfile.write(textrecord)
|
||||
self.binfile.write(binrecord)
|
||||
self.textfile.flush()
|
||||
self.binfile.flush()
|
||||
global logfull
|
||||
try:
|
||||
if self.textfile is None:
|
||||
self.textfile = open(self.textpath, mode='ab')
|
||||
if self.binfile is None:
|
||||
self.binfile = open(self.binpath, mode='ab')
|
||||
if not isinstance(textrecord, bytes):
|
||||
textrecord = textrecord.encode('utf-8')
|
||||
self.textfile.write(textrecord)
|
||||
self.binfile.write(binrecord)
|
||||
self.textfile.flush()
|
||||
self.binfile.flush()
|
||||
except (IOError, OSError) as e:
|
||||
if not daemonized:
|
||||
raise
|
||||
logfull = True
|
||||
|
||||
def get_textfile_offset(self, data_len):
|
||||
if self.textfile is None:
|
||||
@@ -561,28 +586,35 @@ class Logger(object):
|
||||
textdate = time.strftime(
|
||||
'%b %d %H:%M:%S ', time.localtime(tstamp))
|
||||
flock(textfile, LOCK_EX)
|
||||
offset = textfile.tell() + len(textdate)
|
||||
datalen = len(data)
|
||||
eventaux = entry[4]
|
||||
if eventaux is None:
|
||||
eventaux = 0
|
||||
# metadata length is always 16 for this code at the moment
|
||||
binrecord = struct.pack(
|
||||
">BBIHIBBH", 16, ltype, offset, datalen, tstamp, evtdata,
|
||||
eventaux, 0)
|
||||
if self.isconsole:
|
||||
if ltype == 2:
|
||||
textrecord = data
|
||||
try:
|
||||
offset = textfile.tell() + len(textdate)
|
||||
datalen = len(data)
|
||||
eventaux = entry[4]
|
||||
if eventaux is None:
|
||||
eventaux = 0
|
||||
# metadata length is always 16 for this code at the moment
|
||||
binrecord = struct.pack(
|
||||
">BBIHIBBH", 16, ltype, offset, datalen, tstamp, evtdata,
|
||||
eventaux, 0)
|
||||
if self.isconsole:
|
||||
if ltype == 2:
|
||||
textrecord = data
|
||||
else:
|
||||
textrecord = textdate + data + ']'
|
||||
else:
|
||||
textrecord = textdate + data + ']'
|
||||
else:
|
||||
textrecord = textdate + data
|
||||
if not textrecord.endswith('\n'):
|
||||
textrecord += '\n'
|
||||
files = self.handler.try_emit(binrecord, textrecord)
|
||||
textrecord = textdate + data
|
||||
if not textrecord.endswith('\n'):
|
||||
textrecord += '\n'
|
||||
files = self.handler.try_emit(binrecord, textrecord)
|
||||
except struct.error:
|
||||
files = self.handler.doRollover(RollingTypes.size_rolling)
|
||||
finally:
|
||||
try:
|
||||
flock(textfile, LOCK_UN)
|
||||
except Exception:
|
||||
pass
|
||||
if not files:
|
||||
self.handler.emit(binrecord, textrecord)
|
||||
flock(textfile, LOCK_UN)
|
||||
else:
|
||||
# Log the rolling event at first, then log the last data
|
||||
# which cause the rolling event.
|
||||
@@ -716,7 +748,7 @@ class Logger(object):
|
||||
pass
|
||||
|
||||
def log(self, logdata=None, ltype=None, event=0, eventdata=None):
|
||||
if type(logdata) not in (str, unicode, dict):
|
||||
if type(logdata) not in (bytes, unicode, dict):
|
||||
raise Exception("Unsupported logdata")
|
||||
if ltype is None:
|
||||
if type(logdata) == dict:
|
||||
@@ -753,11 +785,13 @@ globaleventlog = None
|
||||
tracelog = None
|
||||
|
||||
|
||||
def log(logdata=None, ltype=None, event=0, eventdata=None):
|
||||
def log(logdata=None, ltype=None, event=0, eventdata=None, flush=False):
|
||||
global globaleventlog
|
||||
if globaleventlog is None:
|
||||
globaleventlog = Logger('events')
|
||||
globaleventlog.log(logdata, ltype, event, eventdata)
|
||||
if flush:
|
||||
globaleventlog.writedata()
|
||||
|
||||
def logtrace():
|
||||
global tracelog
|
||||
|
||||
@@ -43,9 +43,11 @@ except ImportError:
|
||||
import confluent.discovery.core as disco
|
||||
import eventlet
|
||||
dbgif = False
|
||||
if map(int, (eventlet.__version__.split('.'))) > [0, 18]:
|
||||
try:
|
||||
import eventlet.backdoor as backdoor
|
||||
dbgif = True
|
||||
except Exception:
|
||||
pass
|
||||
havefcntl = True
|
||||
try:
|
||||
import fcntl
|
||||
@@ -72,7 +74,7 @@ def _daemonize():
|
||||
os.setsid()
|
||||
thispid = os.fork()
|
||||
if thispid > 0:
|
||||
print 'confluent server starting as pid %d' % thispid
|
||||
print('confluent server starting as pid {0}'.format(thispid))
|
||||
os._exit(0)
|
||||
os.closerange(0, 2)
|
||||
os.umask(63)
|
||||
@@ -81,6 +83,7 @@ def _daemonize():
|
||||
os.dup2(0, 2)
|
||||
sys.stdout = log.Logger('stdout', buffered=False)
|
||||
sys.stderr = log.Logger('stderr', buffered=False)
|
||||
log.daemonized = True
|
||||
|
||||
|
||||
def _updatepidfile():
|
||||
@@ -224,6 +227,11 @@ def run():
|
||||
except:
|
||||
doexit()
|
||||
raise
|
||||
try:
|
||||
log.log({'info': 'Confluent management service starting'}, flush=True)
|
||||
except (OSError, IOError) as e:
|
||||
print(repr(e))
|
||||
sys.exit(1)
|
||||
_daemonize()
|
||||
if havefcntl:
|
||||
_updatepidfile()
|
||||
@@ -231,7 +239,7 @@ def run():
|
||||
signal.signal(signal.SIGTERM, terminate)
|
||||
collective.startup()
|
||||
if dbgif:
|
||||
oumask = os.umask(0077)
|
||||
oumask = os.umask(0o077)
|
||||
try:
|
||||
os.remove('/var/run/confluent/dbg.sock')
|
||||
except OSError:
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user