diff --git a/confluent_client/bin/confetty b/confluent_client/bin/confetty index 4b34490f..64764a5e 100755 --- a/confluent_client/bin/confetty +++ b/confluent_client/bin/confetty @@ -235,7 +235,11 @@ def rcompleter(text, state): def parse_command(command): - args = shlex.split(command, posix=True) + try: + args = shlex.split(command, posix=True) + except ValueError as ve: + print('Error: ' + ve.message) + return [] return args diff --git a/confluent_client/bin/nodeattrib b/confluent_client/bin/nodeattrib new file mode 100644 index 00000000..a05915c6 --- /dev/null +++ b/confluent_client/bin/nodeattrib @@ -0,0 +1,165 @@ +#!/usr/bin/env python +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2017 Lenovo +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__author__ = 'alin37' + +import optparse +import os +import sys + +path = os.path.dirname(os.path.realpath(__file__)) +path = os.path.realpath(os.path.join(path, '..', 'lib', 'python')) +if path.startswith('/opt'): + sys.path.append(path) + +import confluent.client as client + +def attrrequested(attr, attrlist, seenattributes): + for candidate in attrlist: + truename = candidate + if candidate.startswith('hm'): + candidate = candidate.replace('hm', 'hardwaremanagement', 1) + if candidate == attr: + seenattributes.add(truename) + return True + elif '.' not in candidate and attr.startswith(candidate + '.'): + seenattributes.add(truename) + return True + return False +argparser = optparse.OptionParser( + usage='''\n %prog [options] noderange [list of attributes] \ + \n %prog [options] noderange attribute1=value1,attribute2=value,... + \n ''') +argparser.add_option('-b', '--blame', action='store_true', + help='Show information about how attributes inherited') +argparser.add_option('-c', '--clear', action='store_true', + help='Clear variables') +(options, args) = argparser.parse_args() + +showtype = 'current' +requestargs=None +try: + noderange = args[0] + nodelist = '/noderange/{0}/nodes/'.format(noderange) +except IndexError: + nodelist = '/nodes/' +session = client.Command() +exitcode = 0 + + +#Sets attributes +if len(args) > 1: + #clears attribute + if options.clear: + targpath = '/noderange/{0}/attributes/all'.format(noderange) + keydata = {} + for attrib in args[1:]: + keydata[attrib] = None + for res in session.update(targpath, keydata): + if 'error' in res: + if 'errorcode' in res: + exitcode = res['errorcode'] + sys.stderr.write('Error: ' + res['error'] + '\n') + sys.exit(exitcode) + else: + if args[1] == 'all': + showtype = 'all' + elif args[1] == 'current': + showtype = 'current' + elif "=" in args[1]: + try: + if len(args[1:]) > 1: + for val in args[1:]: + val = val.split('=') + exitcode=session.simple_noderange_command(noderange, 'attributes/all'.format(noderange), val[1], val[0]) + else: + val=args[1].split('=') + exitcode=session.simple_noderange_command(noderange, 'attributes/all'.format(noderange),val[1],val[0]) + except: + sys.stderr.write('Error: {0} not a valid expression\n'.format(str (args[1:]))) + exitcode = 1 + sys.exit(exitcode) + else: + requestargs = args[1:] + +# Lists all attributes +if len(args) > 0: + seenattributes = set([]) + for res in session.read('/noderange/{0}/attributes/{1}'.format(noderange,showtype)): + if 'error' in res: + print "found error" + sys.stderr.write(res['error'] + '\n') + exitcode = 1 + continue + for node in res['databynode']: + for attr in res['databynode'][node]: + seenattributes.add(attr) + currattr = res['databynode'][node][attr] + if requestargs is None or attrrequested(attr, args[1:], seenattributes): + if 'value' in currattr: + if currattr['value'] is not None: + attrout = '{0}: {1}: {2}'.format( + node, attr, currattr['value']) + else: + attrout = '{0}: {1}:'.format(node, attr) + elif 'isset' in currattr: + if currattr['isset']: + attrout = '{0}: {1}: ********'.format(node, attr) + else: + attrout = '{0}: {1}:'.format(node, attr) + elif 'broken' in currattr: + attrout = '{0}: {1}: *ERROR* BROKEN EXPRESSION: ' \ + '{2}'.format(node, attr, + currattr['broken']) + elif isinstance(currattr, list) or isinstance(currattr, tuple): + attrout = '{0}: {1}: {2}'.format(node, attr, ', '.join(map(str, currattr))) + elif isinstance(currattr, dict): + dictout = [] + for k,v in currattr.items: + dictout.append("{0}={1}".format(k,v)) + attrout = '{0}: {1}: {2}'.format(node, attr, ', '.join(map(str, dictout))) + else: + print ("CODE ERROR" + repr(attr)) + + if options.blame or 'broken' in currattr: + blamedata = [] + if 'inheritedfrom' in currattr: + blamedata.append('inherited from group {0}'.format( + currattr['inheritedfrom'] + )) + if 'expression' in currattr: + blamedata.append( + 'derived from expression "{0}"'.format( + currattr['expression'])) + if blamedata: + attrout += ' (' + ', '.join(blamedata) + ')' + print attrout + + if not exitcode: + if requestargs: + for attr in args[1:]: + if attr not in seenattributes: + sys.stderr.write('Error: {0} not a valid attribute\n'.format(attr)) + exitcode = 1 +else: + for res in session.read(nodelist): + if 'error' in res: + sys.stderr.write(res['error'] + '\n') + exitcode = 1 + else: + print res['item']['href'].replace('/', '') +sys.exit(exitcode) \ No newline at end of file diff --git a/confluent_client/bin/nodeboot.py b/confluent_client/bin/nodeboot old mode 100644 new mode 100755 similarity index 100% rename from confluent_client/bin/nodeboot.py rename to confluent_client/bin/nodeboot diff --git a/confluent_client/bin/nodeconsole b/confluent_client/bin/nodeconsole old mode 100644 new mode 100755 diff --git a/confluent_client/bin/nodeeventlog b/confluent_client/bin/nodeeventlog old mode 100644 new mode 100755 index e95f31f1..fc7a68a1 --- a/confluent_client/bin/nodeeventlog +++ b/confluent_client/bin/nodeeventlog @@ -1,7 +1,7 @@ #!/usr/bin/python # vim: tabstop=4 shiftwidth=4 softtabstop=4 -# Copyright 2015-2016 Lenovo +# Copyright 2015-2017 Lenovo # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ # limitations under the License. from datetime import datetime as dt +import optparse import os import sys @@ -26,13 +27,13 @@ if path.startswith('/opt'): import confluent.client as client - +argparser = optparse.OptionParser( + usage="Usage: %prog [options] noderange (clear)") +(options, args) = argparser.parse_args() try: - noderange = sys.argv[1] + noderange = args[0] except IndexError: - sys.stderr.write( - 'Usage: {0} [clear]\n'.format( - sys.argv[0])) + argparser.print_help() sys.exit(1) deletemode = False diff --git a/confluent_client/bin/nodefirmware b/confluent_client/bin/nodefirmware old mode 100644 new mode 100755 index 356d3363..1b51d6ec --- a/confluent_client/bin/nodefirmware +++ b/confluent_client/bin/nodefirmware @@ -1,7 +1,7 @@ #!/usr/bin/python # vim: tabstop=4 shiftwidth=4 softtabstop=4 -# Copyright 2016 Lenovo +# 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. @@ -15,6 +15,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +import optparse import os import sys path = os.path.dirname(os.path.realpath(__file__)) @@ -57,12 +58,12 @@ def printfirm(node, prefix, data): print('{0}: {1}: {2}'.format(node, prefix, version)) +argparser = optparse.OptionParser(usage="Usage: %prog ") +(options, args) = argparser.parse_args() try: - noderange = sys.argv[1] + noderange = args[0] except IndexError: - sys.stderr.write( - 'Usage: {0} \n'.format( - sys.argv[0])) + argparser.print_help() sys.exit(1) try: session = client.Command() diff --git a/confluent_client/bin/nodehealth b/confluent_client/bin/nodehealth index 75a7c6b0..294a73b4 100755 --- a/confluent_client/bin/nodehealth +++ b/confluent_client/bin/nodehealth @@ -1,7 +1,7 @@ #!/usr/bin/env python # vim: tabstop=4 shiftwidth=4 softtabstop=4 -# Copyright 2015 Lenovo +# Copyright 2015-2017 Lenovo # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ # limitations under the License. import codecs +import optparse import os import sys @@ -28,10 +29,12 @@ import confluent.client as client sys.stdout = codecs.getwriter('utf8')(sys.stdout) +argparser = optparse.OptionParser(usage="Usage: %prog ") +(options, args) = argparser.parse_args() try: - noderange = sys.argv[1] + noderange = args[0] except IndexError: - sys.stderr.write('Usage: {0} \n'.format(sys.argv[0])) + argparser.print_help() sys.exit(1) diff --git a/confluent_client/bin/nodeidentify b/confluent_client/bin/nodeidentify index 956827ce..17c397d9 100755 --- a/confluent_client/bin/nodeidentify +++ b/confluent_client/bin/nodeidentify @@ -1,7 +1,7 @@ #!/usr/bin/env python # vim: tabstop=4 shiftwidth=4 softtabstop=4 -# Copyright 2015 Lenovo +# Copyright 2015-2017 Lenovo # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -15,6 +15,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +import optparse import os import sys @@ -25,10 +26,12 @@ if path.startswith('/opt'): import confluent.client as client +argparser = optparse.OptionParser(usage="Usage: %prog [on|off]") +(options, args) = argparser.parse_args() try: - noderange = sys.argv[1] + noderange = args[0] except IndexError: - sys.stderr.write('Usage: {0} [on|off]\n'.format(sys.argv[0])) + argparser.print_help() sys.exit(1) identifystate = None diff --git a/confluent_client/bin/nodeinventory b/confluent_client/bin/nodeinventory old mode 100644 new mode 100755 index 8fc9caec..83a44bd7 --- a/confluent_client/bin/nodeinventory +++ b/confluent_client/bin/nodeinventory @@ -1,7 +1,7 @@ #!/usr/bin/python # vim: tabstop=4 shiftwidth=4 softtabstop=4 -# Copyright 2016 Lenovo +# 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. @@ -15,6 +15,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +import optparse import os import sys path = os.path.dirname(os.path.realpath(__file__)) @@ -69,12 +70,12 @@ def printerror(res, node=None): exitcode = 1 +argparser = optparse.OptionParser(usage="Usage: %prog ") +(options, args) = argparser.parse_args() try: noderange = sys.argv[1] except IndexError: - sys.stderr.write( - 'Usage: {0} \n'.format( - sys.argv[0])) + argparser.print_help() sys.exit(1) try: session = client.Command() diff --git a/confluent_client/bin/nodelist b/confluent_client/bin/nodelist old mode 100644 new mode 100755 index 14b3fc87..92ea8320 --- a/confluent_client/bin/nodelist +++ b/confluent_client/bin/nodelist @@ -1,7 +1,7 @@ #!/usr/bin/env python # vim: tabstop=4 shiftwidth=4 softtabstop=4 -# Copyright 2015 Lenovo +# Copyright 2015-2017 Lenovo # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -42,7 +42,7 @@ def attrrequested(attr, attrlist, seenattributes): return True return False argparser = optparse.OptionParser( - usage="Usage: %prog [options] noderange [list of attributes") + usage="Usage: %prog [options] noderange [list of attributes]") argparser.add_option('-b', '--blame', action='store_true', help='Show information about how attributes inherited') (options, args) = argparser.parse_args() @@ -76,7 +76,21 @@ if len(args) > 1: attrout = '{0}: {1}: ********'.format(node, attr) else: attrout = '{0}: {1}:'.format(node, attr) - if options.blame: + elif 'broken' in currattr: + attrout = '{0}: {1}: *ERROR* BROKEN EXPRESSION: ' \ + '{2}'.format(node, attr, + currattr['broken']) + elif isinstance(currattr, list) or isinstance(currattr, tuple): + attrout = '{0}: {1}: {2}'.format(node, attr, ', '.join(map(str, currattr))) + elif isinstance(currattr, dict): + dictout = [] + for k, v in currattr.items: + dictout.append("{0}={1}".format(k, v)) + attrout = '{0}: {1}: {2}'.format(node, attr, ', '.join(map(str, dictout))) + else: + print ("CODE ERROR" + repr(attr)) + + if options.blame or 'broken' in currattr: blamedata = [] if 'inheritedfrom' in currattr: blamedata.append('inherited from group {0}'.format( diff --git a/confluent_client/bin/nodepower b/confluent_client/bin/nodepower index 7dbb8722..5dd0b007 100755 --- a/confluent_client/bin/nodepower +++ b/confluent_client/bin/nodepower @@ -1,7 +1,7 @@ #!/usr/bin/env python # vim: tabstop=4 shiftwidth=4 softtabstop=4 -# Copyright 2015 Lenovo +# Copyright 2015-2017 Lenovo # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -15,6 +15,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +import optparse import os import sys @@ -25,13 +26,14 @@ if path.startswith('/opt'): import confluent.client as client - +argparser = optparse.OptionParser( + usage="Usage: %prog [options] noderange " + "([status|on|off|shutdown|boot|reset])") +(options, args) = argparser.parse_args() try: - noderange = sys.argv[1] + noderange = args[0] except IndexError: - sys.stderr.write( - 'Usage: {0} ([status|on|off|shutdown|boot|reset]\n'.format( - sys.argv[0])) + argparser.print_help() sys.exit(1) setstate = None @@ -43,5 +45,6 @@ if len(sys.argv) > 2: session = client.Command() exitcode = 0 +session.add_precede_key('oldstate') sys.exit( session.simple_noderange_command(noderange, '/power/state', setstate)) \ No newline at end of file diff --git a/confluent_client/bin/noderun b/confluent_client/bin/noderun new file mode 100755 index 00000000..dbebd4b2 --- /dev/null +++ b/confluent_client/bin/noderun @@ -0,0 +1,86 @@ +#!/usr/bin/env python +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2016-2017 Lenovo +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import optparse +import os +import select +import shlex +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 + + +argparser = optparse.OptionParser( + usage="Usage: %prog node commandexpression", + epilog="Expressions are the same as in attributes, e.g. " + "'ipmitool -H {hardwaremanagement.manager}' will be expanded.") +argparser.disable_interspersed_args() +(options, args) = argparser.parse_args() +if len(args) < 2: + argparser.print_help() + sys.exit(1) +c = client.Command() +cmdstr = " ".join(args[1:]) + +nodeforpopen = {} +popens = [] +for exp in c.create('/noderange/{0}/attributes/expression'.format(args[0]), + {'expression': cmdstr}): + ex = exp['databynode'] + for node in ex: + cmd = ex[node]['value'].encode('utf-8') + cmdv = shlex.split(cmd) + nopen = subprocess.Popen( + cmdv, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + popens.append(nopen) + nodeforpopen[nopen] = node + +all = set([]) +pipedesc = {} +exitcode = 0 +for pop in popens: + node = nodeforpopen[pop] + pipedesc[pop.stdout] = { 'node': node, 'popen': pop, 'type': 'stdout'} + pipedesc[pop.stderr] = {'node': node, 'popen': pop, 'type': 'stderr'} + all.add(pop.stdout) + all.add(pop.stderr) +rdy, _, _ = select.select(all, [], [], 10) +while all and rdy: + for r in rdy: + data = r.readline() + desc = pipedesc[r] + if data: + node = desc['node'] + if desc['type'] == 'stdout': + sys.stdout.write('{0}: {1}'.format(node,data)) + else: + sys.stderr.write('{0}: {1}'.format(node, data)) + else: + pop = desc['popen'] + ret = pop.poll() + if ret is not None: + exitcode = exitcode | ret + all.discard(r) + if all: + rdy, _, _ = select.select(all, [], [], 10) +sys.exit(exitcode) \ No newline at end of file diff --git a/confluent_client/bin/nodesensors b/confluent_client/bin/nodesensors index a086f4d3..3c9827a3 100755 --- a/confluent_client/bin/nodesensors +++ b/confluent_client/bin/nodesensors @@ -1,7 +1,7 @@ #!/usr/bin/env python # vim: tabstop=4 shiftwidth=4 softtabstop=4 -# Copyright 2015 Lenovo +# Copyright 2015-2017 Lenovo # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -40,7 +40,7 @@ sensorcollections = { argparser = optparse.OptionParser( - usage="Usage: %prog [options] noderange [sensor(s)") + usage="Usage: %prog [options] noderange ([sensor(s)])") argparser.add_option('-i', '--interval', type='float', help='Interval to do repeated samples over') argparser.add_option('-n', '--numreadings', type='int', @@ -60,7 +60,7 @@ if options.numreadings: try: noderange = args[0] except IndexError: - argparser.print_usage() + argparser.print_help() sys.exit(1) sensors = [] for sensorgroup in args[1:]: diff --git a/confluent_client/bin/nodesetboot b/confluent_client/bin/nodesetboot index 9a26e1bf..65f8aaeb 100755 --- a/confluent_client/bin/nodesetboot +++ b/confluent_client/bin/nodesetboot @@ -26,7 +26,8 @@ if path.startswith('/opt'): import confluent.client as client -argparser = optparse.OptionParser() +argparser = optparse.OptionParser( + usage='Usage: %prog [options] noderange [default|cd|network|setup|hd]') argparser.add_option('-b', '--bios', dest='biosmode', action='store_true', default=False, help='Request BIOS style boot (rather than UEFI)') @@ -34,15 +35,16 @@ argparser.add_option('-p', '--persist', dest='persist', action='store_true', default=False, help='Request the boot device be persistent rather than ' 'one time') +argparser.add_option('-u', '--uefi', dest='uefi', action='store_true', + default=True, + help='Request UEFI style boot (rather than BIOS)') (options, args) = argparser.parse_args() try: noderange = args[0] except IndexError: - sys.stderr.write( - 'Usage: {0} [default|cd|network|setup|hd]\n'.format( - sys.argv[0])) + argparser.print_help() sys.exit(1) bootdev = None if len(sys.argv) > 2: diff --git a/confluent_client/confluent/client.py b/confluent_client/confluent/client.py index 38ff2bbf..5d213003 100644 --- a/confluent_client/confluent/client.py +++ b/confluent_client/confluent/client.py @@ -44,6 +44,7 @@ def _parseserver(string): class Command(object): def __init__(self, server=None): + self._prevkeyname = None self.connection = None if server is None: if 'CONFLUENT_HOST' in os.environ: @@ -74,6 +75,9 @@ class Command(object): if authdata['authpassed'] == 1: self.authenticated = True + def add_precede_key(self, keyname): + self._prevkeyname = keyname + def handle_results(self, ikey, rc, res): if 'error' in res: sys.stderr.write('Error: {0}\n'.format(res['error'])) @@ -93,7 +97,17 @@ class Command(object): else: rc |= 1 elif ikey in res[node]: - print('{0}: {1}'.format(node, res[node][ikey]['value'])) + if 'value' in res[node][ikey]: + val = res[node][ikey]['value'] + elif 'isset' in res[node][ikey]: + val = '********' if res[node][ikey] else '' + else: + val = repr(res[node][ikey]) + if self._prevkeyname and self._prevkeyname in res[node]: + print('{0}: {2}->{1}'.format( + node, val, res[node][self._prevkeyname]['value'])) + else: + print('{0}: {1}'.format(node, val)) return rc def simple_noderange_command(self, noderange, resource, input=None, @@ -223,7 +237,12 @@ def send_request(operation, path, server, parameters=None): tlvdata.send(server, payload) result = tlvdata.recv(server) while '_requestdone' not in result: - yield result + try: + yield result + except GeneratorExit: + while '_requestdone' not in result: + result = tlvdata.recv(server) + raise result = tlvdata.recv(server) diff --git a/confluent_client/doc/man/confetty.ronn b/confluent_client/doc/man/confetty.ronn new file mode 100644 index 00000000..b7c4758b --- /dev/null +++ b/confluent_client/doc/man/confetty.ronn @@ -0,0 +1,37 @@ +confetty(1) --- Interactive confluent client +================================================= + +## SYNOPSIS + +`confetty` + +## DESCRIPTION + +**confetty** launches an interactive CLI session to the +confluent service. It provides a filesystem-like +view of the confluent interface. It is intended to +be mostly an aid for developing client software, with +day to day administration generally being easier with +the various function specific commands. + +## COMMANDS + +The CLI may be navigated by shell commands and some other +commands. + +* `cd`: + Change the location within the tree +* `ls`: + List the elements within the current directory/tree +* `show` **ELEMENT**, `cat` **ELEMENT**: + Display the result of reading a specific element (by full or relative path) +* `unset` **ELEMENT** **ATTRIBUTE** + For an element with attributes, request to clear the value of the attribue +* `set` **ELEMENT** **ATTRIBUTE**=**VALUE** + Set the specified attribute to the given value +* `start` **ELEMENT** + Start a console session indicated by **ELEMENT** (e.g. /nodes/n1/console/session) +* `rm` **ELEMENT** + Request removal of an element. (e.g. rm events/hardware/log clears log from a node) + + diff --git a/confluent_client/doc/man/nodeattrib.ronn b/confluent_client/doc/man/nodeattrib.ronn new file mode 100644 index 00000000..dc330b0c --- /dev/null +++ b/confluent_client/doc/man/nodeattrib.ronn @@ -0,0 +1,75 @@ +nodeattrib(1) -- List or change confluent nodes attributes +========================================================= + +## SYNOPSIS + +`nodeattrib` `noderange` [ current | all ] +`nodeattrib` `noderange` [-b] [...] +`nodeattrib` `noderange` [ ...] +`nodeattrib` `noderange` [-c] [ ...] + +## DESCRIPTION + +**nodeattrib** queries the confluent server to get information about nodes. In +the simplest form, it simply takes the given noderange(5) and lists the +matching nodes, one line at a time. + +If a list of node attribute names are given, the value of those are also +displayed. If `-b` is specified, it will also display information on +how inherited and expression based attributes are defined. There is more +information on node attributes in nodeattributes(5) man page. +If `-c` is specified, this will set the nodeattribute to a null valid. +This is different from setting the value to an empty string. + +## OPTIONS + +* `-b`, `--blame`: + Annotate inherited and expression based attributes to show their base value. +* `-c`, `--clear`: + Clear given nodeattributes since '' is not the same as empty + +## EXAMPLES +* Listing matching nodes of a simple noderange: + `# nodeattrib n1-n2` + `n1`: console.method: ipmi + `n1`: hardwaremanagement.manager: 172.30.3.1 + `n2`: console.method: ipmi + `n2`: hardwaremanagement.manager: 172.30.3.2 + +* Getting an attribute of nodes matching a noderange: + `# nodeattrib n1,n2 hardwaremanagement.manager` + `n1: hardwaremanagement.manager: 172.30.3.1` + `n2: hardwaremanagement.manager: 172.30.3.2` + +* Getting a group of attributes while determining what group defines them: + `# nodeattrib n1,n2 hardwaremanagement --blame` + `n1: hardwaremanagement.manager: 172.30.3.1` + `n1: hardwaremanagement.method: ipmi (inherited from group everything)` + `n1: hardwaremanagement.switch: r8e1` + `n1: hardwaremanagement.switchport: 14` + `n2: hardwaremanagement.manager: 172.30.3.2` + `n2: hardwaremanagement.method: ipmi (inherited from group everything)` + `n2: hardwaremanagement.switch: r8e1` + `n2: hardwaremanagement.switchport: 2` + + * Listing matching nodes of a simple noderange that are set: + `# nodeattrib n1-n2 current` + `n1`: console.method: ipmi + `n1`: hardwaremanagement.manager: 172.30.3.1 + `n2`: console.method: ipmi + `n2`: hardwaremanagement.manager: 172.30.3.2 + + * Change attribute on nodes of a simple noderange: + `# nodeattrib n1-n2 console.method=serial` + `n1`: console.method: serial + `n1`: hardwaremanagement.manager: 172.30.3.1 + `n2`: console.method: serial + `n2`: hardwaremanagement.manager: 172.30.3.2 + + * Clear attribute on nodes of a simple noderange, if you want to retain the variable set the attribute to "": + `# nodeattrib n1-n2 -c console.method` + `# nodeattrib n1-n2 console.method` + Error: console.logging not a valid attribute + + + diff --git a/confluent_client/doc/man/nodeconsole.ronn b/confluent_client/doc/man/nodeconsole.ronn new file mode 100644 index 00000000..c7e3eb4d --- /dev/null +++ b/confluent_client/doc/man/nodeconsole.ronn @@ -0,0 +1,30 @@ +nodeconsole(1) -- Open a console to a confluent node +===================================================== + +## SYNOPSIS +`nodeconsole` `node` + +## DESCRIPTION + +**nodeconsole** opens an interactive console session to a given node. This is the +text or serial console of a system. Exiting is done by hitting `Ctrl-e`, then `c`, + then `.`. Note that console output by default is additionally logged to +`/var/log/confluent/consoles/`**NODENAME**. + +## ESCAPE SEQUENCE COMMANDS + +While connected to a console, a number of commands may be performed through escape +sequences. To begin an command escape sequence, hit `Ctrl-e`, then `c`. The next +keystroke will be interpreted as a command. The following commands are available. + +* `.`: + Exit the session and return to the command prompt +* `b`: + Send a break to the remote console when possible (some console plugins may not support this) +* `o`: + Request confluent to disconnect and reconnect to console. For example if there is suspicion + that the console has gone inoperable, but would work if reconnected. +* `?`: + Get a list of supported commands +* ``: + Abandon entering an escape sequence command diff --git a/confluent_client/setup.py.tmpl b/confluent_client/setup.py.tmpl index 9bcddfd3..f8768bd5 100644 --- a/confluent_client/setup.py.tmpl +++ b/confluent_client/setup.py.tmpl @@ -1,4 +1,7 @@ from setuptools import setup +import os + +scriptlist = ['bin/{0}'.format(d) for d in os.listdir('bin/')] setup( name='confluent_client', @@ -7,9 +10,6 @@ setup( author_email='jjohnson2@lenovo.com', url='http://xcat.sf.net/', packages=['confluent'], - scripts=['bin/confetty', 'bin/nodeconsole', 'bin/nodeeventlog', - 'bin/nodefirmware', 'bin/nodehealth', 'bin/nodeidentify', - 'bin/nodeinventory', 'bin/nodelist', 'bin/nodepower', - 'bin/nodesensors', 'bin/nodesetboot'], + scripts=scriptlist, data_files=[('/etc/profile.d', ['confluent_env.sh'])], ) diff --git a/confluent_server/bin/confluentdbutil b/confluent_server/bin/confluentdbutil new file mode 100755 index 00000000..e5acb419 --- /dev/null +++ b/confluent_server/bin/confluentdbutil @@ -0,0 +1,67 @@ +#!/usr/bin/env python +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2017 Lenovo +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +import optparse +import sys +import os +path = os.path.dirname(os.path.realpath(__file__)) +path = os.path.realpath(os.path.join(path, '..', 'lib', 'python')) +if path.startswith('/opt'): + # if installed into system path, do not muck with things + sys.path.append(path) +import confluent.config.configmanager as cfm +import confluent.config.conf as conf +import confluent.main as main + +argparser = optparse.OptionParser( + usage="Usage: %prog [options] [dump|restore] [path]") +argparser.add_option('-p', '--password', + help='Password to use to protect/unlock a protected dump') +argparser.add_option('-r', '--redact', action='store_true', + help='Redact potentially sensitive data rather than store') +argparser.add_option('-u', '--unprotected', action='store_true', + help='Specify that no password should be used to protect' + ' the key information. Fields will be encrypted, ' + 'but keys.json will contain unencrypted decryption' + ' keys that may be used to read the dump') +(options, args) = argparser.parse_args() +if len(args) != 2 or args[0] not in ('dump', 'restore'): + argparser.print_help() + sys.exit(1) +dumpdir = args[1] + + +if args[0] == 'restore': + pid = main.is_running() + if pid is not None: + print("Confluent is running, must shut down to restore db") + sys.exit(1) + cfm.restore_db_from_directory(dumpdir, options.password) +elif args[0] == 'dump': + if options.password is None and not (options.unprotected or options.redact): + print("Must indicate a password to protect or -u to opt opt of " + "secure value protection or -r to skip all protected data") + sys.exit(1) + os.umask(077) + main._initsecurity(conf.get_config()) + if not os.path.exists(dumpdir): + os.makedirs(dumpdir) + cfm.dump_db_to_directory(dumpdir, options.password, options.redact) + + + diff --git a/confluent_server/confluent/auth.py b/confluent_server/confluent/auth.py index 452cdc40..85fc1b14 100644 --- a/confluent_server/confluent/auth.py +++ b/confluent_server/confluent/auth.py @@ -37,6 +37,7 @@ _passcache = {} _passchecking = {} authworkers = None +authcleaner = None class Credentials(object): @@ -195,6 +196,13 @@ def check_user_passphrase(name, passphrase, element=None, tenant=False): #such a beast could be passed into pyghmi as a way for pyghmi to #magically get offload of the crypto functions without having #to explicitly get into the eventlet tpool game + global authworkers + global authcleaner + if authworkers is None: + authworkers = multiprocessing.Pool(processes=1) + else: + authcleaner.cancel() + authcleaner = eventlet.spawn_after(30, _clean_authworkers) crypted = eventlet.tpool.execute(_do_pbkdf, passphrase, salt) del _passchecking[(user, tenant)] eventlet.sleep(0.05) # either way, we want to stall so that client can't @@ -211,19 +219,16 @@ def _apply_pbkdf(passphrase, salt): lambda p, s: hmac.new(p, s, hashlib.sha256).digest()) +def _clean_authworkers(): + global authworkers + global authcleaner + authworkers = None + authcleaner = None + + def _do_pbkdf(passphrase, salt): # we must get it over to the authworkers pool or else get blocked in # compute. However, we do want to wait for result, so we have # one of the exceedingly rare sort of circumstances where 'apply' # actually makes sense - return authworkers.apply(_apply_pbkdf, [passphrase, salt]) - - -def init_auth(): - # have a some auth workers available. Keep them distinct from - # the general populace of workers to avoid unauthorized users - # starving out productive work - global authworkers - # for now we'll just have one auth worker and see if there is any - # demand for more. I personally doubt it. - authworkers = multiprocessing.Pool(processes=1) + return authworkers.apply(_apply_pbkdf, [passphrase, salt]) \ No newline at end of file diff --git a/confluent_server/confluent/config/configmanager.py b/confluent_server/confluent/config/configmanager.py index c5043ab9..4a36a97d 100644 --- a/confluent_server/confluent/config/configmanager.py +++ b/confluent_server/confluent/config/configmanager.py @@ -65,6 +65,7 @@ import anydbm as dbm import ast import base64 import confluent.config.attributes as allattributes +import confluent.config.conf as conf import confluent.log import confluent.util import confluent.exceptions as exc @@ -128,6 +129,18 @@ def _get_protected_key(keydict, password, paramname): raise exc.LockedCredentials("No available decryption key") +def _parse_key(keydata, password=None): + if keydata.startswith('*unencrypted:'): + return base64.b64decode(keydata[13:]) + elif password: + salt, iv, crypt, hmac = [base64.b64decode(x) + for x in keydata.split('!')] + privkey, integkey = _derive_keys(password, salt) + return decrypt_value([iv, crypt, hmac], privkey, integkey) + raise(exc.LockedCredentials( + "Passphrase protected secret requires password")) + + def _format_key(key, password=None): if password is not None: salt = os.urandom(32) @@ -707,6 +720,8 @@ class ConfigManager(object): :param uid: Custom identifier number if desired. Defaults to random. :param displayname: Optional long format name for UI consumption """ + if 'idmap' not in _cfgstore['main']: + _cfgstore['main']['idmap'] = {} if uid is None: uid = _generate_new_id() else: @@ -720,8 +735,6 @@ class ConfigManager(object): self._cfgstore['users'][name] = {'id': uid} if displayname is not None: self._cfgstore['users'][name]['displayname'] = displayname - if 'idmap' not in _cfgstore['main']: - _cfgstore['main']['idmap'] = {} _cfgstore['main']['idmap'][uid] = { 'tenant': self.tenant, 'username': name @@ -761,6 +774,14 @@ class ConfigManager(object): decrypt=self.decrypt) return nodeobj + def expand_attrib_expression(self, nodelist, expression): + if type(nodelist) in (unicode, str): + nodelist = (nodelist,) + for node in nodelist: + cfgobj = self._cfgstore['nodes'][node] + fmt = _ExpressionFormat(cfgobj, node) + yield (node, fmt.format(expression)) + def get_node_attributes(self, nodelist, attributes=(), decrypt=None): if decrypt is None: decrypt = self.decrypt @@ -1179,6 +1200,70 @@ class ConfigManager(object): self._bg_sync_to_file() #TODO: wait for synchronization to suceed/fail??) + def _load_from_json(self, jsondata): + """Load fresh configuration data from jsondata + + :param jsondata: String of jsondata + :return: + """ + dumpdata = json.loads(jsondata) + tmpconfig = {} + for confarea in _config_areas: + if confarea not in dumpdata: + continue + tmpconfig[confarea] = {} + for element in dumpdata[confarea]: + newelement = copy.deepcopy(dumpdata[confarea][element]) + for attribute in dumpdata[confarea][element]: + if newelement[attribute] == '*REDACTED*': + raise Exception( + "Unable to restore from redacted backup") + elif attribute == 'cryptpass': + passparts = newelement[attribute].split('!') + newelement[attribute] = tuple([base64.b64decode(x) + for x in passparts]) + elif 'cryptvalue' in newelement[attribute]: + bincrypt = newelement[attribute]['cryptvalue'] + bincrypt = tuple([base64.b64decode(x) + for x in bincrypt.split('!')]) + newelement[attribute]['cryptvalue'] = bincrypt + elif attribute in ('nodes', '_expressionkeys'): + # A group with nodes + # delete it and defer until nodes are being added + # which will implicitly fill this up + # Or _expressionkeys attribute, which will similarly + # be rebuilt + del newelement[attribute] + tmpconfig[confarea][element] = newelement + # We made it through above section without an exception, go ahead and + # replace + # Start by erasing the dbm files if present + for confarea in _config_areas: + try: + os.unlink(os.path.join(self._cfgdir, confarea)) + except OSError as e: + if e.errno == 2: + pass + # Now we have to iterate through each fixed up element, using the + # set attribute to flesh out inheritence and expressions + for confarea in _config_areas: + if confarea not in tmpconfig: + continue + if confarea == 'nodes': + self.set_node_attributes(tmpconfig[confarea], True) + elif confarea == 'nodegroups': + self.set_group_attributes(tmpconfig[confarea], True) + elif confarea == 'users': + for user in tmpconfig[confarea]: + uid = tmpconfig[confarea].get('id', None) + displayname = tmpconfig[confarea].get('displayname', None) + self.create_user(user, uid=uid, displayname=displayname) + if 'cryptpass' in tmpconfig[confarea][user]: + self._cfgstore['users'][user]['cryptpass'] = \ + tmpconfig[confarea][user]['cryptpass'] + _mark_dirtykey('users', user, self.tenant) + self._bg_sync_to_file() + def _dump_to_json(self, redact=None): """Dump the configuration in json form to output @@ -1336,28 +1421,80 @@ class ConfigManager(object): self._recalculate_expressions(cfgobj[key], formatter, node, changeset) + +def _restore_keys(jsond, password, newpassword=None): + # the jsond from the restored file, password (if any) used to protect + # the file, and newpassword to use, (also check the service.cfg file) + global _masterkey + global _masterintegritykey + keydata = json.loads(jsond) + cryptkey = _parse_key(keydata['cryptkey'], password) + integritykey = _parse_key(keydata['integritykey'], password) + conf.init_config() + cfg = conf.get_config() + if cfg.has_option('security', 'externalcfgkey'): + keyfilename = cfg.get('security', 'externalcfgkey') + with open(keyfilename, 'r') as keyfile: + newpassword = keyfile.read() + set_global('master_privacy_key', _format_key(cryptkey, + password=newpassword)) + set_global('master_integrity_key', _format_key(integritykey, + password=newpassword)) + _masterkey = cryptkey + _masterintegritykey = integritykey + ConfigManager.wait_for_sync() + # At this point, we should have the key situation all sorted + + def _dump_keys(password): if _masterkey is None or _masterintegritykey is None: init_masterkey() cryptkey = _format_key(_masterkey, password=password) - cryptkey = '!'.join(map(base64.b64encode, cryptkey['passphraseprotected'])) + if 'passphraseprotected' in cryptkey: + cryptkey = '!'.join(map(base64.b64encode, + cryptkey['passphraseprotected'])) + else: + cryptkey = '*unencrypted:{0}'.format(base64.b64encode( + cryptkey['unencryptedvalue'])) integritykey = _format_key(_masterintegritykey, password=password) - integritykey = '!'.join(map(base64.b64encode, integritykey['passphraseprotected'])) + if 'passphraseprotected' in integritykey: + integritykey = '!'.join(map(base64.b64encode, + integritykey['passphraseprotected'])) + else: + integritykey = '*unencrypted:{0}'.format(base64.b64encode( + integritykey['unencryptedvalue'])) return json.dumps({'cryptkey': cryptkey, 'integritykey': integritykey}, sort_keys=True, indent=4, separators=(',', ': ')) +def restore_db_from_directory(location, password): + try: + with open(os.path.join(location, 'keys.json'), 'r') as cfgfile: + keydata = cfgfile.read() + json.loads(keydata) + _restore_keys(keydata, password) + except IOError as e: + if e.errno == 2: + raise Exception("Cannot restore without keys, this may be a " + "redacted dump") + with open(os.path.join(location, 'main.json'), 'r') as cfgfile: + cfgdata = cfgfile.read() + ConfigManager(tenant=None)._load_from_json(cfgdata) + + def dump_db_to_directory(location, password, redact=None): - with open(os.path.join(location, 'keys.json'), 'w') as cfgfile: - cfgfile.write(_dump_keys(password)) - cfgfile.write('\n') + if not redact: + with open(os.path.join(location, 'keys.json'), 'w') as cfgfile: + cfgfile.write(_dump_keys(password)) + cfgfile.write('\n') with open(os.path.join(location, 'main.json'), 'w') as cfgfile: cfgfile.write(ConfigManager(tenant=None)._dump_to_json(redact=redact)) cfgfile.write('\n') try: for tenant in os.listdir( os.path.join(ConfigManager._cfgdir, '/tenants/')): - with open(os.path.join(location, tenant + '.json'), 'w') as cfgfile: + with open(os.path.join(location, 'tenants', tenant, + 'main.json'), 'w') as cfgfile: cfgfile.write(ConfigManager(tenant=tenant)._dump_to_json( redact=redact)) cfgfile.write('\n') diff --git a/confluent_server/confluent/core.py b/confluent_server/confluent/core.py index 9aa0adaf..608e8206 100644 --- a/confluent_server/confluent/core.py +++ b/confluent_server/confluent/core.py @@ -122,6 +122,7 @@ def _init_core(): 'attributes': { 'all': PluginRoute({'handler': 'attributes'}), 'current': PluginRoute({'handler': 'attributes'}), + 'expression': PluginRoute({'handler': 'attributes'}), }, 'boot': { 'nextdevice': PluginRoute({ diff --git a/confluent_server/confluent/httpapi.py b/confluent_server/confluent/httpapi.py index 4551eb0f..9e7ed2df 100644 --- a/confluent_server/confluent/httpapi.py +++ b/confluent_server/confluent/httpapi.py @@ -35,6 +35,7 @@ import eventlet.greenthread import greenlet import json import socket +import sys import traceback import time import urlparse @@ -233,6 +234,17 @@ def _csrf_valid(env, session): # oblige the request and apply a new token to the # session session['csrftoken'] = util.randomstring(32) + elif 'HTTP_REFERER' in env: + # If there is a referrer, make sure it stays consistent + # across the session. A change in referer is a bad thing + try: + referer = env['HTTP_REFERER'].split('/')[2] + except IndexError: + return False + if 'validreferer' not in session: + session['validreferer'] = referer + elif session['validreferer'] != referer: + return False return True # The session has CSRF protection enabled, only mark valid if # the client has provided an auth token and that token matches the @@ -257,15 +269,15 @@ def _authorize_request(env, operation): sessionid = cc['confluentsessionid'].value sessid = sessionid if sessionid in httpsessions: - if env['PATH_INFO'] == '/sessions/current/logout': - targets = [] - for mythread in httpsessions[sessionid]['inflight']: - targets.append(mythread) - for mythread in targets: - eventlet.greenthread.kill(mythread) - del httpsessions[sessionid] - return ('logout',) if _csrf_valid(env, httpsessions[sessionid]): + if env['PATH_INFO'] == '/sessions/current/logout': + targets = [] + for mythread in httpsessions[sessionid]['inflight']: + targets.append(mythread) + for mythread in targets: + eventlet.greenthread.kill(mythread) + del httpsessions[sessionid] + return ('logout',) httpsessions[sessionid]['expiry'] = time.time() + 90 name = httpsessions[sessionid]['name'] authdata = auth.authorize( @@ -273,6 +285,12 @@ def _authorize_request(env, operation): skipuserobj=httpsessions[sessionid]['skipuserobject']) if (not authdata) and 'HTTP_AUTHORIZATION' in env: if env['PATH_INFO'] == '/sessions/current/logout': + if 'HTTP_REFERER' in env: + # note that this doesn't actually do harm + # otherwise, but this way do not give appearance + # of something having a side effect if it has the smell + # of a CSRF + return {'code': 401} return ('logout',) name, passphrase = base64.b64decode( env['HTTP_AUTHORIZATION'].replace('Basic ', '')).split(':', 1) @@ -369,7 +387,8 @@ def resourcehandler_backend(env, start_response): """Function to handle new wsgi requests """ mimetype, extension = _pick_mimetype(env) - headers = [('Content-Type', mimetype), ('Cache-Control', 'no-cache'), + headers = [('Content-Type', mimetype), ('Cache-Control', 'no-store'), + ('Pragma', 'no-cache'), ('X-Content-Type-Options', 'nosniff'), ('Content-Security-Policy', "default-src 'self'"), ('X-XSS-Protection', '1'), ('X-Frame-Options', 'deny'), @@ -723,9 +742,20 @@ def serve(bind_host, bind_port): #but deps are simpler without flup #also, the potential for direct http can be handy #todo remains unix domain socket for even http - eventlet.wsgi.server( - eventlet.listen((bind_host, bind_port, 0, 0), family=socket.AF_INET6), - resourcehandler, log=False, log_output=False, debug=False) + sock = None + while not sock: + try: + sock = eventlet.listen( + (bind_host, bind_port, 0, 0), family=socket.AF_INET6) + except socket.error as e: + if e.errno != 98: + raise + sys.stderr.write( + '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) class HttpApi(object): diff --git a/confluent_server/confluent/main.py b/confluent_server/confluent/main.py index b8536eec..cd26fba5 100644 --- a/confluent_server/confluent/main.py +++ b/confluent_server/confluent/main.py @@ -1,7 +1,7 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 # Copyright 2014 IBM Corporation -# Copyright 2015 Lenovo +# Copyright 2015-2017 Lenovo # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -89,15 +89,41 @@ def _updatepidfile(): pidfile.close() +def is_running(): + # Utility function for utilities to check if confluent is running + try: + pidfile = open('/var/run/confluent/pid', 'r+') + fcntl.flock(pidfile, fcntl.LOCK_SH) + pid = pidfile.read() + if pid != '': + try: + os.kill(int(pid), 0) + return pid + except OSError: + # There is no process running by that pid, must be stale + pass + fcntl.flock(pidfile, fcntl.LOCK_UN) + pidfile.close() + except IOError: + pass + return None + + def _checkpidfile(): try: pidfile = open('/var/run/confluent/pid', 'r+') fcntl.flock(pidfile, fcntl.LOCK_EX) pid = pidfile.read() if pid != '': - print ('/var/run/confluent/pid exists and indicates %s is still ' - 'running' % pid) - sys.exit(1) + try: + os.kill(int(pid), 0) + print ('/var/run/confluent/pid exists and indicates %s is still ' + 'running' % pid) + sys.exit(1) + except OSError: + # There is no process running by that pid, must be stale + pass + pidfile.seek(0) pidfile.write(str(os.getpid())) fcntl.flock(pidfile, fcntl.LOCK_UN) pidfile.close() @@ -196,13 +222,14 @@ def run(): _daemonize() if havefcntl: _updatepidfile() - auth.init_auth() signal.signal(signal.SIGINT, terminate) signal.signal(signal.SIGTERM, terminate) - #TODO(jbjohnso): eventlet has a bug about unix domain sockets, this code - #works with bugs fixed if dbgif: oumask = os.umask(0077) + try: + os.remove('/var/run/confluent/dbg.sock') + except OSError: + pass # We are not expecting the file to exist dbgsock = eventlet.listen("/var/run/confluent/dbg.sock", family=socket.AF_UNIX) eventlet.spawn_n(backdoor.backdoor_server, dbgsock) diff --git a/confluent_server/confluent/messages.py b/confluent_server/confluent/messages.py index 4f43abe1..fc4a7df2 100644 --- a/confluent_server/confluent/messages.py +++ b/confluent_server/confluent/messages.py @@ -797,6 +797,11 @@ class PowerState(ConfluentChoiceMessage): ]) keyname = 'state' + def __init__(self, node, state, oldstate=None): + super(PowerState, self).__init__(node, state) + if oldstate is not None: + self.kvpairs[node]['oldstate'] = {'value': oldstate} + class BMCReset(ConfluentChoiceMessage): valid_values = set([ diff --git a/confluent_server/confluent/plugins/configuration/attributes.py b/confluent_server/confluent/plugins/configuration/attributes.py index 9ee0f149..10aa3307 100644 --- a/confluent_server/confluent/plugins/configuration/attributes.py +++ b/confluent_server/confluent/plugins/configuration/attributes.py @@ -152,6 +152,20 @@ def update_nodegroup(group, element, configmanager, inputdata): return retrieve_nodegroup(group, element, configmanager, inputdata) +def _expand_expression(nodes, configmanager, inputdata): + expression = inputdata.get_attributes(list(nodes)[0]) + if type(expression) is dict: + expression = expression['expression'] + if type(expression) is dict: + expression = expression['expression'] + for expanded in configmanager.expand_attrib_expression(nodes, expression): + yield msg.KeyValueData({'value': expanded[1]}, expanded[0]) + + +def create(nodes, element, configmanager, inputdata): + if nodes is not None and element[-1] == 'expression': + return _expand_expression(nodes, configmanager, inputdata) + def update_nodes(nodes, element, configmanager, inputdata): updatedict = {} for node in nodes: diff --git a/confluent_server/confluent/plugins/hardwaremanagement/ipmi.py b/confluent_server/confluent/plugins/hardwaremanagement/ipmi.py index 64fbe900..ce92b869 100644 --- a/confluent_server/confluent/plugins/hardwaremanagement/ipmi.py +++ b/confluent_server/confluent/plugins/hardwaremanagement/ipmi.py @@ -344,8 +344,8 @@ class IpmiHandler(object): except socket.gaierror as ge: if ge[0] == -2: raise exc.TargetEndpointUnreachable(ge[1]) + raise self.ipmicmd = persistent_ipmicmds[(node, tenant)] - self.ipmicmd.setup_confluent_keyhandler() bootdevices = { 'optical': 'cd' @@ -356,7 +356,9 @@ class IpmiHandler(object): self.broken = True self.error = response['error'] else: + self.ipmicmd = ipmicmd self.loggedin = True + self.ipmicmd.setup_confluent_keyhandler() self._logevt.set() def handle_request(self): @@ -793,10 +795,22 @@ class IpmiHandler(object): return elif 'update' == self.op: powerstate = self.inputdata.powerstate(self.node) + oldpower = None + if powerstate == 'boot': + oldpower = self.ipmicmd.get_power() + if 'powerstate' in oldpower: + oldpower = oldpower['powerstate'] self.ipmicmd.set_power(powerstate, wait=30) - power = self.ipmicmd.get_power() + if powerstate == 'boot' and oldpower == 'on': + power = {'powerstate': 'reset'} + else: + power = self.ipmicmd.get_power() + if powerstate == 'reset' and power['powerstate'] == 'on': + power['powerstate'] = 'reset' + self.output.put(msg.PowerState(node=self.node, - state=power['powerstate'])) + state=power['powerstate'], + oldstate=oldpower)) return def handle_reset(self): diff --git a/confluent_server/confluent/sockapi.py b/confluent_server/confluent/sockapi.py index 874372d0..679c6140 100644 --- a/confluent_server/confluent/sockapi.py +++ b/confluent_server/confluent/sockapi.py @@ -170,7 +170,8 @@ def process_request(connection, request, cfm, authdata, authname, skipauth): auditlog.log(auditmsg) try: if operation == 'start': - return start_term(authname, cfm, connection, params, path) + return start_term(authname, cfm, connection, params, path, + authdata, skipauth) elif operation == 'shutdown': configmanager.ConfigManager.shutdown() else: @@ -187,7 +188,7 @@ def process_request(connection, request, cfm, authdata, authname, skipauth): return -def start_term(authname, cfm, connection, params, path): +def start_term(authname, cfm, connection, params, path, authdata, skipauth): elems = path.split('/') if len(elems) < 4 or elems[1] != 'nodes': raise exc.InvalidArgumentException('Invalid path {0}'.format(path)) @@ -233,7 +234,9 @@ def start_term(authname, cfm, connection, params, path): consession.reopen() continue else: - raise Exception("TODO") + process_request(connection, data, cfm, authdata, authname, + skipauth) + continue if not data: consession.destroy() return @@ -244,7 +247,16 @@ def _tlshandler(bind_host, bind_port): plainsocket = socket.socket(socket.AF_INET6) plainsocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) plainsocket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) - plainsocket.bind((bind_host, bind_port, 0, 0)) + bound = False + while not bound: + try: + plainsocket.bind((bind_host, bind_port, 0, 0)) + bound = True + except socket.error as e: + if e.errno != 98: + raise + sys.stderr.write('TLS Socket in use, retrying in 1 second\n') + eventlet.sleep(1) plainsocket.listen(5) while (1): # TODO: exithook cnn, addr = plainsocket.accept() diff --git a/confluent_server/confluent_server.spec.tmpl b/confluent_server/confluent_server.spec.tmpl index 315daea9..6a8c7aec 100644 --- a/confluent_server/confluent_server.spec.tmpl +++ b/confluent_server/confluent_server.spec.tmpl @@ -33,6 +33,9 @@ done grep -v confluent/__init__.py INSTALLED_FILES.bare > INSTALLED_FILES cat INSTALLED_FILES +%post +if [ -x /usr/bin/systemctl ]; then /usr/bin/systemctl try-restart confluent; fi + %clean rm -rf $RPM_BUILD_ROOT diff --git a/confluent_server/setup.py.tmpl b/confluent_server/setup.py.tmpl index 9bd1d2ac..fdbbee84 100644 --- a/confluent_server/setup.py.tmpl +++ b/confluent_server/setup.py.tmpl @@ -5,7 +5,7 @@ setup( name='confluent_server', version='#VERSION#', author='Jarrod Johnson', - author_email='jbjohnso@us.ibm.com', + author_email='jjohnson2@lenovo.com', url='http://xcat.sf.net/', description='confluent systems management server', packages=['confluent', 'confluent/config', 'confluent/interface', @@ -14,7 +14,7 @@ setup( 'confluent/plugins/configuration/'], install_requires=['paramiko', 'pycrypto>=2.6', 'confluent_client>=0.1.0', 'eventlet', 'pyghmi>=0.6.5'], - scripts=['bin/confluent'], + scripts=['bin/confluent', 'bin/confluentdbutil'], data_files=[('/etc/init.d', ['sysvinit/confluent']), ('/usr/lib/systemd/system', ['systemd/confluent.service']), ('/opt/confluent/lib/python/confluent/plugins/console/', [])],