From eca18a451bbd85e1e6272c854c703613bad06d58 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Fri, 27 Jan 2017 10:09:15 -0500 Subject: [PATCH 01/14] Fix no password backup If password=None was supplied, it would fail. Now accomodate unprotected keys. --- .../confluent/config/configmanager.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/confluent_server/confluent/config/configmanager.py b/confluent_server/confluent/config/configmanager.py index 63d8aa4a..4506e5e9 100644 --- a/confluent_server/confluent/config/configmanager.py +++ b/confluent_server/confluent/config/configmanager.py @@ -1344,13 +1344,24 @@ class ConfigManager(object): self._recalculate_expressions(cfgobj[key], formatter, node, changeset) + 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=(',', ': ')) From d2e8fa652139321ef086c93ec0bfb13c053e5d34 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Fri, 27 Jan 2017 11:20:55 -0500 Subject: [PATCH 02/14] Start work on a db restore Start by parsing the previously dumped key data, since the key data requires special handling. --- .../confluent/config/configmanager.py | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/confluent_server/confluent/config/configmanager.py b/confluent_server/confluent/config/configmanager.py index 4506e5e9..8ce69753 100644 --- a/confluent_server/confluent/config/configmanager.py +++ b/confluent_server/confluent/config/configmanager.py @@ -128,6 +128,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) @@ -1345,6 +1357,18 @@ class ConfigManager(object): changeset) +def _restore_keys(jsond, password, newpassword): + # 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) + _masterkey = cryptkey + _masterintegritykey = integritykey + + def _dump_keys(password): if _masterkey is None or _masterintegritykey is None: init_masterkey() From d07e5a08c901fb55996e40e4f35615c07a35084b Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Fri, 27 Jan 2017 14:17:29 -0500 Subject: [PATCH 03/14] Provide for a terminal session to process normal commands If it is not one of the 'special' terminal ones, assume it's a normal one. Recurse into the process request to handle it as a separate request. --- confluent_server/confluent/sockapi.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/confluent_server/confluent/sockapi.py b/confluent_server/confluent/sockapi.py index 8205d7e3..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 From 85205867b3ace1de030d76dbb9ca0f852bdf7487 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Fri, 27 Jan 2017 16:50:03 -0500 Subject: [PATCH 04/14] Flesh out the restoration of the master keys With this commit, the key portion of import should be complete. --- .../confluent/config/configmanager.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/confluent_server/confluent/config/configmanager.py b/confluent_server/confluent/config/configmanager.py index 8ce69753..e00d79e1 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 @@ -1357,7 +1358,7 @@ class ConfigManager(object): changeset) -def _restore_keys(jsond, password, newpassword): +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 @@ -1365,8 +1366,20 @@ def _restore_keys(jsond, password, newpassword): 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): From f1867f900df110f6316f84862e7fcd01b591b492 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Mon, 30 Jan 2017 15:31:36 -0500 Subject: [PATCH 05/14] Implement restore from db This provides at least restore for the primary tenant (the only tenant currently used by anyone) --- .../confluent/config/configmanager.py | 79 ++++++++++++++++++- 1 file changed, 76 insertions(+), 3 deletions(-) diff --git a/confluent_server/confluent/config/configmanager.py b/confluent_server/confluent/config/configmanager.py index e00d79e1..7bf757be 100644 --- a/confluent_server/confluent/config/configmanager.py +++ b/confluent_server/confluent/config/configmanager.py @@ -720,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: @@ -733,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 @@ -1200,6 +1200,68 @@ 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 tmpconfig: + 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 @@ -1403,6 +1465,16 @@ def _dump_keys(password): sort_keys=True, indent=4, separators=(',', ': ')) +def restore_db_from_directory(location, password): + with open(os.path.join(location, 'keys.json'), 'r') as cfgfile: + keydata = cfgfile.read() + json.loads(keydata) + _restore_keys(keydata, password) + 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)) @@ -1413,7 +1485,8 @@ def dump_db_to_directory(location, password, redact=None): 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') From 4a4b1a623f1f1944ce6ad13b762e1d56fd0bf1db Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Mon, 30 Jan 2017 16:08:28 -0500 Subject: [PATCH 06/14] Add a utility to frontend DB dump/restore This exposes the library functions as a utility --- confluent_server/bin/confluentdbutil | 66 ++++++++++++++++++++++++++++ confluent_server/confluent/main.py | 20 +++++++++ 2 files changed, 86 insertions(+) create mode 100644 confluent_server/bin/confluentdbutil diff --git a/confluent_server/bin/confluentdbutil b/confluent_server/bin/confluentdbutil new file mode 100644 index 00000000..828f06da --- /dev/null +++ b/confluent_server/bin/confluentdbutil @@ -0,0 +1,66 @@ +#!/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: + print("Must indicate a password to protect or -u to opt opt of " + "secure value protection") + 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) + + + diff --git a/confluent_server/confluent/main.py b/confluent_server/confluent/main.py index 6e4ca9d6..2c5ce1d5 100644 --- a/confluent_server/confluent/main.py +++ b/confluent_server/confluent/main.py @@ -89,6 +89,26 @@ 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+') From cfc6fd04fcd29eea8b435c49cd8cc3c0aa2b90f7 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Mon, 30 Jan 2017 16:10:01 -0500 Subject: [PATCH 07/14] Make confluentdbutil executable --- confluent_server/bin/confluentdbutil | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 confluent_server/bin/confluentdbutil diff --git a/confluent_server/bin/confluentdbutil b/confluent_server/bin/confluentdbutil old mode 100644 new mode 100755 From cbc0ffbc1c1570fbfd8b0bbec37d36674a879c7b Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Mon, 30 Jan 2017 16:12:49 -0500 Subject: [PATCH 08/14] Actually hook up the redact feature --- confluent_server/bin/confluentdbutil | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/confluent_server/bin/confluentdbutil b/confluent_server/bin/confluentdbutil index 828f06da..e5acb419 100755 --- a/confluent_server/bin/confluentdbutil +++ b/confluent_server/bin/confluentdbutil @@ -28,7 +28,8 @@ 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 = 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', @@ -52,15 +53,15 @@ if args[0] == 'restore': sys.exit(1) cfm.restore_db_from_directory(dumpdir, options.password) elif args[0] == 'dump': - if options.password is None and not options.unprotected: + 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") + "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) + cfm.dump_db_to_directory(dumpdir, options.password, options.redact) From a288136a80d3ed517558fdecc8103e4f4783a046 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Mon, 30 Jan 2017 16:13:49 -0500 Subject: [PATCH 09/14] Do not write out keys when redacting It's silly to store keys when redact is requested --- confluent_server/confluent/config/configmanager.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/confluent_server/confluent/config/configmanager.py b/confluent_server/confluent/config/configmanager.py index 7bf757be..2876b9d0 100644 --- a/confluent_server/confluent/config/configmanager.py +++ b/confluent_server/confluent/config/configmanager.py @@ -1476,9 +1476,10 @@ def restore_db_from_directory(location, password): 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') From c381fefc494415768eb186ef0dd432dc3d02ff2c Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Mon, 30 Jan 2017 16:27:49 -0500 Subject: [PATCH 10/14] Give a friendlier message on restore of redact DB A redacted dump will not have a keys.json file, which is natural. Replace 'file not found' with a message indicating the possibility of a redacted dump. --- confluent_server/confluent/config/configmanager.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/confluent_server/confluent/config/configmanager.py b/confluent_server/confluent/config/configmanager.py index 2876b9d0..10805ea7 100644 --- a/confluent_server/confluent/config/configmanager.py +++ b/confluent_server/confluent/config/configmanager.py @@ -1466,10 +1466,15 @@ def _dump_keys(password): def restore_db_from_directory(location, password): - with open(os.path.join(location, 'keys.json'), 'r') as cfgfile: - keydata = cfgfile.read() - json.loads(keydata) - _restore_keys(keydata, 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) From 3b38d8ac430d225f4836e0c879d8dee199783be6 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Mon, 30 Jan 2017 16:38:43 -0500 Subject: [PATCH 11/14] Assure deterministic enumeration of areas nodegroup must be restored first. --- confluent_server/confluent/config/configmanager.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/confluent_server/confluent/config/configmanager.py b/confluent_server/confluent/config/configmanager.py index 10805ea7..4a36a97d 100644 --- a/confluent_server/confluent/config/configmanager.py +++ b/confluent_server/confluent/config/configmanager.py @@ -1246,7 +1246,9 @@ class ConfigManager(object): pass # Now we have to iterate through each fixed up element, using the # set attribute to flesh out inheritence and expressions - for confarea in tmpconfig: + for confarea in _config_areas: + if confarea not in tmpconfig: + continue if confarea == 'nodes': self.set_node_attributes(tmpconfig[confarea], True) elif confarea == 'nodegroups': From 02dd29b027a4404e2a44945ffb882de5e6edeb8e Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Tue, 31 Jan 2017 15:38:54 -0500 Subject: [PATCH 12/14] Add the util to the setup.py Previously packaging was missing the new utility --- confluent_server/setup.py.tmpl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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/', [])], From ffd1bdfae3ba454f437274ca24c7fb66a4510585 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Thu, 2 Feb 2017 17:00:09 -0500 Subject: [PATCH 13/14] Clean up nodepower nodepower boot and reset output was misleading. --- confluent_client/bin/nodepower | 1 + confluent_client/confluent/client.py | 11 ++++++++++- confluent_server/confluent/messages.py | 5 +++++ .../confluent/plugins/hardwaremanagement/ipmi.py | 16 ++++++++++++++-- 4 files changed, 30 insertions(+), 3 deletions(-) diff --git a/confluent_client/bin/nodepower b/confluent_client/bin/nodepower index 6711aea0..5dd0b007 100755 --- a/confluent_client/bin/nodepower +++ b/confluent_client/bin/nodepower @@ -45,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/confluent/client.py b/confluent_client/confluent/client.py index 38ff2bbf..aa30e6a1 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,12 @@ class Command(object): else: rc |= 1 elif ikey in res[node]: - print('{0}: {1}'.format(node, res[node][ikey]['value'])) + if self._prevkeyname and self._prevkeyname in res[node]: + print('{0}: {2}->{1}'.format( + node, res[node][ikey]['value'], + res[node][self._prevkeyname]['value'])) + else: + print('{0}: {1}'.format(node, res[node][ikey]['value'])) return rc def simple_noderange_command(self, noderange, resource, input=None, 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/hardwaremanagement/ipmi.py b/confluent_server/confluent/plugins/hardwaremanagement/ipmi.py index d929b2df..ce92b869 100644 --- a/confluent_server/confluent/plugins/hardwaremanagement/ipmi.py +++ b/confluent_server/confluent/plugins/hardwaremanagement/ipmi.py @@ -795,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): From f46269a6f2cbf2587916d40e5970bfc873dbed89 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Fri, 10 Feb 2017 16:41:47 -0500 Subject: [PATCH 14/14] Add debug of broken expression to nodelist If an expression is invalid, have nodelist give the error data allowing the user to see and potentially take action. --- confluent_client/bin/nodelist | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/confluent_client/bin/nodelist b/confluent_client/bin/nodelist index 50245628..6cff5dcd 100755 --- a/confluent_client/bin/nodelist +++ b/confluent_client/bin/nodelist @@ -76,7 +76,11 @@ 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']) + if options.blame or 'broken' in currattr: blamedata = [] if 'inheritedfrom' in currattr: blamedata.append('inherited from group {0}'.format(