From 0e879dc3dea372bc637ef9256a1bb2088ae4b1ca Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Mon, 9 May 2022 09:57:35 -0400 Subject: [PATCH 01/31] Add el7 to alternat squashfs name --- imgutil/confluent_imgutil.spec.tmpl | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/imgutil/confluent_imgutil.spec.tmpl b/imgutil/confluent_imgutil.spec.tmpl index 268b63f6..16ab1114 100644 --- a/imgutil/confluent_imgutil.spec.tmpl +++ b/imgutil/confluent_imgutil.spec.tmpl @@ -13,9 +13,13 @@ Requires: squashfs-tools %if "%{dist}" == ".el9" Requires: squashfs-tools %else +%if "%{dist}" == ".el7" +Requires: squashfs-tools +%else Requires: squashfs %endif %endif +%endif %description From 6229cb23e8a7af143f05e48b63e2d2bff5fc7f1c Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Tue, 10 May 2022 16:00:08 -0400 Subject: [PATCH 02/31] Begin PDU implementation --- .../confluent/config/attributes.py | 6 + .../confluent/config/configmanager.py | 2 +- confluent_server/confluent/core.py | 5 + confluent_server/confluent/httpapi.py | 4 + .../plugins/hardwaremanagement/geist.py | 103 ++++++++++++++++++ .../plugins/hardwaremanagement/pdu.py | 93 ++++++++++++++++ 6 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 confluent_server/confluent/plugins/hardwaremanagement/geist.py create mode 100644 confluent_server/confluent/plugins/hardwaremanagement/pdu.py diff --git a/confluent_server/confluent/config/attributes.py b/confluent_server/confluent/config/attributes.py index 647458b1..5444e0d7 100644 --- a/confluent_server/confluent/config/attributes.py +++ b/confluent_server/confluent/config/attributes.py @@ -534,6 +534,12 @@ node = { 'To support this scenario, the switch should be set up to allow independent operation of member ports123654 (e.g. lacp bypass mode or fallback mode).', 'validvalues': ('lacp', 'loadbalance', 'roundrobin', 'activebackup', 'none') }, + 'power.pdu': { + 'description': 'Specifies the managed PDU associated with a power input on the node' + }, + 'power.outlet': { + 'description': 'Species the outlet identifier on the PDU associoted with a power input on the node' + }, # 'id.modelnumber': { # 'description': 'The manufacturer dictated model number for the node', # }, diff --git a/confluent_server/confluent/config/configmanager.py b/confluent_server/confluent/config/configmanager.py index d42c6f96..5c21d89f 100644 --- a/confluent_server/confluent/config/configmanager.py +++ b/confluent_server/confluent/config/configmanager.py @@ -485,7 +485,7 @@ def attribute_is_invalid(attrname, attrval): def _get_valid_attrname(attrname): - if attrname.startswith('net.'): + if attrname.startswith('net.') or attrname.startswith('power.'): # For net.* attribtues, split on the dots and put back together # longer term we might want a generic approach, but # right now it's just net. attributes diff --git a/confluent_server/confluent/core.py b/confluent_server/confluent/core.py index 1a1631ca..690886c6 100644 --- a/confluent_server/confluent/core.py +++ b/confluent_server/confluent/core.py @@ -360,6 +360,10 @@ def _init_core(): {'pluginattrs': ['hardwaremanagement.method'], 'default': 'ipmi'}), }, + '_pdu': { + 'outlets': PluginCollection( + {'pluginattrs': ['hardwaremanagement.method']}), + }, 'shell': { # another special case similar to console 'sessions': PluginCollection({ @@ -457,6 +461,7 @@ def _init_core(): 'pluginattrs': ['hardwaremanagement.method'], 'default': 'ipmi', }), + 'inlets': PluginCollection({'handler': 'pdu'}), 'reseat': PluginRoute({'handler': 'enclosure'}), }, 'sensors': { diff --git a/confluent_server/confluent/httpapi.py b/confluent_server/confluent/httpapi.py index 295f99f2..d6b4af5d 100644 --- a/confluent_server/confluent/httpapi.py +++ b/confluent_server/confluent/httpapi.py @@ -21,6 +21,10 @@ try: import Cookie except ModuleNotFoundError: import http.cookies as Cookie +try: + import pywarp +except ImportError: + pywarp = None import confluent.auth as auth import confluent.config.attributes as attribs import confluent.consoleserver as consoleserver diff --git a/confluent_server/confluent/plugins/hardwaremanagement/geist.py b/confluent_server/confluent/plugins/hardwaremanagement/geist.py new file mode 100644 index 00000000..acc9713d --- /dev/null +++ b/confluent_server/confluent/plugins/hardwaremanagement/geist.py @@ -0,0 +1,103 @@ +# Copyright 2022 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 pyghmi.util.webclient as wc +import confluent.util as util +import confluent.messages as msg + + +class GeistClient(object): + def __init__(self, pdu, configmanager): + self.node = pdu + self.configmanager = configmanager + self._token = None + self._wc = None + self.username = None + + @property + def token(self): + if not self._token: + self._token = self.login(self.configmanager) + return self._token + + @property + def wc(self): + if self._wc: + return self._wc + targcfg = self.configmanager.get_node_attributes(self.node, + ['hardwaremanagement.manager'], + decrypt=True) + targcfg = targcfg.get(self.node, {}) + target = targcfg.get( + 'hardwaremanagement.manager', {}).get('value', None) + if not target: + target = self.node + cv = util.TLSCertVerifier( + self.configmanager, self.node, + 'pubkeys.tls_hardwaremanager').verify_cert + self._wc = wc.SecureHTTPConnection(target, verifycallback=cv) + return self._wc + + def login(self, configmanager): + credcfg = configmanager.get_node_attributes(self.node, + ['secret.hardwaremanagementuser', + 'secret.hardwaremanagementpassword'], + decrypt=True) + username = credcfg.get( + 'secret.hardwaremanagementuser', {}).get('value', None) + passwd = credcfg.get( + 'secret.hardwaremanagementpassword', {}).get('value', None) + if not username or not passwd: + raise Exception('Missing username or password') + self.username = username + rsp = self.wc.grab_json_response( + '/api/auth/{0]'.format(username), + {'cmd': 'login', 'data': {'password': passwd}}) + token = rsp['data']['token'] + return token + + def logout(self): + if self._token: + self.wc.grab_json_response('/api/auth/{0}'.format(self.username), + {'cmd': 'logout', 'token': self.token}) + self._token = None + + def get_outlet(self, outlet): + rsp = self.wc.grab_json_response('/api/dev') + rsp = rsp['data'] + if len(rsp) != 1: + raise Exception('Multiple PDUs not supported per pdu') + pduname = list(rsp)[0] + outlet = rsp[pduname]['outlet'][str(int(outlet) - 1)] + state = outlet['state'] + return state + + def set_outlet(self, outlet, state): + rsp = self.wc.grab_json_response('/api/dev') + if len(rsp['data'] != 1): + self.logout() + raise Exception('Multiple PDUs per endpoint not supported') + pdu = list(rsp['data'])[0] + outlet = int(outlet) - 1 + rsp = self.wc.grab_json_response( + '/api/dev/{0}/outlet/{1}'.format(pdu, outlet), + {'cmd': 'control', 'token': self.token, + 'data': {'action': state, 'delay': False}}) + + +def retrieve(nodes, element, configmanager, inputdata): + for node in nodes: + gc = GeistClient(node, configmanager) + state = gc.get_outlet(element[-1]) + yield msg.PowerState(node=node, state=state) diff --git a/confluent_server/confluent/plugins/hardwaremanagement/pdu.py b/confluent_server/confluent/plugins/hardwaremanagement/pdu.py new file mode 100644 index 00000000..a1e82e8e --- /dev/null +++ b/confluent_server/confluent/plugins/hardwaremanagement/pdu.py @@ -0,0 +1,93 @@ +# Copyright 2017 Lenovo +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import confluent.core as core +import confluent.messages as msg +import pyghmi.exceptions as pygexc +import confluent.exceptions as exc + +def retrieve(nodes, element, configmanager, inputdata): + emebs = configmanager.get_node_attributes( + nodes, (u'power.*pdu', u'power.*outlet')) + if element == ['power', 'inlets']: + outletnames = set([]) + for node in nodes: + for attrib in emebs[node]: + attrib = attrib.replace('power.', '').rsplit('.', 1) + if len(attrib) > 1: + outletnames.add('inlet_' + attrib[0]) + else: + outletnames.add('default') + if outletnames: + outletnames.add('all') + for inlet in outletnames: + yield msg.ChildCollection(inlet) + elif len(element) == 3: + inletname = element[-1] + outlets = get_outlets(nodes, emebs, inletname) + for node in outlets: + for pgroup in outlets[node]: + pdu = outlets[node][pgroup]['pdu'] + outlet = outlets[node][pgroup]['outlet'] + for rsp in core.handle_path( + '/nodes/{0}/_pdu/outlets/{1}'.format(pdu, outlet), + 'retrieve', configmanager): + yield msg.KeyValueData({pgroup: rsp.kvpairs['state']['value']}, node) + +def get_outlets(nodes, emebs, inletname): + outlets = {} + for node in nodes: + if node not in outlets: + outlets[node] = {} + for attrib in emebs[node]: + v = emebs[node][attrib].get('value', None) + if not v: + continue + attrib = attrib.replace('power.', '').rsplit('.', 1) + if len(attrib) > 1: + pgroup = 'inlet_' + attrib[0] + else: + pgroup = 'default' + if inletname == 'all' or pgroup == 'inletname': + if pgroup not in outlets[node]: + outlets[node][pgroup] = {} + outlets[node][pgroup][attrib[-1]] = v + return outlets + + +def update(nodes, element, configmanager, inputdata): + emebs = configmanager.get_node_attributes( + nodes, (u'power.*pdu', u'power.*outlet')) + for node in nodes: + for attrib in emebs[node]: + print(repr(attrib)) + try: + em = emebs[node]['enclosure.manager']['value'] + eb = emebs[node]['enclosure.bay']['value'] + except KeyError: + em = node + eb = -1 + if not em: + em = node + if not eb: + eb = -1 + try: + for rsp in core.handle_path( + '/nodes/{0}/_enclosure/reseat_bay'.format(em), + 'update', configmanager, + inputdata={'reseat': int(eb)}): + yield rsp + except pygexc.UnsupportedFunctionality as uf: + yield msg.ConfluentNodeError(node, str(uf)) + except exc.TargetEndpointUnreachable as uf: + yield msg.ConfluentNodeError(node, str(uf)) From 8dbcc804ed83bb93234f7306513518a031185614 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Tue, 10 May 2022 16:05:37 -0400 Subject: [PATCH 03/31] Pull outlets into the generic hierarchy This will more easily facilitate adding pdus without dependent nodes. --- confluent_server/confluent/core.py | 5 +---- confluent_server/confluent/plugins/hardwaremanagement/pdu.py | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/confluent_server/confluent/core.py b/confluent_server/confluent/core.py index 690886c6..fe6c4be4 100644 --- a/confluent_server/confluent/core.py +++ b/confluent_server/confluent/core.py @@ -360,10 +360,6 @@ def _init_core(): {'pluginattrs': ['hardwaremanagement.method'], 'default': 'ipmi'}), }, - '_pdu': { - 'outlets': PluginCollection( - {'pluginattrs': ['hardwaremanagement.method']}), - }, 'shell': { # another special case similar to console 'sessions': PluginCollection({ @@ -462,6 +458,7 @@ def _init_core(): 'default': 'ipmi', }), 'inlets': PluginCollection({'handler': 'pdu'}), + 'outlets': PluginCollection({'pluginattrs': ['hardwaremanagement.method']}), 'reseat': PluginRoute({'handler': 'enclosure'}), }, 'sensors': { diff --git a/confluent_server/confluent/plugins/hardwaremanagement/pdu.py b/confluent_server/confluent/plugins/hardwaremanagement/pdu.py index a1e82e8e..e7570b4d 100644 --- a/confluent_server/confluent/plugins/hardwaremanagement/pdu.py +++ b/confluent_server/confluent/plugins/hardwaremanagement/pdu.py @@ -40,7 +40,7 @@ def retrieve(nodes, element, configmanager, inputdata): pdu = outlets[node][pgroup]['pdu'] outlet = outlets[node][pgroup]['outlet'] for rsp in core.handle_path( - '/nodes/{0}/_pdu/outlets/{1}'.format(pdu, outlet), + '/nodes/{0}/power/outlets/{1}'.format(pdu, outlet), 'retrieve', configmanager): yield msg.KeyValueData({pgroup: rsp.kvpairs['state']['value']}, node) From d8a0f111dbae2ce56eaa8e27f6f859d95414059f Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Wed, 11 May 2022 08:53:24 -0400 Subject: [PATCH 04/31] Implement changing PDU state on set --- confluent_server/confluent/messages.py | 2 ++ .../plugins/hardwaremanagement/geist.py | 16 ++++++++-- .../plugins/hardwaremanagement/pdu.py | 32 ++++++------------- 3 files changed, 25 insertions(+), 25 deletions(-) diff --git a/confluent_server/confluent/messages.py b/confluent_server/confluent/messages.py index 396cd58f..6f0c3c25 100644 --- a/confluent_server/confluent/messages.py +++ b/confluent_server/confluent/messages.py @@ -515,6 +515,8 @@ def get_input_message(path, operation, inputdata, nodes=None, multinode=False, return InputVolumes(path, nodes, inputdata) elif 'inventory/firmware/updates/active' in '/'.join(path) and inputdata: return InputFirmwareUpdate(path, nodes, inputdata, configmanager) + elif ('/'.join(path).startswith('power/inlets/') or '/'.join(path).startswith('power/outlets/')) and inputdata: + return InputPowerMessage(path, nodes, inputdata) elif '/'.join(path).startswith('media/detach'): return DetachMedia(path, nodes, inputdata) elif '/'.join(path).startswith('media/') and inputdata: diff --git a/confluent_server/confluent/plugins/hardwaremanagement/geist.py b/confluent_server/confluent/plugins/hardwaremanagement/geist.py index acc9713d..979550f5 100644 --- a/confluent_server/confluent/plugins/hardwaremanagement/geist.py +++ b/confluent_server/confluent/plugins/hardwaremanagement/geist.py @@ -54,15 +54,20 @@ class GeistClient(object): ['secret.hardwaremanagementuser', 'secret.hardwaremanagementpassword'], decrypt=True) + credcfg = credcfg.get(self.node, {}) username = credcfg.get( 'secret.hardwaremanagementuser', {}).get('value', None) passwd = credcfg.get( 'secret.hardwaremanagementpassword', {}).get('value', None) + if not isinstance(username, str): + username = username.decode('utf8') + if not isinstance(passwd, str): + passwd = passwd.decode('utf8') if not username or not passwd: raise Exception('Missing username or password') self.username = username rsp = self.wc.grab_json_response( - '/api/auth/{0]'.format(username), + '/api/auth/{0}'.format(username), {'cmd': 'login', 'data': {'password': passwd}}) token = rsp['data']['token'] return token @@ -85,7 +90,7 @@ class GeistClient(object): def set_outlet(self, outlet, state): rsp = self.wc.grab_json_response('/api/dev') - if len(rsp['data'] != 1): + if len(rsp['data']) != 1: self.logout() raise Exception('Multiple PDUs per endpoint not supported') pdu = list(rsp['data'])[0] @@ -101,3 +106,10 @@ def retrieve(nodes, element, configmanager, inputdata): gc = GeistClient(node, configmanager) state = gc.get_outlet(element[-1]) yield msg.PowerState(node=node, state=state) + +def update(nodes, element, configmanager, inputdata): + for node in nodes: + gc = GeistClient(node, configmanager) + newstate = inputdata.powerstate(node) + gc.set_outlet(element[-1], newstate) + return retrieve(nodes, element, configmanager, inputdata) \ No newline at end of file diff --git a/confluent_server/confluent/plugins/hardwaremanagement/pdu.py b/confluent_server/confluent/plugins/hardwaremanagement/pdu.py index e7570b4d..825e9609 100644 --- a/confluent_server/confluent/plugins/hardwaremanagement/pdu.py +++ b/confluent_server/confluent/plugins/hardwaremanagement/pdu.py @@ -68,26 +68,12 @@ def get_outlets(nodes, emebs, inletname): def update(nodes, element, configmanager, inputdata): emebs = configmanager.get_node_attributes( nodes, (u'power.*pdu', u'power.*outlet')) - for node in nodes: - for attrib in emebs[node]: - print(repr(attrib)) - try: - em = emebs[node]['enclosure.manager']['value'] - eb = emebs[node]['enclosure.bay']['value'] - except KeyError: - em = node - eb = -1 - if not em: - em = node - if not eb: - eb = -1 - try: - for rsp in core.handle_path( - '/nodes/{0}/_enclosure/reseat_bay'.format(em), - 'update', configmanager, - inputdata={'reseat': int(eb)}): - yield rsp - except pygexc.UnsupportedFunctionality as uf: - yield msg.ConfluentNodeError(node, str(uf)) - except exc.TargetEndpointUnreachable as uf: - yield msg.ConfluentNodeError(node, str(uf)) + inletname = element[-1] + outlets = get_outlets(nodes, emebs, inletname) + for node in outlets: + for pgroup in outlets[node]: + pdu = outlets[node][pgroup]['pdu'] + outlet = outlets[node][pgroup]['outlet'] + for rsp in core.handle_path('/nodes/{0}/power/outlets/{1}'.format(pdu, outlet), + 'update', configmanager, inputdata={'state': inputdata.powerstate(node)}): + yield msg.KeyValueData({pgroup: rsp.kvpairs['state']['value']}, node) From e4d7be649ab7eda784b668c70b2b118d21876be6 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Wed, 11 May 2022 13:31:19 -0400 Subject: [PATCH 05/31] Fix single inlet operations --- confluent_server/confluent/plugins/hardwaremanagement/pdu.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/confluent_server/confluent/plugins/hardwaremanagement/pdu.py b/confluent_server/confluent/plugins/hardwaremanagement/pdu.py index 825e9609..7eaa3330 100644 --- a/confluent_server/confluent/plugins/hardwaremanagement/pdu.py +++ b/confluent_server/confluent/plugins/hardwaremanagement/pdu.py @@ -58,7 +58,7 @@ def get_outlets(nodes, emebs, inletname): pgroup = 'inlet_' + attrib[0] else: pgroup = 'default' - if inletname == 'all' or pgroup == 'inletname': + if inletname == 'all' or pgroup == inletname: if pgroup not in outlets[node]: outlets[node][pgroup] = {} outlets[node][pgroup][attrib[-1]] = v From caba650143c36ca88cb3e580c6c4effda5f7faa8 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Wed, 11 May 2022 14:59:54 -0400 Subject: [PATCH 06/31] Add nodepower arguments for PDU operations --- confluent_client/bin/nodepower | 16 ++++++++++------ confluent_client/confluent/client.py | 3 +++ confluent_client/doc/man/nodepower.ronn | 3 +++ .../plugins/hardwaremanagement/geist.py | 2 +- 4 files changed, 17 insertions(+), 7 deletions(-) diff --git a/confluent_client/bin/nodepower b/confluent_client/bin/nodepower index b21314b3..59d55b13 100755 --- a/confluent_client/bin/nodepower +++ b/confluent_client/bin/nodepower @@ -33,7 +33,7 @@ import confluent.client as client argparser = optparse.OptionParser( usage="Usage: %prog [options] " - "([status|on|off|shutdown|boot|reset])") + "([status|on|off|shutdown|boot|reset|pdu_status|pdu_off|pdu_on])") argparser.add_option('-p', '--showprevious', dest='previous', action='store_true', default=False, help='Show previous power state') @@ -53,20 +53,24 @@ setstate = None if len(args) > 1: if setstate == 'softoff': setstate = 'shutdown' - elif not args[1] in ('stat', 'state', 'status'): - setstate = args[1] -if setstate not in (None, 'on', 'off', 'shutdown', 'boot', 'reset'): +if setstate not in (None, 'on', 'off', 'shutdown', 'boot', 'reset', 'pdu_status', 'pdu_stat', 'pdu_on', 'pdu_off'): argparser.print_help() sys.exit(1) session = client.Command() exitcode = 0 session.add_precede_key('oldstate') +powurl = 'state' +if setstate and setstate.startswith('pdu_'): + setstate = setstate.replace('pdu_', '') + powurl = 'inlets/all' +if setstate in ('status', 'state', 'stat'): + setstate = None if options.previous: # get previous states prev = {} - for rsp in session.read("/noderange/{0}/power/state".format(noderange)): + for rsp in session.read("/noderange/{0}/power/{1}".format(noderange, powurl)): # gets previous (current) states databynode = rsp["databynode"] @@ -77,4 +81,4 @@ if options.previous: # add dictionary to session session.add_precede_dict(prev) -sys.exit(session.simple_noderange_command(noderange, '/power/state', setstate, promptover=options.maxnodes)) +sys.exit(session.simple_noderange_command(noderange, '/power/{0}'.format(powurl), setstate, promptover=options.maxnodes, key='state')) diff --git a/confluent_client/confluent/client.py b/confluent_client/confluent/client.py index bee16f00..47eaa494 100644 --- a/confluent_client/confluent/client.py +++ b/confluent_client/confluent/client.py @@ -245,6 +245,9 @@ class Command(object): node, val, self._prevdict[node])) else: cprint('{0}: {1}'.format(node, val)) + elif ikey == 'state': + for k in res[node]: + cprint('{0}: {1}: {2}'.format(node, k, res[node][k])) return rc def simple_noderange_command(self, noderange, resource, input=None, diff --git a/confluent_client/doc/man/nodepower.ronn b/confluent_client/doc/man/nodepower.ronn index e2d90dd1..bdf303f3 100644 --- a/confluent_client/doc/man/nodepower.ronn +++ b/confluent_client/doc/man/nodepower.ronn @@ -24,6 +24,9 @@ respond. * `reset`: Request immediate reset of nodes of the noderange. Nodes that are off will not react to this request. * `status`: Behave identically to having no argument passed at all. +* `pdu_status`: Query state of associated PDU outlets, if configured. +* `pdu_on`: Energize all PDU outlets associated with the noderange. +* `pdu_off`: De-energize all PDU outlets associated with the noderange. ## OPTIONS diff --git a/confluent_server/confluent/plugins/hardwaremanagement/geist.py b/confluent_server/confluent/plugins/hardwaremanagement/geist.py index 979550f5..0181b39d 100644 --- a/confluent_server/confluent/plugins/hardwaremanagement/geist.py +++ b/confluent_server/confluent/plugins/hardwaremanagement/geist.py @@ -85,7 +85,7 @@ class GeistClient(object): raise Exception('Multiple PDUs not supported per pdu') pduname = list(rsp)[0] outlet = rsp[pduname]['outlet'][str(int(outlet) - 1)] - state = outlet['state'] + state = outlet['state'].split('2')[-1] return state def set_outlet(self, outlet, state): From c328fea49a7e13b910321faba53aa18a9d2c0c2c Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Wed, 11 May 2022 16:01:43 -0400 Subject: [PATCH 07/31] Cleaner output on cli Based on feedback, remove the added 'inlet_' from pdu output. Also, fix geist plugin to block unsupported features for now. --- confluent_client/bin/nodepower | 6 +++++- confluent_client/confluent/client.py | 13 ++++++------- .../confluent/plugins/hardwaremanagement/geist.py | 5 +++++ 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/confluent_client/bin/nodepower b/confluent_client/bin/nodepower index 59d55b13..dffa241c 100755 --- a/confluent_client/bin/nodepower +++ b/confluent_client/bin/nodepower @@ -51,6 +51,7 @@ except IndexError: client.check_globbing(noderange) setstate = None if len(args) > 1: + setstate = args[1] if setstate == 'softoff': setstate = 'shutdown' @@ -81,4 +82,7 @@ if options.previous: # add dictionary to session session.add_precede_dict(prev) -sys.exit(session.simple_noderange_command(noderange, '/power/{0}'.format(powurl), setstate, promptover=options.maxnodes, key='state')) +def outhandler(node, res): + for k in res[node]: + client.cprint('{0}: {1}: {2}'.format(node, k.replace('inlet_', ''), res[node][k])) +sys.exit(session.simple_noderange_command(noderange, '/power/{0}'.format(powurl), setstate, promptover=options.maxnodes, key='state', outhandler=outhandler)) diff --git a/confluent_client/confluent/client.py b/confluent_client/confluent/client.py index 47eaa494..ad29ff02 100644 --- a/confluent_client/confluent/client.py +++ b/confluent_client/confluent/client.py @@ -208,7 +208,7 @@ class Command(object): def add_precede_dict(self, dict): self._prevdict = dict - def handle_results(self, ikey, rc, res, errnodes=None): + def handle_results(self, ikey, rc, res, errnodes=None, outhandler=None): if 'error' in res: if errnodes is not None: errnodes.add(self._currnoderange) @@ -245,13 +245,12 @@ class Command(object): node, val, self._prevdict[node])) else: cprint('{0}: {1}'.format(node, val)) - elif ikey == 'state': - for k in res[node]: - cprint('{0}: {1}: {2}'.format(node, k, res[node][k])) + elif outhandler: + outhandler(node, res) return rc def simple_noderange_command(self, noderange, resource, input=None, - key=None, errnodes=None, promptover=None, **kwargs): + key=None, errnodes=None, promptover=None, outhandler=None, **kwargs): try: self._currnoderange = noderange rc = 0 @@ -265,13 +264,13 @@ class Command(object): if input is None: for res in self.read('/noderange/{0}/{1}'.format( noderange, resource)): - rc = self.handle_results(ikey, rc, res, errnodes) + rc = self.handle_results(ikey, rc, res, errnodes, outhandler) else: self.stop_if_noderange_over(noderange, promptover) kwargs[ikey] = input for res in self.update('/noderange/{0}/{1}'.format( noderange, resource), kwargs): - rc = self.handle_results(ikey, rc, res, errnodes) + rc = self.handle_results(ikey, rc, res, errnodes, outhandler) self._currnoderange = None return rc except KeyboardInterrupt: diff --git a/confluent_server/confluent/plugins/hardwaremanagement/geist.py b/confluent_server/confluent/plugins/hardwaremanagement/geist.py index 0181b39d..62bac271 100644 --- a/confluent_server/confluent/plugins/hardwaremanagement/geist.py +++ b/confluent_server/confluent/plugins/hardwaremanagement/geist.py @@ -15,6 +15,7 @@ import pyghmi.util.webclient as wc import confluent.util as util import confluent.messages as msg +import confluent.exceptions as exc class GeistClient(object): @@ -102,12 +103,16 @@ class GeistClient(object): def retrieve(nodes, element, configmanager, inputdata): + if 'outlets' not in element: + raise exc.NotImplementedException('Not implemented') for node in nodes: gc = GeistClient(node, configmanager) state = gc.get_outlet(element[-1]) yield msg.PowerState(node=node, state=state) def update(nodes, element, configmanager, inputdata): + if 'outlets' not in element: + raise exc.NotImplementedException('Not implemented') for node in nodes: gc = GeistClient(node, configmanager) newstate = inputdata.powerstate(node) From 459c9a5210401683cb6834aaffbfd99316740b20 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Thu, 12 May 2022 16:39:58 -0400 Subject: [PATCH 08/31] Wait for a login attempt to run its course If an existing session was not quite logged in, but may be getting there, join in and wait for result instead of starting over again. --- confluent_server/confluent/plugins/hardwaremanagement/ipmi.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/confluent_server/confluent/plugins/hardwaremanagement/ipmi.py b/confluent_server/confluent/plugins/hardwaremanagement/ipmi.py index b2e5615c..b5220791 100644 --- a/confluent_server/confluent/plugins/hardwaremanagement/ipmi.py +++ b/confluent_server/confluent/plugins/hardwaremanagement/ipmi.py @@ -482,7 +482,7 @@ class IpmiHandler(object): self.tenant = cfg.tenant tenant = cfg.tenant while ((node, tenant) not in persistent_ipmicmds or - not persistent_ipmicmds[(node, tenant)].ipmi_session.logged or + not (persistent_ipmicmds[(node, tenant)].ipmi_session.logged or persistent_ipmicmds[(node, tenant)].ipmi_session.logging) or persistent_ipmicmds[(node, tenant)].ipmi_session.broken): try: persistent_ipmicmds[(node, tenant)].close_confluent() @@ -514,6 +514,8 @@ class IpmiHandler(object): raise exc.TargetEndpointUnreachable(ge.strerror) raise self.ipmicmd = persistent_ipmicmds[(node, tenant)] + while not self.ipmicmd.ipmi_session.broken and not self.ipmicmd.ipmi_session.logged: + self.ipmicmd.ipmi_session.wait_for_rsp(3) bootdevices = { 'optical': 'cd' From 54741517f10357b6a039e857d0645bb75e2fb4c0 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Sat, 14 May 2022 18:12:19 -0400 Subject: [PATCH 09/31] Clear DHCP buffer between recv Very large PXE requests can leave residual information that small, non-pxe requests will interact poorly with, leading to spurious pxe-client with cloned uuid of most recent large request. Clearing between IO normalizes the state to avoid the bleed over. --- confluent_server/confluent/discovery/protocols/pxe.py | 1 + 1 file changed, 1 insertion(+) diff --git a/confluent_server/confluent/discovery/protocols/pxe.py b/confluent_server/confluent/discovery/protocols/pxe.py index 906cc425..ca06cfba 100644 --- a/confluent_server/confluent/discovery/protocols/pxe.py +++ b/confluent_server/confluent/discovery/protocols/pxe.py @@ -381,6 +381,7 @@ def snoop(handler, protocol=None, nodeguess=None): if not ready or not ready[0]: continue for netc in ready[0]: + rawbuffer[:] = b'\x00' * 2048 idx = None if netc == net4: i = recvmsg(netc.fileno(), ctypes.pointer(msg), 0) From eb99fbd8b22cc6197541cdcf5eec36248389e386 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Sat, 14 May 2022 18:35:15 -0400 Subject: [PATCH 10/31] Switch from clear buffer to sized memoryview Use the recvmsg hint to mask out the buffer rather than zeroing the entire buffer. This is more efficient and further improves efficiency of parsing of the packet. --- confluent_server/confluent/discovery/protocols/pxe.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/confluent_server/confluent/discovery/protocols/pxe.py b/confluent_server/confluent/discovery/protocols/pxe.py index ca06cfba..97efdad9 100644 --- a/confluent_server/confluent/discovery/protocols/pxe.py +++ b/confluent_server/confluent/discovery/protocols/pxe.py @@ -381,7 +381,6 @@ def snoop(handler, protocol=None, nodeguess=None): if not ready or not ready[0]: continue for netc in ready[0]: - rawbuffer[:] = b'\x00' * 2048 idx = None if netc == net4: i = recvmsg(netc.fileno(), ctypes.pointer(msg), 0) @@ -394,7 +393,7 @@ def snoop(handler, protocol=None, nodeguess=None): if level == socket.IPPROTO_IP and typ == IP_PKTINFO: idx, recv = struct.unpack('II', cmsgarr[16:24]) recv = ipfromint(recv) - rqv = memoryview(rawbuffer) + rqv = memoryview(rawbuffer)[:i] if rawbuffer[0] == 1: # Boot request process_dhcp4req(handler, nodeguess, cfg, net4, idx, recv, rqv) elif netc == net6: From abb85d503463abc75b5297d8f6ec5724941fdb8f Mon Sep 17 00:00:00 2001 From: erderial <71669104+erderial@users.noreply.github.com> Date: Mon, 16 May 2022 21:21:42 +0300 Subject: [PATCH 11/31] Created nodeping file --- confluent_client/doc/man/nodeping.ronn | 38 ++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 confluent_client/doc/man/nodeping.ronn diff --git a/confluent_client/doc/man/nodeping.ronn b/confluent_client/doc/man/nodeping.ronn new file mode 100644 index 00000000..3a1a6a37 --- /dev/null +++ b/confluent_client/doc/man/nodeping.ronn @@ -0,0 +1,38 @@ +nodeping(8) -- Pings a node or a noderange. +============================== +## SYNOPSIS +`nodeping [options] noderange` + +## DESCRIPTION +**nodeping** is a command that pings the default NIC on a node. +It can also be used with the `-s` flag to change the ping location to something that is 'non primary' + + +## OPTIONS +* ` -f` COUNT, `-c` COUNT, --count=COUNT + Number of commands to run at a time +* `-h`, `--help`: + Show help message and exit +* `-s` SUBSTITUTENAME, --substitutename=SUBSTITUTENAME + Use a different name other than the nodename for ping + +## EXAMPLES + * Pinging a node : + `# nodeping ` + `node : ping` + +* Pinging a group: + `# nodeping ` + `Node1 : ping + Node2 : ping + Node3 : ping` + +* Pinging BMC on a node: + `# nodeping -s {bmc} ` + ` Node-bmc : ping` + +* Fail to ping node: + `# nodeping ` + `node : no_ping` + + From 8deadec7a6494670f9ef24bd0c5eb75b6fbfd983 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Tue, 17 May 2022 10:44:07 -0400 Subject: [PATCH 12/31] Provide hook to override rungesis Some applications may want much of the genesis distribution, but want to have a bootstrap process independent of confluent. Provide hooks either through the identity image or the boot image itself. --- .../genesis/initramfs/opt/confluent/bin/rungenesis | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/confluent_osdeploy/genesis/initramfs/opt/confluent/bin/rungenesis b/confluent_osdeploy/genesis/initramfs/opt/confluent/bin/rungenesis index 2e888928..ff5c9209 100644 --- a/confluent_osdeploy/genesis/initramfs/opt/confluent/bin/rungenesis +++ b/confluent_osdeploy/genesis/initramfs/opt/confluent/bin/rungenesis @@ -43,6 +43,20 @@ mkdir -p /etc/pki/tls/certs cat /tls/*.pem > /etc/pki/tls/certs/ca-bundle.crt TRIES=0 touch /etc/confluent/confluent.info +if [ -e /dev/disk/by-label/CNFLNT_IDNT ]; then + mkdir -p /media/ident + mount /dev/disk/by-label/CNFLNT_IDNT /media/ident + if [ -e /media/ident/genesis_bootstrap.sh ]; then + exec sh /media/ident/genesis_bootstrap.sh + fi +fi +if [ -e /dev/disk/by-label/GENESIS-X86 ]; then + mkdir -p /media/genesis + mount /dev/disk/by-label/GENESIS-X86 /media/genesis + if [ -e /media/genesis/genesis_bootstrap.sh ]; then + exec sh /media/genesis/genesis_bootstrap.sh + fi +fi cd /sys/class/net echo -n "Scanning for network configuration..." while ! grep ^EXTMGRINFO: /etc/confluent/confluent.info | awk -F'|' '{print $3}' | grep 1 >& /dev/null && [ "$TRIES" -lt 30 ]; do From f9d47bb0d3ac70806f09ee021a93ed4825b2f0ad Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Fri, 20 May 2022 08:35:43 -0400 Subject: [PATCH 13/31] Fix markingrequest as not implemented The Geist PDU support inadvertently took down unrelated parts of a request, fix by properly showing not implemented in a node specific way. --- .../confluent/plugins/hardwaremanagement/geist.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/confluent_server/confluent/plugins/hardwaremanagement/geist.py b/confluent_server/confluent/plugins/hardwaremanagement/geist.py index 62bac271..7490965a 100644 --- a/confluent_server/confluent/plugins/hardwaremanagement/geist.py +++ b/confluent_server/confluent/plugins/hardwaremanagement/geist.py @@ -104,7 +104,9 @@ class GeistClient(object): def retrieve(nodes, element, configmanager, inputdata): if 'outlets' not in element: - raise exc.NotImplementedException('Not implemented') + for node in nodes: + yield msg.ConfluentResourceUnavailable(node, 'Not implemented') + return for node in nodes: gc = GeistClient(node, configmanager) state = gc.get_outlet(element[-1]) @@ -112,7 +114,8 @@ def retrieve(nodes, element, configmanager, inputdata): def update(nodes, element, configmanager, inputdata): if 'outlets' not in element: - raise exc.NotImplementedException('Not implemented') + yield msg.ConfluentResourceUnavailable(node, 'Not implemented') + return for node in nodes: gc = GeistClient(node, configmanager) newstate = inputdata.powerstate(node) From a3cce144bc0ef507e135d856c16e5f7abef2c7ee Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Tue, 24 May 2022 07:24:56 -0400 Subject: [PATCH 14/31] Extend manager principals for ssh When doing osdeploy initialize -l (not recommended usually), add on more forms of the name and ip addresses to be consistent with node ssh behavior. --- confluent_server/bin/osdeploy | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/confluent_server/bin/osdeploy b/confluent_server/bin/osdeploy index d3f68647..378d00d2 100644 --- a/confluent_server/bin/osdeploy +++ b/confluent_server/bin/osdeploy @@ -22,6 +22,8 @@ import confluent.util as util import confluent.client as client import confluent.sshutil as sshutil import confluent.certutil as certutil +import confluent.netutil as netutil +import socket try: input = raw_input except NameError: @@ -149,10 +151,13 @@ def local_node_trust_setup(): neededlines = set([ 'HostbasedAuthentication yes', 'HostbasedUsesNameFromPacketOnly yes', 'IgnoreRhosts no']) - if domain and not myname.endswith(domain): - myprincipals.add('{0}.{1}'.format(myname, domain)) - if domain and '.' in myname and myname.endswith(domain): - myprincipals.add(myname.split('.')[0]) + myshortname = myname.split('.')[0] + myprincipals.add(myshortname) + if domain: + myprincipals.add('{0}.{1}'.format(myshortname, domain)) + for addr in netutil.get_my_addresses(): + addr = socket.inet_ntop(addr[0], addr[1]) + myprincipals.add(addr) for pubkey in glob.glob('/etc/ssh/ssh_host_*_key.pub'): currpubkey = open(pubkey, 'rb').read() cert = sshutil.sign_host_key(currpubkey, myname, myprincipals) From 371942b0ce79ec3c0417600c6388765c9809ca33 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Tue, 24 May 2022 08:17:04 -0400 Subject: [PATCH 15/31] Add myhostname to improve name resolution errors in genesis --- genesis/97genesis/install-base | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/genesis/97genesis/install-base b/genesis/97genesis/install-base index da97dc9c..b0e56743 100644 --- a/genesis/97genesis/install-base +++ b/genesis/97genesis/install-base @@ -10,7 +10,7 @@ dracut_install awk egrep dirname bc expr sort dracut_install ssh sshd vi reboot lspci parted tmux mkfs mkfs.ext4 mkfs.xfs xfs_db mkswap dracut_install efibootmgr dracut_install du df ssh-keygen scp clear dhclient lldpd lldpcli tee -dracut_install /lib64/libnss_dns-2.28.so /lib64/libnss_dns.so.2 +dracut_install /lib64/libnss_dns-2.28.so /lib64/libnss_dns.so.2 /lib64/libnss_myhostname.so.2 dracut_install ldd uptime /usr/lib64/libnl-3.so.200 dracut_install poweroff date /etc/nsswitch.conf /etc/services /etc/protocols dracut_install /usr/share/terminfo/x/xterm /usr/share/terminfo/l/linux /usr/share/terminfo/v/vt100 /usr/share/terminfo/x/xterm-color /usr/share/terminfo/s/screen /usr/share/terminfo/x/xterm-256color /usr/share/terminfo/p/putty-256color /usr/share/terminfo/p/putty /usr/share/terminfo/d/dumb From 48d46bcfae5a109a1f1fdbdb6e46e8febe75e2c2 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Tue, 24 May 2022 08:27:36 -0400 Subject: [PATCH 16/31] Add resolv setup to genesis --- .../initramfs/opt/confluent/bin/rungenesis | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/confluent_osdeploy/genesis/initramfs/opt/confluent/bin/rungenesis b/confluent_osdeploy/genesis/initramfs/opt/confluent/bin/rungenesis index ff5c9209..b7035fe0 100644 --- a/confluent_osdeploy/genesis/initramfs/opt/confluent/bin/rungenesis +++ b/confluent_osdeploy/genesis/initramfs/opt/confluent/bin/rungenesis @@ -155,6 +155,23 @@ elif [ "$autoconfigmethod" = "static" ]; then ip route add default via $v4gw fi fi +nameserversec=0 +while read -r entry; do + if [ $nameserversec = 1 ]; then + if [[ $entry == "-"* ]] && [[ $entry != "- ''" ]]; then + echo nameserver ${entry#- } >> /etc/resolv.conf + continue + fi + fi + nameserversec=0 + if [ "${entry%:*}" = "nameservers" ]; then + nameserversec=1 + continue + fi +done < /etc/confluent/confluent.deploycfg +dnsdomain=$(grep ^dnsdomain: /etc/confluent/confluent.deploycfg) +dnsdomain=${dnsdomain#dnsdomain: } +echo search $dnsdomain >> /etc/resolv.conf echo -n "Initializing ssh..." ssh-keygen -A for pubkey in /etc/ssh/ssh_host*key.pub; do From 9b39c96135ec421120686f4071abec60bdf197a5 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Tue, 24 May 2022 10:22:34 -0400 Subject: [PATCH 17/31] Begin work on webauthn support Provide appropriate registration options as a first step. --- confluent_server/confluent/auth.py | 2 + .../confluent/config/configmanager.py | 6 +-- confluent_server/confluent/httpapi.py | 12 ++++- confluent_server/confluent/webauthn.py | 51 +++++++++++++++++++ 4 files changed, 66 insertions(+), 5 deletions(-) create mode 100644 confluent_server/confluent/webauthn.py diff --git a/confluent_server/confluent/auth.py b/confluent_server/confluent/auth.py index 9e675fbb..7e3e269e 100644 --- a/confluent_server/confluent/auth.py +++ b/confluent_server/confluent/auth.py @@ -162,6 +162,8 @@ def authorize(name, element, tenant=False, operation='create', return False manager = configmanager.ConfigManager(tenant, username=user) userobj = manager.get_user(user) + if userobj and userobj.get('role', None) == 'Stub': + userobj = None if not userobj: for group in userutil.grouplist(user): userobj = manager.get_usergroup(group) diff --git a/confluent_server/confluent/config/configmanager.py b/confluent_server/confluent/config/configmanager.py index 5c21d89f..c9a8d937 100644 --- a/confluent_server/confluent/config/configmanager.py +++ b/confluent_server/confluent/config/configmanager.py @@ -113,7 +113,7 @@ _attraliases = { 'bmcpass': 'secret.hardwaremanagementpassword', 'switchpass': 'secret.hardwaremanagementpassword', } -_validroles = ('Administrator', 'Operator', 'Monitor') +_validroles = ('Administrator', 'Operator', 'Monitor', 'Stub') membership_callback = None @@ -2777,8 +2777,8 @@ def dump_db_to_directory(location, password, redact=None, skipkeys=False): cfgfile.write('\n') bkupglobals = get_globals() if bkupglobals: - json.dump(bkupglobals, open(os.path.join(location, 'globals.json'), - 'w')) + with open(os.path.join(location, 'globals.json'), 'w') as globout: + json.dump(bkupglobals, globout) try: for tenant in os.listdir( os.path.join(ConfigManager._cfgdir, '/tenants/')): diff --git a/confluent_server/confluent/httpapi.py b/confluent_server/confluent/httpapi.py index d6b4af5d..6fc2aee2 100644 --- a/confluent_server/confluent/httpapi.py +++ b/confluent_server/confluent/httpapi.py @@ -22,9 +22,9 @@ try: except ModuleNotFoundError: import http.cookies as Cookie try: - import pywarp + import confluent.webauthn as webauthn except ImportError: - pywarp = None + webauthn = None import confluent.auth as auth import confluent.config.attributes as attribs import confluent.consoleserver as consoleserver @@ -834,6 +834,14 @@ def resourcehandler_backend(env, start_response): tlvdata.unicode_dictvalues(sessinfo) yield json.dumps(sessinfo) return + elif url.startswith('/sessions/current/webauthn/'): + if not webauthn: + start_response('501 Not Implemented', headers) + yield '' + return + for rsp in webauthn.handle_api_request(url, env, start_response, authorized['username'], cfgmgr, headers): + yield rsp + return resource = '.' + url[url.rindex('/'):] lquerydict = copy.deepcopy(querydict) try: diff --git a/confluent_server/confluent/webauthn.py b/confluent_server/confluent/webauthn.py new file mode 100644 index 00000000..055cc1cf --- /dev/null +++ b/confluent_server/confluent/webauthn.py @@ -0,0 +1,51 @@ +import confluent.util as util +import json +import pywarp +import pywarp.backends + +creds = {} +challenges = {} + +class TestBackend(pywarp.backends.CredentialStorageBackend): + def __init__(self): + pass + + def get_credential_by_email(self, email): + return creds[email] + + def save_credential_for_user(self, email, credential): + creds[email] = credential + + def save_challenge_for_user(self, email, challenge, type): + challenges[email] = challenge + + def get_challenge_for_user(self, email, type): + return challenges[email] + + +def handle_api_request(url, env, start_response, username, cfm, headers): + if env['REQUEST_METHOD'] != 'POST': + raise Exception('Only POST supported for webauthn operations') + url = url.replace('/sessions/current/webauthn', '') + if'CONTENT_LENGTH' in env and int(env['CONTENT_LENGTH']) > 0: + reqbody = env['wsgi.input'].read(int(env['CONTENT_LENGTH'])) + reqtype = env['CONTENT_TYPE'] + if url == '/registration_options': + rp = pywarp.RelyingPartyManager('Confluent Web UI', credential_storage_backend=TestBackend()) + userinfo = cfm.get_user(username) + if not userinfo: + cfm.create_user(username, role='Stub') + userinfo = cfm.get_user(username) + authid = userinfo.get('authid', None) + if not authid: + authid = util.randomstring(64) + cfm.set_user(username, {'authid': authid}) + opts = rp.get_registration_options(username) + # pywarp generates an id derived + # from username, which is a 'must not' in the spec + # we replace that with a complying approach + opts['user']['id'] = authid + if 'icon' in opts['user']: + del opts['user']['icon'] + start_response('200 OK', headers) + yield json.dumps(opts) From c93f09bc9177371e3c829dbc84fe37332f548b93 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Tue, 24 May 2022 19:17:31 -0400 Subject: [PATCH 18/31] Provide hook to get registered credentials This has to relax the session in getting and requesting validation. --- confluent_server/confluent/auth.py | 2 ++ confluent_server/confluent/httpapi.py | 23 +++++++++----- confluent_server/confluent/webauthn.py | 43 +++++++++++++++++++++++--- 3 files changed, 56 insertions(+), 12 deletions(-) diff --git a/confluent_server/confluent/auth.py b/confluent_server/confluent/auth.py index 7e3e269e..02828f36 100644 --- a/confluent_server/confluent/auth.py +++ b/confluent_server/confluent/auth.py @@ -162,6 +162,8 @@ def authorize(name, element, tenant=False, operation='create', return False manager = configmanager.ConfigManager(tenant, username=user) userobj = manager.get_user(user) + if element in ('/sessions/current/webauthn/registered_credentials', '/sessions/current/webauthn/validate'): + return userobj, manager, user, tenant, skipuserobj if userobj and userobj.get('role', None) == 'Stub': userobj = None if not userobj: diff --git a/confluent_server/confluent/httpapi.py b/confluent_server/confluent/httpapi.py index 6fc2aee2..ece18980 100644 --- a/confluent_server/confluent/httpapi.py +++ b/confluent_server/confluent/httpapi.py @@ -211,6 +211,8 @@ def _should_skip_authlog(env): if '/sessions/current/async' in env['PATH_INFO']: # this is effectively invisible return True + if '/sessions/current/webauthn/registered_credentials' in env['PATH_INFO']: + return True if (env['REQUEST_METHOD'] == 'GET' and ('/sensors/' in env['PATH_INFO'] or '/health/' in env['PATH_INFO'] or @@ -274,11 +276,18 @@ def _authorize_request(env, operation): authdata = None name = '' sessionid = None + sessid = None cookie = Cookie.SimpleCookie() element = env['PATH_INFO'] if element.startswith('/sessions/current/'): - element = None - if 'HTTP_COOKIE' in env: + if (element.startswith('/sessions/current/webauthn/registered_credentials/') + or element.startswith('/sessions/current/webauthn/validate/')): + username = element.rsplit('/')[-1] + element = element.replace('/' + username, '') + authdata = auth.authorize(name, element=element, operation=operation) + else: + element = None + if (not authdata) and 'HTTP_COOKIE' in env: cidx = (env['HTTP_COOKIE']).find('confluentsessionid=') if cidx >= 0: sessionid = env['HTTP_COOKIE'][cidx+19:cidx+51] @@ -356,11 +365,11 @@ def _authorize_request(env, operation): auditmsg['user'] = util.stringify(authdata[2]) if sessid is not None: authinfo['sessionid'] = sessid + if 'csrftoken' in httpsessions[sessid]: + authinfo['authtoken'] = httpsessions[sessid]['csrftoken'] + httpsessions[sessid]['cfgmgr'] = authdata[1] if not skiplog: auditlog.log(auditmsg) - if 'csrftoken' in httpsessions[sessid]: - authinfo['authtoken'] = httpsessions[sessid]['csrftoken'] - httpsessions[sessid]['cfgmgr'] = authdata[1] return authinfo elif authdata is None: return {'code': 401} @@ -636,7 +645,7 @@ def resourcehandler_backend(env, start_response): raise Exception("Unrecognized code from auth engine") headers.extend( ("Set-Cookie", m.OutputString()) - for m in authorized['cookie'].values()) + for m in authorized.get('cookie', {}).values()) cfgmgr = authorized['cfgmgr'] if (operation == 'create') and env['PATH_INFO'] == '/sessions/current/async': pagecontent = "" @@ -839,7 +848,7 @@ def resourcehandler_backend(env, start_response): start_response('501 Not Implemented', headers) yield '' return - for rsp in webauthn.handle_api_request(url, env, start_response, authorized['username'], cfgmgr, headers): + for rsp in webauthn.handle_api_request(url, env, start_response, authorized['username'], cfgmgr, headers, reqbody): yield rsp return resource = '.' + url[url.rindex('/'):] diff --git a/confluent_server/confluent/webauthn.py b/confluent_server/confluent/webauthn.py index 055cc1cf..9c5c362a 100644 --- a/confluent_server/confluent/webauthn.py +++ b/confluent_server/confluent/webauthn.py @@ -1,3 +1,4 @@ +import base64 import confluent.util as util import json import pywarp @@ -11,9 +12,13 @@ class TestBackend(pywarp.backends.CredentialStorageBackend): pass def get_credential_by_email(self, email): + if not isinstance(email, str): + email = email.decode('utf8') return creds[email] def save_credential_for_user(self, email, credential): + if not isinstance(email, str): + email = email.decode('utf8') creds[email] = credential def save_challenge_for_user(self, email, challenge, type): @@ -23,15 +28,12 @@ class TestBackend(pywarp.backends.CredentialStorageBackend): return challenges[email] -def handle_api_request(url, env, start_response, username, cfm, headers): +def handle_api_request(url, env, start_response, username, cfm, headers, reqbody): if env['REQUEST_METHOD'] != 'POST': raise Exception('Only POST supported for webauthn operations') url = url.replace('/sessions/current/webauthn', '') - if'CONTENT_LENGTH' in env and int(env['CONTENT_LENGTH']) > 0: - reqbody = env['wsgi.input'].read(int(env['CONTENT_LENGTH'])) - reqtype = env['CONTENT_TYPE'] if url == '/registration_options': - rp = pywarp.RelyingPartyManager('Confluent Web UI', credential_storage_backend=TestBackend()) + rp = pywarp.RelyingPartyManager('Confluent Web UI', credential_storage_backend=TestBackend(), require_attestation=False) userinfo = cfm.get_user(username) if not userinfo: cfm.create_user(username, role='Stub') @@ -47,5 +49,36 @@ def handle_api_request(url, env, start_response, username, cfm, headers): opts['user']['id'] = authid if 'icon' in opts['user']: del opts['user']['icon'] + if 'id' in opts['rp']: + del opts['rp']['id'] start_response('200 OK', headers) yield json.dumps(opts) + elif url.startswith('/registered_credentials/'): + username = url.rsplit('/', 1)[-1] + rp = pywarp.RelyingPartyManager('Confluent Web UI', credential_storage_backend=TestBackend()) + if not isinstance(username, bytes): + username = username.encode('utf8') + opts = rp.get_authentication_options(username) + opts['challenge'] = base64.b64encode(opts['challenge']).decode('utf8') + start_response('200 OK', headers) + yield json.dumps(opts) + elif url == '/validate': + rp = pywarp.RelyingPartyManager('Confluent Web UI', credential_storage_backend=TestBackend()) + req = json.loads(reqbody) + for x in req: + req[x] = base64.b64decode(req[x].replace('-', '+').replace('_', '/')) + req['email'] = username + rsp = rp.verify(**req) + start_response('200 OK') + yield json.dumps(rsp) + elif url == '/register_credential': + rp = pywarp.RelyingPartyManager('Confluent Web UI', credential_storage_backend=TestBackend(), require_attestation=False) + req = json.loads(reqbody) + for x in req: + req[x] = base64.b64decode(req[x].replace('-', '+').replace('_', '/')) + if not isinstance(username, bytes): + username = username.encode('utf8') + req['email'] = username + rsp = rp.register(**req) + start_response('200 OK', headers) + yield json.dumps(rsp) \ No newline at end of file From e0079b5a86185c774ea7eda73f139c8a457e2435 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Wed, 25 May 2022 10:58:02 -0400 Subject: [PATCH 19/31] Amend webauthn validation api --- confluent_server/confluent/webauthn.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/confluent_server/confluent/webauthn.py b/confluent_server/confluent/webauthn.py index 9c5c362a..731b33e5 100644 --- a/confluent_server/confluent/webauthn.py +++ b/confluent_server/confluent/webauthn.py @@ -62,7 +62,10 @@ def handle_api_request(url, env, start_response, username, cfm, headers, reqbody opts['challenge'] = base64.b64encode(opts['challenge']).decode('utf8') start_response('200 OK', headers) yield json.dumps(opts) - elif url == '/validate': + elif url.startswith('/validate/'): + username = url.rsplit('/', 1)[-1] + if not isinstance(username, bytes): + username = username.encode('utf8') rp = pywarp.RelyingPartyManager('Confluent Web UI', credential_storage_backend=TestBackend()) req = json.loads(reqbody) for x in req: From 4773acb37d642a3b5d7be5d9e75f0bf3562d92ed Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Wed, 25 May 2022 10:58:35 -0400 Subject: [PATCH 20/31] Adapt geist to work with python2 --- confluent_server/confluent/plugins/hardwaremanagement/geist.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/confluent_server/confluent/plugins/hardwaremanagement/geist.py b/confluent_server/confluent/plugins/hardwaremanagement/geist.py index 7490965a..8f977eb5 100644 --- a/confluent_server/confluent/plugins/hardwaremanagement/geist.py +++ b/confluent_server/confluent/plugins/hardwaremanagement/geist.py @@ -120,4 +120,5 @@ def update(nodes, element, configmanager, inputdata): gc = GeistClient(node, configmanager) newstate = inputdata.powerstate(node) gc.set_outlet(element[-1], newstate) - return retrieve(nodes, element, configmanager, inputdata) \ No newline at end of file + for res in retrieve(nodes, element, configmanager, inputdata): + yield res From f6a17b5f32fdb21e5ad47a18df0d79d2c72a290a Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Wed, 25 May 2022 15:58:20 -0400 Subject: [PATCH 21/31] Have validate serve as session info request This should facilitate login. Further, provide a quick persistence for the credential test backend --- confluent_server/confluent/auth.py | 2 +- confluent_server/confluent/httpapi.py | 43 ++++++++++++++++---------- confluent_server/confluent/webauthn.py | 36 ++++++++++++++++++--- 3 files changed, 58 insertions(+), 23 deletions(-) diff --git a/confluent_server/confluent/auth.py b/confluent_server/confluent/auth.py index 02828f36..6ded2e70 100644 --- a/confluent_server/confluent/auth.py +++ b/confluent_server/confluent/auth.py @@ -162,7 +162,7 @@ def authorize(name, element, tenant=False, operation='create', return False manager = configmanager.ConfigManager(tenant, username=user) userobj = manager.get_user(user) - if element in ('/sessions/current/webauthn/registered_credentials', '/sessions/current/webauthn/validate'): + if element.startswith('/sessions/current/webauthn/registered_credentials/') or element.startswith('/sessions/current/webauthn/validate/'): return userobj, manager, user, tenant, skipuserobj if userobj and userobj.get('role', None) == 'Stub': userobj = None diff --git a/confluent_server/confluent/httpapi.py b/confluent_server/confluent/httpapi.py index ece18980..ec80a095 100644 --- a/confluent_server/confluent/httpapi.py +++ b/confluent_server/confluent/httpapi.py @@ -269,7 +269,7 @@ def _csrf_valid(env, session): env['HTTP_CONFLUENTAUTHTOKEN'] == session['csrftoken']) -def _authorize_request(env, operation): +def _authorize_request(env, operation, reqbody): """Grant/Deny access based on data from wsgi env """ @@ -282,8 +282,7 @@ def _authorize_request(env, operation): if element.startswith('/sessions/current/'): if (element.startswith('/sessions/current/webauthn/registered_credentials/') or element.startswith('/sessions/current/webauthn/validate/')): - username = element.rsplit('/')[-1] - element = element.replace('/' + username, '') + name = element.rsplit('/')[-1] authdata = auth.authorize(name, element=element, operation=operation) else: element = None @@ -335,18 +334,13 @@ def _authorize_request(env, operation): return {'code': 403} elif not authdata: return {'code': 401} - sessid = util.randomstring(32) - while sessid in httpsessions: - sessid = util.randomstring(32) - httpsessions[sessid] = {'name': name, 'expiry': time.time() + 90, - 'skipuserobject': authdata[4], - 'inflight': set([])} - if 'HTTP_CONFLUENTAUTHTOKEN' in env: - httpsessions[sessid]['csrftoken'] = util.randomstring(32) - cookie['confluentsessionid'] = util.stringify(sessid) - cookie['confluentsessionid']['secure'] = 1 - cookie['confluentsessionid']['httponly'] = 1 - cookie['confluentsessionid']['path'] = '/' + sessid = _establish_http_session(env, authdata, name, cookie) + if authdata and element and element.startswith('/sessions/current/webauthn/validate/'): + if webauthn: + for rsp in webauthn.handle_api_request(element, env, None, authdata[2], authdata[1], None, reqbody, None): + if rsp['verified']: + sessid = _establish_http_session(env, authdata, name, cookie) + break skiplog = _should_skip_authlog(env) if authdata: auditmsg = { @@ -376,6 +370,21 @@ def _authorize_request(env, operation): else: return {'code': 403} +def _establish_http_session(env, authdata, name, cookie): + sessid = util.randomstring(32) + while sessid in httpsessions: + sessid = util.randomstring(32) + httpsessions[sessid] = {'name': name, 'expiry': time.time() + 90, + 'skipuserobject': authdata[4], + 'inflight': set([])} + if 'HTTP_CONFLUENTAUTHTOKEN' in env: + httpsessions[sessid]['csrftoken'] = util.randomstring(32) + cookie['confluentsessionid'] = util.stringify(sessid) + cookie['confluentsessionid']['secure'] = 1 + cookie['confluentsessionid']['httponly'] = 1 + cookie['confluentsessionid']['path'] = '/' + return sessid + def _pick_mimetype(env): """Detect the http indicated mime to send back. @@ -616,7 +625,7 @@ def resourcehandler_backend(env, start_response): if operation != 'retrieve' and 'restexplorerop' in querydict: operation = querydict['restexplorerop'] del querydict['restexplorerop'] - authorized = _authorize_request(env, operation) + authorized = _authorize_request(env, operation, reqbody) if 'logout' in authorized: start_response('200 Successful logout', headers) yield('{"result": "200 - Successful logout"}') @@ -848,7 +857,7 @@ def resourcehandler_backend(env, start_response): start_response('501 Not Implemented', headers) yield '' return - for rsp in webauthn.handle_api_request(url, env, start_response, authorized['username'], cfgmgr, headers, reqbody): + for rsp in webauthn.handle_api_request(url, env, start_response, authorized['username'], cfgmgr, headers, reqbody, authorized): yield rsp return resource = '.' + url[url.rindex('/'):] diff --git a/confluent_server/confluent/webauthn.py b/confluent_server/confluent/webauthn.py index 731b33e5..a11984c7 100644 --- a/confluent_server/confluent/webauthn.py +++ b/confluent_server/confluent/webauthn.py @@ -1,34 +1,51 @@ import base64 +import confluent.tlvdata as tlvdata import confluent.util as util import json import pywarp import pywarp.backends +import pywarp.credentials creds = {} challenges = {} class TestBackend(pywarp.backends.CredentialStorageBackend): def __init__(self): - pass + global creds + try: + with open('/tmp/mycreds.json', 'r') as ji: + creds = json.load(ji) + except Exception: + pass def get_credential_by_email(self, email): if not isinstance(email, str): email = email.decode('utf8') - return creds[email] + cred = creds[email] + cid = base64.b64decode(cred['cid']) + cpk = base64.b64decode(cred['cpk']) + return pywarp.credentials.Credential(credential_id=cid, credential_public_key=cpk) def save_credential_for_user(self, email, credential): if not isinstance(email, str): email = email.decode('utf8') + credential = {'cid': base64.b64encode(credential.id).decode('utf8'), 'cpk': base64.b64encode(bytes(credential.public_key)).decode('utf8')} creds[email] = credential + with open('/tmp/mycreds.json', 'w') as jo: + json.dump(creds, jo) def save_challenge_for_user(self, email, challenge, type): + if not isinstance(email, str): + email = email.decode('utf8') challenges[email] = challenge def get_challenge_for_user(self, email, type): + if not isinstance(email, str): + email = email.decode('utf8') return challenges[email] -def handle_api_request(url, env, start_response, username, cfm, headers, reqbody): +def handle_api_request(url, env, start_response, username, cfm, headers, reqbody, authorized): if env['REQUEST_METHOD'] != 'POST': raise Exception('Only POST supported for webauthn operations') url = url.replace('/sessions/current/webauthn', '') @@ -72,8 +89,17 @@ def handle_api_request(url, env, start_response, username, cfm, headers, reqbody req[x] = base64.b64decode(req[x].replace('-', '+').replace('_', '/')) req['email'] = username rsp = rp.verify(**req) - start_response('200 OK') - yield json.dumps(rsp) + if start_response: + start_response('200 OK', headers) + sessinfo = {'username': username} + if 'authtoken' in authorized: + sessinfo['authtoken'] = authorized['authtoken'] + if 'sessionid' in authorized: + sessinfo['sessionid'] = authorized['sessionid'] + tlvdata.unicode_dictvalues(sessinfo) + yield json.dumps(sessinfo) + else: + yield rsp elif url == '/register_credential': rp = pywarp.RelyingPartyManager('Confluent Web UI', credential_storage_backend=TestBackend(), require_attestation=False) req = json.loads(reqbody) From 3c3d6bb314688a71981ac10032cb2e7e248ac253 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Wed, 25 May 2022 17:40:35 -0400 Subject: [PATCH 22/31] Fix auth handling of the session/info --- confluent_server/confluent/auth.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/confluent_server/confluent/auth.py b/confluent_server/confluent/auth.py index 6ded2e70..ce8cfd49 100644 --- a/confluent_server/confluent/auth.py +++ b/confluent_server/confluent/auth.py @@ -162,7 +162,7 @@ def authorize(name, element, tenant=False, operation='create', return False manager = configmanager.ConfigManager(tenant, username=user) userobj = manager.get_user(user) - if element.startswith('/sessions/current/webauthn/registered_credentials/') or element.startswith('/sessions/current/webauthn/validate/'): + if element and (element.startswith('/sessions/current/webauthn/registered_credentials/') or element.startswith('/sessions/current/webauthn/validate/')): return userobj, manager, user, tenant, skipuserobj if userobj and userobj.get('role', None) == 'Stub': userobj = None From 9b6114f5233165eba8094546923580886d71bb1f Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Thu, 26 May 2022 15:01:47 -0400 Subject: [PATCH 23/31] Break if stuck in loop for over a minute --- confluent_server/confluent/plugins/hardwaremanagement/ipmi.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/confluent_server/confluent/plugins/hardwaremanagement/ipmi.py b/confluent_server/confluent/plugins/hardwaremanagement/ipmi.py index b5220791..99b94021 100644 --- a/confluent_server/confluent/plugins/hardwaremanagement/ipmi.py +++ b/confluent_server/confluent/plugins/hardwaremanagement/ipmi.py @@ -514,8 +514,11 @@ class IpmiHandler(object): raise exc.TargetEndpointUnreachable(ge.strerror) raise self.ipmicmd = persistent_ipmicmds[(node, tenant)] + giveup = util._monotonic_time() + 60 while not self.ipmicmd.ipmi_session.broken and not self.ipmicmd.ipmi_session.logged: self.ipmicmd.ipmi_session.wait_for_rsp(3) + if util._monotonic_time() > giveup: + self.ipmicmd.ipmi_session.broken = True bootdevices = { 'optical': 'cd' From 7072a85d794813e8fc6086f05179d1962eab2e84 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Thu, 26 May 2022 16:34:18 -0400 Subject: [PATCH 24/31] Transition to multi-authenticator support Provide a way to store a plurality of keys for a user. This enables use of 'backup' authenticators. --- confluent_server/confluent/webauthn.py | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/confluent_server/confluent/webauthn.py b/confluent_server/confluent/webauthn.py index a11984c7..17d63579 100644 --- a/confluent_server/confluent/webauthn.py +++ b/confluent_server/confluent/webauthn.py @@ -18,6 +18,20 @@ class TestBackend(pywarp.backends.CredentialStorageBackend): except Exception: pass + def get_credential_ids_by_email(self, email): + if not isinstance(email, str): + email = email.decode('utf8') + for cid in creds[email]: + yield base64.b64decode(cid) + + def get_credential_by_email_id(self, email, id): + if not isinstance(email, str): + email = email.decode('utf8') + cid = base64.b64encode(id).decode('utf8') + pk = creds[email][cid]['cpk'] + pk = base64.b64decode(pk) + return pywarp.credentials.Credential(credential_id=id, credential_public_key=pk) + def get_credential_by_email(self, email): if not isinstance(email, str): email = email.decode('utf8') @@ -29,8 +43,11 @@ class TestBackend(pywarp.backends.CredentialStorageBackend): def save_credential_for_user(self, email, credential): if not isinstance(email, str): email = email.decode('utf8') - credential = {'cid': base64.b64encode(credential.id).decode('utf8'), 'cpk': base64.b64encode(bytes(credential.public_key)).decode('utf8')} - creds[email] = credential + cid = base64.b64encode(credential.id).decode('utf8') + credential = {'cid': cid, 'cpk': base64.b64encode(bytes(credential.public_key)).decode('utf8')} + if email not in creds: + creds[email] = {} + creds[email][cid] = credential with open('/tmp/mycreds.json', 'w') as jo: json.dump(creds, jo) From b638bb69f9034695c77bdcca36b6d981dac0de1d Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Thu, 26 May 2022 17:41:16 -0400 Subject: [PATCH 25/31] Change webauthn to use confluent user db This allows persistence with the rest of the configuration. --- .../confluent/config/configmanager.py | 8 ++-- confluent_server/confluent/webauthn.py | 40 +++++++++---------- 2 files changed, 23 insertions(+), 25 deletions(-) diff --git a/confluent_server/confluent/config/configmanager.py b/confluent_server/confluent/config/configmanager.py index c9a8d937..e88e9483 100644 --- a/confluent_server/confluent/config/configmanager.py +++ b/confluent_server/confluent/config/configmanager.py @@ -2447,10 +2447,10 @@ class ConfigManager(object): 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) + for attrname in ('authid', 'authenticators', 'cryptpass'): + if attrname in tmpconfig[confarea][user]: + self._cfgstore['users'][user][attrname] = tmpconfig[confarea][user][attrname] + _mark_dirtykey('users', user, self.tenant) if sync: self._bg_sync_to_file() diff --git a/confluent_server/confluent/webauthn.py b/confluent_server/confluent/webauthn.py index 17d63579..7e3b148f 100644 --- a/confluent_server/confluent/webauthn.py +++ b/confluent_server/confluent/webauthn.py @@ -6,36 +6,36 @@ import pywarp import pywarp.backends import pywarp.credentials -creds = {} challenges = {} -class TestBackend(pywarp.backends.CredentialStorageBackend): - def __init__(self): - global creds - try: - with open('/tmp/mycreds.json', 'r') as ji: - creds = json.load(ji) - except Exception: - pass +class ConfluentBackend(pywarp.backends.CredentialStorageBackend): + def __init__(self, cfg): + self.cfg = cfg def get_credential_ids_by_email(self, email): if not isinstance(email, str): email = email.decode('utf8') - for cid in creds[email]: + authenticators = self.cfg.get_user(email).get('authenticators', {}) + if not authenticators: + raise Exception('No authenticators found') + for cid in authenticators: yield base64.b64decode(cid) def get_credential_by_email_id(self, email, id): if not isinstance(email, str): email = email.decode('utf8') + authenticators = self.cfg.get_user(email).get('authenticators', {}) cid = base64.b64encode(id).decode('utf8') - pk = creds[email][cid]['cpk'] + pk = authenticators[cid]['cpk'] pk = base64.b64decode(pk) return pywarp.credentials.Credential(credential_id=id, credential_public_key=pk) def get_credential_by_email(self, email): if not isinstance(email, str): email = email.decode('utf8') - cred = creds[email] + authenticators = self.cfg.get_user(email) + cid = list(authenticators)[0] + cred = authenticators[cid] cid = base64.b64decode(cred['cid']) cpk = base64.b64decode(cred['cpk']) return pywarp.credentials.Credential(credential_id=cid, credential_public_key=cpk) @@ -45,11 +45,9 @@ class TestBackend(pywarp.backends.CredentialStorageBackend): email = email.decode('utf8') cid = base64.b64encode(credential.id).decode('utf8') credential = {'cid': cid, 'cpk': base64.b64encode(bytes(credential.public_key)).decode('utf8')} - if email not in creds: - creds[email] = {} - creds[email][cid] = credential - with open('/tmp/mycreds.json', 'w') as jo: - json.dump(creds, jo) + authenticators = self.cfg.get_user(email).get('authenticators', {}) + authenticators[cid] = credential + self.cfg.set_user(email, {'authenticators': authenticators}) def save_challenge_for_user(self, email, challenge, type): if not isinstance(email, str): @@ -67,7 +65,7 @@ def handle_api_request(url, env, start_response, username, cfm, headers, reqbody raise Exception('Only POST supported for webauthn operations') url = url.replace('/sessions/current/webauthn', '') if url == '/registration_options': - rp = pywarp.RelyingPartyManager('Confluent Web UI', credential_storage_backend=TestBackend(), require_attestation=False) + rp = pywarp.RelyingPartyManager('Confluent Web UI', credential_storage_backend=ConfluentBackend(cfm), require_attestation=False) userinfo = cfm.get_user(username) if not userinfo: cfm.create_user(username, role='Stub') @@ -89,7 +87,7 @@ def handle_api_request(url, env, start_response, username, cfm, headers, reqbody yield json.dumps(opts) elif url.startswith('/registered_credentials/'): username = url.rsplit('/', 1)[-1] - rp = pywarp.RelyingPartyManager('Confluent Web UI', credential_storage_backend=TestBackend()) + rp = pywarp.RelyingPartyManager('Confluent Web UI', credential_storage_backend=ConfluentBackend(cfm)) if not isinstance(username, bytes): username = username.encode('utf8') opts = rp.get_authentication_options(username) @@ -100,7 +98,7 @@ def handle_api_request(url, env, start_response, username, cfm, headers, reqbody username = url.rsplit('/', 1)[-1] if not isinstance(username, bytes): username = username.encode('utf8') - rp = pywarp.RelyingPartyManager('Confluent Web UI', credential_storage_backend=TestBackend()) + rp = pywarp.RelyingPartyManager('Confluent Web UI', credential_storage_backend=ConfluentBackend(cfm)) req = json.loads(reqbody) for x in req: req[x] = base64.b64decode(req[x].replace('-', '+').replace('_', '/')) @@ -118,7 +116,7 @@ def handle_api_request(url, env, start_response, username, cfm, headers, reqbody else: yield rsp elif url == '/register_credential': - rp = pywarp.RelyingPartyManager('Confluent Web UI', credential_storage_backend=TestBackend(), require_attestation=False) + rp = pywarp.RelyingPartyManager('Confluent Web UI', credential_storage_backend=ConfluentBackend(cfm), require_attestation=False) req = json.loads(reqbody) for x in req: req[x] = base64.b64decode(req[x].replace('-', '+').replace('_', '/')) From 9c7e23f29e1913b9b2ce93e72d7a3320b58d1676 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Thu, 26 May 2022 17:41:54 -0400 Subject: [PATCH 26/31] Add missing nodepower statuses --- confluent_client/bin/nodepower | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/confluent_client/bin/nodepower b/confluent_client/bin/nodepower index dffa241c..ccbf41bf 100755 --- a/confluent_client/bin/nodepower +++ b/confluent_client/bin/nodepower @@ -55,7 +55,7 @@ if len(args) > 1: if setstate == 'softoff': setstate = 'shutdown' -if setstate not in (None, 'on', 'off', 'shutdown', 'boot', 'reset', 'pdu_status', 'pdu_stat', 'pdu_on', 'pdu_off'): +if setstate not in (None, 'on', 'off', 'shutdown', 'boot', 'reset', 'pdu_status', 'pdu_stat', 'pdu_on', 'pdu_off', 'status', 'stat', 'state'): argparser.print_help() sys.exit(1) session = client.Command() From e4e859c798dfc9eff85de1150bff275bdfce10d4 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Thu, 26 May 2022 18:07:19 -0400 Subject: [PATCH 27/31] Fix invocation of monotonic_time --- confluent_server/confluent/plugins/hardwaremanagement/ipmi.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/confluent_server/confluent/plugins/hardwaremanagement/ipmi.py b/confluent_server/confluent/plugins/hardwaremanagement/ipmi.py index 99b94021..923607cc 100644 --- a/confluent_server/confluent/plugins/hardwaremanagement/ipmi.py +++ b/confluent_server/confluent/plugins/hardwaremanagement/ipmi.py @@ -514,10 +514,10 @@ class IpmiHandler(object): raise exc.TargetEndpointUnreachable(ge.strerror) raise self.ipmicmd = persistent_ipmicmds[(node, tenant)] - giveup = util._monotonic_time() + 60 + giveup = util.monotonic_time() + 60 while not self.ipmicmd.ipmi_session.broken and not self.ipmicmd.ipmi_session.logged: self.ipmicmd.ipmi_session.wait_for_rsp(3) - if util._monotonic_time() > giveup: + if util.monotonic_time() > giveup: self.ipmicmd.ipmi_session.broken = True bootdevices = { From 6d07dac46cabda2c4e06d1d7484eea3d466b85fc Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Fri, 27 May 2022 16:10:08 -0400 Subject: [PATCH 28/31] Fix ipmi missing logging state and trim el9 dependencies --- confluent_server/confluent/plugins/hardwaremanagement/ipmi.py | 2 +- confluent_server/confluent_server.spec.tmpl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/confluent_server/confluent/plugins/hardwaremanagement/ipmi.py b/confluent_server/confluent/plugins/hardwaremanagement/ipmi.py index 923607cc..4fbd9e80 100644 --- a/confluent_server/confluent/plugins/hardwaremanagement/ipmi.py +++ b/confluent_server/confluent/plugins/hardwaremanagement/ipmi.py @@ -515,7 +515,7 @@ class IpmiHandler(object): raise self.ipmicmd = persistent_ipmicmds[(node, tenant)] giveup = util.monotonic_time() + 60 - while not self.ipmicmd.ipmi_session.broken and not self.ipmicmd.ipmi_session.logged: + while not self.ipmicmd.ipmi_session.broken and not self.ipmicmd.ipmi_session.logged and self.ipmicmd.ipmi_session.logging: self.ipmicmd.ipmi_session.wait_for_rsp(3) if util.monotonic_time() > giveup: self.ipmicmd.ipmi_session.broken = True diff --git a/confluent_server/confluent_server.spec.tmpl b/confluent_server/confluent_server.spec.tmpl index eef95ae0..63abd54d 100644 --- a/confluent_server/confluent_server.spec.tmpl +++ b/confluent_server/confluent_server.spec.tmpl @@ -17,7 +17,7 @@ Requires: confluent_vtbufferd Requires: python3-pyghmi >= 1.0.34, python3-eventlet, python3-greenlet, python3-pycryptodomex >= 3.4.7, confluent_client == %{version}, python3-pyparsing, python3-paramiko, python3-dns, python3-netifaces, python3-pyasn1 >= 0.2.3, python3-pysnmp >= 4.3.4, python3-lxml, python3-eficompressor, python3-setuptools, python3-dateutil, python3-enum34, python3-asn1crypto, python3-cffi, python3-pyOpenSSL, python3-monotonic, python3-websocket-client python3-msgpack python3-libarchive-c python3-yaml openssl iproute %else %if "%{dist}" == ".el9" -Requires: python3-pyghmi >= 1.0.34, python3-eventlet, python3-greenlet, python3-pycryptodomex >= 3.4.7, confluent_client == %{version}, python3-pyparsing, python3-paramiko, python3-dns, python3-netifaces, python3-pyasn1 >= 0.2.3, python3-pysnmp >= 4.3.4, python3-lxml, python3-eficompressor, python3-setuptools, python3-dateutil, python3-enum34, python3-asn1crypto, python3-cffi, python3-pyOpenSSL, python3-monotonic, python3-websocket-client python3-msgpack python3-libarchive-c python3-yaml openssl iproute +Requires: python3-pyghmi >= 1.0.34, python3-eventlet, python3-greenlet, python3-pycryptodomex >= 3.4.7, confluent_client == %{version}, python3-pyparsing, python3-paramiko, python3-dns, python3-netifaces, python3-pyasn1 >= 0.2.3, python3-pysnmp >= 4.3.4, python3-lxml, python3-eficompressor, python3-setuptools, python3-dateutil, python3-cffi, python3-pyOpenSSL, python3-websocket-client python3-msgpack python3-libarchive-c python3-yaml openssl iproute %else Requires: python-pyghmi >= 1.0.34, python-eventlet, python-greenlet, python-pycryptodomex >= 3.4.7, confluent_client == %{version}, python-pyparsing, python-paramiko, python-dnspython, python-netifaces, python2-pyasn1 >= 0.2.3, python-pysnmp >= 4.3.4, python-lxml, python-eficompressor, python-setuptools, python-dateutil, python2-websocket-client python2-msgpack python-libarchive-c python-yaml python-monotonic %endif From b5f5ba69d264a41fac61876c4baba4b60b165733 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Tue, 31 May 2022 10:18:42 -0400 Subject: [PATCH 29/31] Fix python 3.9 compatibility Python 3.9 renamed an attribute on a threading object, try both names to support old and new python --- confluent_server/confluent/config/configmanager.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/confluent_server/confluent/config/configmanager.py b/confluent_server/confluent/config/configmanager.py index e88e9483..8ba05c2e 100644 --- a/confluent_server/confluent/config/configmanager.py +++ b/confluent_server/confluent/config/configmanager.py @@ -2548,8 +2548,12 @@ class ConfigManager(object): if statelessmode: return with cls._syncstate: + try: + isalive = cls._cfgwriter.isAlive() + except AttributeError: + isalive = cls._cfgwriter.is_alive() if (cls._syncrunning and cls._cfgwriter is not None and - cls._cfgwriter.isAlive()): + isalive): cls._writepending = True return if cls._syncrunning: # This suggests an unclean write attempt, From b42dafb085d8649f83a4e9e032dcbf72aa4d3855 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Tue, 31 May 2022 11:01:09 -0400 Subject: [PATCH 30/31] Fix python 3.9 compatibility In python 3.9, the % is no longer included in the first member of the address tuple. Handle both scenarios by adding '%' in for versions that seem to want to remove it. --- confluent_server/confluent/discovery/protocols/ssdp.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/confluent_server/confluent/discovery/protocols/ssdp.py b/confluent_server/confluent/discovery/protocols/ssdp.py index d483e7fc..762b643a 100644 --- a/confluent_server/confluent/discovery/protocols/ssdp.py +++ b/confluent_server/confluent/discovery/protocols/ssdp.py @@ -281,6 +281,8 @@ def snoop(handler, byehandler=None, protocol=None, uuidlookup=None): def _get_svrip(peerdata): for addr in peerdata['addresses']: if addr[0].startswith('fe80::'): + if '%' not in addr[0]: + return addr[0] + '%{0}'.format(addr[3]) return addr[0] return peerdata['addresses'][0][0] From 83acdab2167ec8752b9f162d69027833e211d1ba Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Tue, 31 May 2022 11:23:27 -0400 Subject: [PATCH 31/31] Amend python 3.9 compatibility fix Avoid checking None for is_alive or isAlive --- confluent_server/confluent/config/configmanager.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/confluent_server/confluent/config/configmanager.py b/confluent_server/confluent/config/configmanager.py index 8ba05c2e..2ccce28b 100644 --- a/confluent_server/confluent/config/configmanager.py +++ b/confluent_server/confluent/config/configmanager.py @@ -2548,12 +2548,13 @@ class ConfigManager(object): if statelessmode: return with cls._syncstate: - try: - isalive = cls._cfgwriter.isAlive() - except AttributeError: - isalive = cls._cfgwriter.is_alive() - if (cls._syncrunning and cls._cfgwriter is not None and - isalive): + isalive = False + if cls._cfgwriter is not None: + try: + isalive = cls._cfgwriter.isAlive() + except AttributeError: + isalive = cls._cfgwriter.is_alive() + if (cls._syncrunning and isalive): cls._writepending = True return if cls._syncrunning: # This suggests an unclean write attempt,