From 334ce65919405fec3ef1e57e96e90313ea56f65c Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Wed, 20 Apr 2022 17:11:01 -0400 Subject: [PATCH 01/13] Add facility for forwarding agent Remote discovery will require that the reporting agent provides port forwarding facility for https access, to allow access without route to the managing confluent server. This will allow the info reported to indicate use of this forwarding facility and the base handler adds ability to generically get the certificate. --- confluent_server/confluent/discovery/core.py | 2 + .../confluent/discovery/handlers/generic.py | 40 +++++++++++++++++-- 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/confluent_server/confluent/discovery/core.py b/confluent_server/confluent/discovery/core.py index 7ca752a4..98560465 100644 --- a/confluent_server/confluent/discovery/core.py +++ b/confluent_server/confluent/discovery/core.py @@ -429,6 +429,8 @@ def handle_api_request(configmanager, inputdata, operation, pathcomponents): return (msg.KeyValueData({'rescan': 'started'}),) elif operation in ('update', 'create'): + if pathcomponents == ['discovery', 'register']: + return if 'node' not in inputdata: raise exc.InvalidArgumentException('Missing node name in input') mac = _get_mac_from_query(pathcomponents) diff --git a/confluent_server/confluent/discovery/handlers/generic.py b/confluent_server/confluent/discovery/handlers/generic.py index aca0f864..d38058c9 100644 --- a/confluent_server/confluent/discovery/handlers/generic.py +++ b/confluent_server/confluent/discovery/handlers/generic.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +import confluent.util as util import errno import eventlet import socket @@ -31,6 +32,15 @@ class NodeHandler(object): self.info = info self.configmanager = configmanager targsa = [None] + self.ipaddr = None + self.relay_url = None + self.relay_server = None + # if this is a remote registered component, prefer to use the agent forwarder + if info.get('forwarder_url', False): + self.relay_url = info['forwarder_url'] + self.relay_server = info['forwarder_server'] + self.relay_token = info['forwarder_token'] + return # first let us prefer LLA if possible, since that's most stable for sa in info['addresses']: if sa[0].startswith('fe80'): @@ -103,11 +113,33 @@ class NodeHandler(object): def https_cert(self): if self._fp: return self._fp - if ':' in self.ipaddr: - ip = '[{0}]'.format(self.ipaddr) + if self.relay_url: + kv = util.TLSCertVerifier(self.configmanager, self.relay_server, + 'pubkeys.tls_hardwaremanager').verify_cert + w = webclient.SecureHTTPConnection(self.relay_server, verifycallback=kv) + relaycreds = self.configmanager.get_node_attributes(self.relay_server, 'secret.*', decrypt=True) + relaycreds = relaycreds.get(self.relay_server, {}) + relayuser = relaycreds.get('secret.hardwaremanagementuser', {}).get('value', None) + relaypass = relaycreds.get('secret.hardwaremanegementpassword', {}).get('value', None) + if not relayuser or not relaypass: + raise Exception('No credentials for {0}'.format(self.relay_server)) + w.set_basic_credentials(relayuser, relaypass) + w.connect() + w.request('GET', self.relay_url) + r = w.getresponse() + rb = r.read() + if r.code != 302: + raise Exception('Unexpected return from forwarder') + newurl = r.getheader('Location') + port = int(newurl.rsplit(':', 1)[-1][:-1]) + ip = self.relay_server else: - ip = self.ipaddr - wc = webclient.SecureHTTPConnection(ip, verifycallback=self._savecert) + port = 443 + if ':' in self.ipaddr: + ip = '[{0}]'.format(self.ipaddr) + else: + ip = self.ipaddr + wc = webclient.SecureHTTPConnection(ip, verifycallback=self._savecert, port=port) try: wc.connect() except IOError as ie: From 00589d06f519b36d20c98024166694dab4f15b0b Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Thu, 21 Apr 2022 09:40:09 -0400 Subject: [PATCH 02/13] Refactor relay and add to XCC/SMM Extract relay versus direct determination and inject into XCC and SMM handlers --- .../confluent/discovery/handlers/generic.py | 52 +++++++++++-------- .../confluent/discovery/handlers/smm.py | 3 +- .../confluent/discovery/handlers/xcc.py | 6 ++- 3 files changed, 35 insertions(+), 26 deletions(-) diff --git a/confluent_server/confluent/discovery/handlers/generic.py b/confluent_server/confluent/discovery/handlers/generic.py index d38058c9..db1d0611 100644 --- a/confluent_server/confluent/discovery/handlers/generic.py +++ b/confluent_server/confluent/discovery/handlers/generic.py @@ -35,6 +35,8 @@ class NodeHandler(object): self.ipaddr = None self.relay_url = None self.relay_server = None + self.web_ip = None + self.web_port = None # if this is a remote registered component, prefer to use the agent forwarder if info.get('forwarder_url', False): self.relay_url = info['forwarder_url'] @@ -113,6 +115,28 @@ class NodeHandler(object): def https_cert(self): if self._fp: return self._fp + ip, port = self.get_web_port_and_ip() + wc = webclient.SecureHTTPConnection(ip, verifycallback=self._savecert, port=port) + try: + wc.connect() + except IOError as ie: + if ie.errno == errno.ECONNREFUSED: + self._certfailreason = 1 + return None + elif ie.errno == errno.EHOSTUNREACH: + self._certfailreason = 2 + return None + self._certfailreason = 2 + return None + except Exception: + self._certfailreason = 2 + return None + return self._fp + + def get_web_port_and_ip(self): + if self.web_ip: + return self.web_ip, self.web_port + # get target ip and port, either direct or relay as applicable if self.relay_url: kv = util.TLSCertVerifier(self.configmanager, self.relay_server, 'pubkeys.tls_hardwaremanager').verify_cert @@ -131,27 +155,9 @@ class NodeHandler(object): if r.code != 302: raise Exception('Unexpected return from forwarder') newurl = r.getheader('Location') - port = int(newurl.rsplit(':', 1)[-1][:-1]) - ip = self.relay_server + self.web_port = int(newurl.rsplit(':', 1)[-1][:-1]) + self.web_ip = self.relay_server else: - port = 443 - if ':' in self.ipaddr: - ip = '[{0}]'.format(self.ipaddr) - else: - ip = self.ipaddr - wc = webclient.SecureHTTPConnection(ip, verifycallback=self._savecert, port=port) - try: - wc.connect() - except IOError as ie: - if ie.errno == errno.ECONNREFUSED: - self._certfailreason = 1 - return None - elif ie.errno == errno.EHOSTUNREACH: - self._certfailreason = 2 - return None - self._certfailreason = 2 - return None - except Exception: - self._certfailreason = 2 - return None - return self._fp + self.web_port = 443 + self.web_ip = self.ipaddr + return self.web_ip, self.web_port diff --git a/confluent_server/confluent/discovery/handlers/smm.py b/confluent_server/confluent/discovery/handlers/smm.py index 6386812f..790ad131 100644 --- a/confluent_server/confluent/discovery/handlers/smm.py +++ b/confluent_server/confluent/discovery/handlers/smm.py @@ -135,7 +135,8 @@ class NodeHandler(bmchandler.NodeHandler): {nodename: {'hardwaremanagement.manager': self.ipaddr}}) def _webconfigcreds(self, username, password): - wc = webclient.SecureHTTPConnection(self.ipaddr, 443, verifycallback=self._validate_cert) + ip, port = self.get_web_port_and_ip() + wc = webclient.SecureHTTPConnection(ip, port, verifycallback=self._validate_cert) wc.connect() authdata = { # start by trying factory defaults 'user': 'USERID', diff --git a/confluent_server/confluent/discovery/handlers/xcc.py b/confluent_server/confluent/discovery/handlers/xcc.py index 537cf285..9fef5535 100644 --- a/confluent_server/confluent/discovery/handlers/xcc.py +++ b/confluent_server/confluent/discovery/handlers/xcc.py @@ -58,7 +58,8 @@ class NodeHandler(immhandler.NodeHandler): return None def scan(self): - c = webclient.SecureHTTPConnection(self.ipaddr, 443, + ip, port = self.get_web_port_and_ip() + c = webclient.SecureHTTPConnection(ip, port, verifycallback=self.validate_cert) i = c.grab_json_response('/api/providers/logoninfo') modelname = i.get('items', [{}])[0].get('machine_name', None) @@ -270,8 +271,9 @@ class NodeHandler(immhandler.NodeHandler): isdefault = True errinfo = {} if self._wc is None: + ip, port = self.get_web_port_and_ip() self._wc = webclient.SecureHTTPConnection( - self.ipaddr, 443, verifycallback=self.validate_cert) + ip, port, verifycallback=self.validate_cert) self._wc.connect() nodename = None if self.nodename: From 14efec36032fd6238d339d05be6d952ac5bf1791 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Mon, 25 Apr 2022 13:04:45 -0400 Subject: [PATCH 03/13] Add file to show the rpm versions at build time of genesis --- genesis/buildgenesis.sh | 12 +++++++++++- genesis/confluent-genesis.spec | 1 + 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/genesis/buildgenesis.sh b/genesis/buildgenesis.sh index 1d5998a8..014c4f25 100644 --- a/genesis/buildgenesis.sh +++ b/genesis/buildgenesis.sh @@ -5,11 +5,21 @@ chmod +x /usr/lib/dracut/modules.d/97genesis/install /usr/lib/dracut/modules.d/9 mkdir -p boot/initramfs mkdir -p boot/efi/boot dracut --no-early-microcode --xz -N -m "genesis base" -f boot/initramfs/distribution $(uname -r) +tdir=$(mktemp -d) +tfile=$(mktemp) +cp boot/initramfs/distribution $tdir +cd $tdir +xzcat distribution|cpio -dumi +rm distribution +find . -type f -exec rpm -qf /{} \; 2> /dev/null | grep -v 'not owned' | sort -u > $tfile +cd - +rm -rf $tdir +cp $tfile rpmlist cp -f /boot/vmlinuz-$(uname -r) boot/kernel cp /boot/efi/EFI/BOOT/BOOTX64.EFI boot/efi/boot cp /boot/efi/EFI/centos/grubx64.efi boot/efi/boot/grubx64.efi mkdir -p ~/rpmbuild/SOURCES/ -tar cf ~/rpmbuild/SOURCES/confluent-genesis.tar boot +tar cf ~/rpmbuild/SOURCES/confluent-genesis.tar boot rpmlist rpmbuild -bb confluent-genesis.spec rm -rf /usr/lib/dracut/modules.d/97genesis cd - diff --git a/genesis/confluent-genesis.spec b/genesis/confluent-genesis.spec index b1a24a6d..30111120 100644 --- a/genesis/confluent-genesis.spec +++ b/genesis/confluent-genesis.spec @@ -28,6 +28,7 @@ find . -type f -exec chmod o+r {} + find . -type f -exec chmod -x {} + %files +/opt/confluent/genesis/%{arch}/rpmlist /opt/confluent/genesis/%{arch}/boot/efi/boot/BOOTX64.EFI /opt/confluent/genesis/%{arch}/boot/efi/boot/grubx64.efi /opt/confluent/genesis/%{arch}/boot/initramfs/distribution From d8a2671dd6dc500222c8cc8e72bf6472df6f3b69 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Mon, 25 Apr 2022 13:11:43 -0400 Subject: [PATCH 04/13] Fix directory traversal --- genesis/buildgenesis.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/genesis/buildgenesis.sh b/genesis/buildgenesis.sh index 014c4f25..fbf82e60 100644 --- a/genesis/buildgenesis.sh +++ b/genesis/buildgenesis.sh @@ -8,11 +8,11 @@ dracut --no-early-microcode --xz -N -m "genesis base" -f boot/initramfs/distribu tdir=$(mktemp -d) tfile=$(mktemp) cp boot/initramfs/distribution $tdir -cd $tdir +pushd $tdir xzcat distribution|cpio -dumi rm distribution find . -type f -exec rpm -qf /{} \; 2> /dev/null | grep -v 'not owned' | sort -u > $tfile -cd - +popd rm -rf $tdir cp $tfile rpmlist cp -f /boot/vmlinuz-$(uname -r) boot/kernel From ee1a763c6818be031203694eba6d7ef5f8b78990 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Mon, 9 May 2022 09:57:35 -0400 Subject: [PATCH 05/13] 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 64b8893b808b86afbb2855be400a34ae8bffe16d Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Fri, 3 Jun 2022 10:24:25 -0400 Subject: [PATCH 06/13] Add a delta pdu plugin This has received limited testing, and the PDUs cannot be used with secure protocols --- .../plugins/hardwaremanagement/deltapdu.py | 182 ++++++++++++++++++ 1 file changed, 182 insertions(+) create mode 100644 confluent_server/confluent/plugins/hardwaremanagement/deltapdu.py diff --git a/confluent_server/confluent/plugins/hardwaremanagement/deltapdu.py b/confluent_server/confluent/plugins/hardwaremanagement/deltapdu.py new file mode 100644 index 00000000..f8f78627 --- /dev/null +++ b/confluent_server/confluent/plugins/hardwaremanagement/deltapdu.py @@ -0,0 +1,182 @@ +# 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 confluent.util as util +import confluent.messages as msg +import confluent.exceptions as exc +import eventlet +from xml.etree.ElementTree import fromstring as rfromstring + +def fromstring(inputdata): + if isinstance(inputdata, bytes): + cmpstr = b'!entity' + else: + cmpstr = '!entity' + if cmpstr in inputdata.lower(): + raise Exception('!ENTITY not supported in this interface') + # The measures above should filter out the risky facets of xml + # We don't need sophisticated feature support + return rfromstring(inputdata) # nosec + + +try: + import Cookie + httplib = eventlet.import_patched('httplib') +except ImportError: + httplib = eventlet.import_patched('http.client') + import http.cookies as Cookie + +# Delta PDU webserver always closes connection, +# replace conditionals with always close +class WebResponse(httplib.HTTPResponse): + def _check_close(self): + return True + +class WebConnection(httplib.HTTPConnection): + response_class = WebResponse + def __init__(self, host): + httplib.HTTPConnection.__init__(self, host, 80) + self.cookies = {} + + def getresponse(self): + try: + rsp = super(WebConnection, self).getresponse() + try: + hdrs = [x.split(':', 1) for x in rsp.msg.headers] + except AttributeError: + hdrs = rsp.msg.items() + for hdr in hdrs: + if hdr[0] == 'Set-Cookie': + c = Cookie.BaseCookie(hdr[1]) + for k in c: + self.cookies[k] = c[k].value + except httplib.BadStatusLine: + self.broken = True + raise + return rsp + + def request(self, method, url, body=None): + headers = {} + if body: + headers['Content-Length'] = len(body) + cookies = [] + for cookie in self.cookies: + cookies.append('{0}={1}'.format(cookie, self.cookies[cookie])) + headers['Cookie'] = ';'.join(cookies) + headers['Host'] = 'pdu.cluster.net' + headers['Accept'] = '*/*' + headers['Accept-Language'] = 'en-US,en;q=0.9' + headers['Connection'] = 'close' + headers['Referer'] = 'http://pdu.cluster.net/setting_admin.htm' + return super(WebConnection, self).request(method, url, body, headers) + + def grab_response(self, url, body=None, method=None): + if method is None: + method = 'GET' if body is None else 'POST' + if body: + self.request(method, url, body) + else: + self.request(method, url) + rsp = self.getresponse() + body = rsp.read() + return body, rsp.status + + + +class PDUClient(object): + def __init__(self, pdu, configmanager): + self.node = pdu + self.configmanager = configmanager + self._token = None + self._wc = None + self.username = None + + @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 + self._wc = WebConnection(target) + self.login(self.configmanager) + return self._wc + + def login(self, configmanager): + credcfg = configmanager.get_node_attributes(self.node, + ['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') + body = 'User={0}&Password={1}&B1=Login'.format(username, passwd) + self.wc.grab_response('/login.htm', body) + + + def logout(self): + self.wc.grab_response('/logout_wait.htm') + + def get_outlet(self, outlet): + rsp = self.wc.grab_response('/setting_admin4.xml') + xd = fromstring(rsp[0]) + for ch in xd: + if 'relay' not in ch.tag: + continue + outnum = ch.tag.split('relay')[-1] + if outnum == outlet: + return ch.text.lower() + + def set_outlet(self, outlet, state): + state = 0 if state == 'off' else 1 + outlet = int(outlet) + sitem = '/SetParm?item=s4r{:02d}?content={}'.format(outlet, state) + self.wc.grab_response(sitem) + +def retrieve(nodes, element, configmanager, inputdata): + if 'outlets' not in element: + for node in nodes: + yield msg.ConfluentResourceUnavailable(node, 'Not implemented') + return + for node in nodes: + gc = PDUClient(node, configmanager) + state = gc.get_outlet(element[-1]) + yield msg.PowerState(node=node, state=state) + gc.logout() + +def update(nodes, element, configmanager, inputdata): + if 'outlets' not in element: + yield msg.ConfluentResourceUnavailable(node, 'Not implemented') + return + for node in nodes: + gc = PDUClient(node, configmanager) + newstate = inputdata.powerstate(node) + gc.set_outlet(element[-1], newstate) + gc.logout() + eventlet.sleep(2) + for res in retrieve(nodes, element, configmanager, inputdata): + yield res From 04c2b1a3228c1ee3ae81e539d05d6c223782b73e Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Wed, 22 Jun 2022 16:47:40 -0400 Subject: [PATCH 07/13] Provide an authenticated path for discovery registration --- confluent_server/confluent/selfservice.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/confluent_server/confluent/selfservice.py b/confluent_server/confluent/selfservice.py index c3548cb0..687e02a0 100644 --- a/confluent_server/confluent/selfservice.py +++ b/confluent_server/confluent/selfservice.py @@ -10,6 +10,7 @@ import eventlet.green.socket as socket import eventlet.green.subprocess as subprocess import confluent.discovery.handlers.xcc as xcc import confluent.discovery.handlers.tsm as tsm +import confluent.discovery.core as disco import base64 import hmac import hashlib @@ -112,7 +113,6 @@ def handle_request(env, start_response): start_response('401', []) yield 'Unauthorized' return - ea = cfg.get_node_attributes(nodename, ['crypted.selfapikey', 'deployment.apiarmed']) eak = ea.get( nodename, {}).get('crypted.selfapikey', {}).get('hashvalue', None) @@ -152,6 +152,16 @@ def handle_request(env, start_response): operation = env['REQUEST_METHOD'] if operation not in ('HEAD', 'GET') and 'CONTENT_LENGTH' in env and int(env['CONTENT_LENGTH']) > 0: reqbody = env['wsgi.input'].read(int(env['CONTENT_LENGTH'])) + if env['PATH_INFO'] == '/self/register_discovered': + rb = json.loads(reqbody) + addrs = rb.get('addresses', []) + rb['addresses'] = [] + for addr in addrs: + rb['addresses'].append(tuple(addr)) + disco.detected(rb) + start_response('200 OK', []) + yield 'Registered' + return if env['PATH_INFO'] == '/self/bmcconfig': hmattr = cfg.get_node_attributes(nodename, 'hardwaremanagement.*') hmattr = hmattr.get(nodename, {}) From 9b79da9522d0f880fb383bfbd1317164521450b4 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Mon, 27 Jun 2022 14:12:51 -0400 Subject: [PATCH 08/13] Add a catch all to redfish for xcc Newer xcc changes things yet again, but we are comfortably in the firmware that can be bootstrapped with redfish, so use that instead once we've cleared the redfish incapable variants. --- .../confluent/discovery/handlers/xcc.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/confluent_server/confluent/discovery/handlers/xcc.py b/confluent_server/confluent/discovery/handlers/xcc.py index ad4f7c20..32c9306f 100644 --- a/confluent_server/confluent/discovery/handlers/xcc.py +++ b/confluent_server/confluent/discovery/handlers/xcc.py @@ -411,7 +411,21 @@ class NodeHandler(immhandler.NodeHandler): rsp, status = wc.grab_json_response_with_status( '/api/function', {'USER_UserModify': '{0},{1},,1,4,0,0,0,0,,8,,,'.format(uid, username)}) + if status == 200 and rsp.get('return', 0) == 13: + wc.set_basic_credentials(self._currcreds[0], self._currcreds[1]) + status = 503 + while status != 200: + rsp, status = wc.grab_json_response_with_status( + '/redfish/v1/AccountService/Accounts/{0}'.format(uid), + {'UserName': username}, method='PATCH') + if status != 200: + rsp = json.loads(rsp) + if rsp.get('error', {}).get('code', 'Unknown') in ('Base.1.8.GeneralError', 'Base.1.12.GeneralError'): + eventlet.sleep(10) + else: + break self.tmppasswd = None + wc.grab_json_response('/api/providers/logout') self._currcreds = (username, passwd) def _convert_sha256account(self, user, passwd, wc): @@ -503,6 +517,7 @@ class NodeHandler(immhandler.NodeHandler): 'Request to use default credentials, but refused by target after it has been changed to {0}'.format(self.tmppasswd)) if not isdefault: self._setup_xcc_account(user, passwd, wc) + wc = self.wc self._convert_sha256account(user, passwd, wc) cd = self.configmanager.get_node_attributes( nodename, ['secret.hardwaremanagementuser', From a54a9a5d099576a23e7168c2e9c2ac7e9a82c0a2 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Mon, 27 Jun 2022 14:34:37 -0400 Subject: [PATCH 09/13] Enable ipmi user if required If redfish models ipmi as an account type, and user wants ipmi, add it to the account. --- .../confluent/discovery/handlers/xcc.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/confluent_server/confluent/discovery/handlers/xcc.py b/confluent_server/confluent/discovery/handlers/xcc.py index 32c9306f..5c236b03 100644 --- a/confluent_server/confluent/discovery/handlers/xcc.py +++ b/confluent_server/confluent/discovery/handlers/xcc.py @@ -536,6 +536,20 @@ class NodeHandler(immhandler.NodeHandler): _, _ = nwc.grab_json_response_with_status( '/redfish/v1/Managers/1/NetworkProtocol', {'IPMI': {'ProtocolEnabled': True}}, method='PATCH') + rsp, status = nwc.grab_json_response_with_status( + '/redfish/v1/AccountService/Accounts/1') + if status == 200: + allowable = rsp.get('AccountTypes@Redfish.AllowableValues', []) + current = rsp.get('AccountTypes', []) + if 'IPMI' in allowable and 'IPMI' not in current: + current.append('IPMI') + updateinf = { + 'AccountTypes': current, + 'Password': self._currcreds[1] + } + rsp, status = nwc.grab_json_response_with_status( + '/redfish/v1/AccountService/Accounts/1', + updateinf, method='PATCH') if ('hardwaremanagement.manager' in cd and cd['hardwaremanagement.manager']['value'] and not cd['hardwaremanagement.manager']['value'].startswith( From f65ff1268a05906ceb93f0df494770ef8466c28d Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Tue, 28 Jun 2022 12:15:54 -0400 Subject: [PATCH 10/13] Add new state offline to confluent storage states --- confluent_server/confluent/messages.py | 1 + 1 file changed, 1 insertion(+) diff --git a/confluent_server/confluent/messages.py b/confluent_server/confluent/messages.py index 6f0c3c25..7c6651cd 100644 --- a/confluent_server/confluent/messages.py +++ b/confluent_server/confluent/messages.py @@ -1600,6 +1600,7 @@ class Disk(ConfluentMessage): 'hotspare', 'rebuilding', 'online', + 'offline', ]) state_aliases = { 'unconfigured bad': 'fault', From 480849640ea2f3f3760b2cf86a91b00031538d6d Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Tue, 19 Jul 2022 10:16:18 -0400 Subject: [PATCH 11/13] Make error message more clear --- confluent_client/confluent/logreader.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/confluent_client/confluent/logreader.py b/confluent_client/confluent/logreader.py index 94034d06..5bb14892 100644 --- a/confluent_client/confluent/logreader.py +++ b/confluent_client/confluent/logreader.py @@ -282,14 +282,19 @@ def _replay_to_console(txtfile, binfile): def replay_to_console(txtfile): if os.path.exists(txtfile + '.cbl'): binfile = txtfile + '.cbl' + elif '.' not in txtfile: + if '/' not in txtfile: + txtfile = os.getcwd() + '/' + txtfile + sys.stderr.write('Unable to locate cbl file: "{0}"\n'.format(txtfile + '.cbl')) + sys.exit(1) else: fileparts = txtfile.split('.') prefix = '.'.join(fileparts[:-1]) binfile = prefix + '.cbl.' + fileparts[-1] if not os.path.exists(binfile): - sys.stderr.write('Unable to locate cbl file\n') + sys.stderr.write('Unable to locate cbl file: "{0}"\n'.format(binfile)) sys.exit(1) _replay_to_console(txtfile, binfile) if __name__ == '__main__': - replay_to_console(sys.argv[1]) \ No newline at end of file + replay_to_console(sys.argv[1]) From 0dc7b532cc75d65845087ee65907fb1a55128707 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Wed, 20 Jul 2022 13:01:11 -0400 Subject: [PATCH 12/13] Implement registration and retrieval Remote discovery can now be registered by switches. --- confluent_client/bin/nodediscover | 15 +++++--- confluent_server/confluent/discovery/core.py | 9 ++++- .../confluent/discovery/handlers/generic.py | 3 +- .../confluent/discovery/protocols/ssdp.py | 6 ++- confluent_server/confluent/selfservice.py | 38 +++++++++++++++++-- 5 files changed, 56 insertions(+), 15 deletions(-) diff --git a/confluent_client/bin/nodediscover b/confluent_client/bin/nodediscover index a14b7358..41ecf8b3 100755 --- a/confluent_client/bin/nodediscover +++ b/confluent_client/bin/nodediscover @@ -57,12 +57,15 @@ def print_disco(options, session, currmac, outhandler, columns): procinfo.update(tmpinfo) if 'Switch' in columns or 'Port' in columns: - for tmpinfo in session.read( - '/networking/macs/by-mac/{0}'.format(currmac)): - if 'ports' in tmpinfo: - # The api sorts so that the most specific available value - # is last - procinfo.update(tmpinfo['ports'][-1]) + if 'switch' in procinfo: + procinfo['port'] = procinfo['switchport'] + else: + for tmpinfo in session.read( + '/networking/macs/by-mac/{0}'.format(currmac)): + if 'ports' in tmpinfo: + # The api sorts so that the most specific available value + # is last + procinfo.update(tmpinfo['ports'][-1]) record = [] for col in columns: rawcol = columnmapping[col] diff --git a/confluent_server/confluent/discovery/core.py b/confluent_server/confluent/discovery/core.py index 30df3c09..7e8b90ed 100644 --- a/confluent_server/confluent/discovery/core.py +++ b/confluent_server/confluent/discovery/core.py @@ -217,7 +217,12 @@ def send_discovery_datum(info): if info['handler'] == pxeh: enrich_pxe_info(info) yield msg.KeyValueData({'nodename': info.get('nodename', '')}) - yield msg.KeyValueData({'ipaddrs': [_printable_ip(x) for x in addresses]}) + if not info.get('forwarder_server', None): + yield msg.KeyValueData({'ipaddrs': [_printable_ip(x) for x in addresses]}) + switch = info.get('forwarder_server', None) + if switch: + yield msg.KeyValueData({'switch': switch}) + yield msg.KeyValueData({'switchport': info['port']}) sn = info.get('serialnumber', '') mn = info.get('modelnumber', '') uuid = info.get('uuid', '') @@ -690,6 +695,8 @@ def detected(info): info['otheraddresses'] = set([]) for i4addr in info.get('attributes', {}).get('ipv4-address', []): info['otheraddresses'].add(i4addr) + for i4addr in info.get('attributes', {}).get('ipv4-addresses', []): + info['otheraddresses'].add(i4addr) if handler and handler.https_supported and not handler.https_cert: if handler.cert_fail_reason == 'unreachable': log.log( diff --git a/confluent_server/confluent/discovery/handlers/generic.py b/confluent_server/confluent/discovery/handlers/generic.py index db1d0611..0d41d366 100644 --- a/confluent_server/confluent/discovery/handlers/generic.py +++ b/confluent_server/confluent/discovery/handlers/generic.py @@ -41,7 +41,6 @@ class NodeHandler(object): if info.get('forwarder_url', False): self.relay_url = info['forwarder_url'] self.relay_server = info['forwarder_server'] - self.relay_token = info['forwarder_token'] return # first let us prefer LLA if possible, since that's most stable for sa in info['addresses']: @@ -144,7 +143,7 @@ class NodeHandler(object): relaycreds = self.configmanager.get_node_attributes(self.relay_server, 'secret.*', decrypt=True) relaycreds = relaycreds.get(self.relay_server, {}) relayuser = relaycreds.get('secret.hardwaremanagementuser', {}).get('value', None) - relaypass = relaycreds.get('secret.hardwaremanegementpassword', {}).get('value', None) + relaypass = relaycreds.get('secret.hardwaremanagementpassword', {}).get('value', None) if not relayuser or not relaypass: raise Exception('No credentials for {0}'.format(self.relay_server)) w.set_basic_credentials(relayuser, relaypass) diff --git a/confluent_server/confluent/discovery/protocols/ssdp.py b/confluent_server/confluent/discovery/protocols/ssdp.py index 762b643a..85b5d7f1 100644 --- a/confluent_server/confluent/discovery/protocols/ssdp.py +++ b/confluent_server/confluent/discovery/protocols/ssdp.py @@ -382,10 +382,12 @@ def _find_service(service, target): if pi is not None: yield pi -def check_fish(urldata): +def check_fish(urldata, port=443, verifycallback=None): + if not verifycallback: + verifycallback = lambda x: True url, data = urldata try: - wc = webclient.SecureHTTPConnection(_get_svrip(data), 443, verifycallback=lambda x: True) + wc = webclient.SecureHTTPConnection(_get_svrip(data), port, verifycallback=verifycallback) peerinfo = wc.grab_json_response(url) except socket.error: return None diff --git a/confluent_server/confluent/selfservice.py b/confluent_server/confluent/selfservice.py index 687e02a0..195c2f72 100644 --- a/confluent_server/confluent/selfservice.py +++ b/confluent_server/confluent/selfservice.py @@ -19,6 +19,10 @@ import json import os import time import yaml +import confluent.discovery.protocols.ssdp as ssdp +import eventlet +webclient = eventlet.import_patched('pyghmi.util.webclient') + currtz = None keymap = 'us' @@ -154,10 +158,36 @@ def handle_request(env, start_response): reqbody = env['wsgi.input'].read(int(env['CONTENT_LENGTH'])) if env['PATH_INFO'] == '/self/register_discovered': rb = json.loads(reqbody) - addrs = rb.get('addresses', []) - rb['addresses'] = [] - for addr in addrs: - rb['addresses'].append(tuple(addr)) + if not rb.get('path', None): + start_response('400 Bad Requst', []) + yield 'Missing Path' + return + targurl = '/hubble/systems/by-port/{0}/webaccess'.format(rb['path']) + tlsverifier = util.TLSCertVerifier(cfg, nodename, 'pubkeys.tls_hardwaremanager') + wc = webclient.SecureHTTPConnection(nodename, 443, verifycallback=tlsverifier.verify_cert) + relaycreds = cfg.get_node_attributes(nodename, 'secret.*', decrypt=True) + relaycreds = relaycreds.get(nodename, {}) + relayuser = relaycreds.get('secret.hardwaremanagementuser', {}).get('value', None) + relaypass = relaycreds.get('secret.hardwaremanagementpassword', {}).get('value', None) + if not relayuser or not relaypass: + raise Exception('No credentials for {0}'.format(nodename)) + wc.set_basic_credentials(relayuser, relaypass) + wc.request('GET', targurl) + rsp = wc.getresponse() + _ = rsp.read() + if rsp.status == 302: + newurl = rsp.headers['Location'] + newhost, newport = newurl.replace('https://', '').split('/')[0].split(':') + def verify_cert(certificate): + hashval = base64.b64decode(rb['fingerprint']) + if len(hashval) == 48: + return hashlib.sha384(certificate).digest() == hashval + raise Exception('Certificate validation failed') + rb['addresses'] = [(newhost, newport)] + rb['forwarder_url'] = targurl + rb['forwarder_server'] = nodename + rb[''] + ssdp.check_fish(('/DeviceDescription.json', rb), newport, verify_cert) disco.detected(rb) start_response('200 OK', []) yield 'Registered' From 2d8bcb4c0f3465619906ea41c688fb8f46174623 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Wed, 20 Jul 2022 16:21:25 -0400 Subject: [PATCH 13/13] Incorporate auto discovery for remote discovery Avail ourselves of secure vouching to handle new and replaced. --- confluent_server/confluent/config/attributes.py | 2 +- confluent_server/confluent/discovery/core.py | 10 +++++++++- confluent_server/confluent/util.py | 12 ++++++++++++ 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/confluent_server/confluent/config/attributes.py b/confluent_server/confluent/config/attributes.py index 5444e0d7..c25045c6 100644 --- a/confluent_server/confluent/config/attributes.py +++ b/confluent_server/confluent/config/attributes.py @@ -256,7 +256,7 @@ node = { 'so long as the node has no existing public key. ' '"open" allows discovery even if a known public key ' 'is already stored', - 'validlist': ('manual', 'permissive', 'pxe', 'open'), + 'validlist': ('manual', 'permissive', 'pxe', 'open', 'verified'), }, 'info.note': { 'description': 'A field used for administrators to make arbitrary ' diff --git a/confluent_server/confluent/discovery/core.py b/confluent_server/confluent/discovery/core.py index 7e8b90ed..05ab0a21 100644 --- a/confluent_server/confluent/discovery/core.py +++ b/confluent_server/confluent/discovery/core.py @@ -1153,7 +1153,15 @@ def discover_node(cfg, handler, info, nodename, manual): 'pubkeys.tls_hardwaremanager attribute is cleared ' 'first'.format(nodename)}) return False # With a permissive policy, do not discover new - elif policies & set(('open', 'permissive')) or manual: + elif policies & set(('open', 'permissive', 'verified')) or manual: + if 'verified' in policies: + if not handler.https_supported or not util.cert_matches(info['fingerprint'], handler.https_cert): + log.log({'info': 'Detected replacement of {0} without verified ' + 'fingerprint and discovery policy is setto verified, not ' + 'doing discovery unless discovery.policy=open or ' + 'pubkeys.tls_hardwaremanager attribute is cleared ' + 'first'.format(nodename)}) + return False info['nodename'] = nodename if info['handler'] == pxeh: return do_pxe_discovery(cfg, handler, info, manual, nodename, policies) diff --git a/confluent_server/confluent/util.py b/confluent_server/confluent/util.py index 52726302..35b1e08e 100644 --- a/confluent_server/confluent/util.py +++ b/confluent_server/confluent/util.py @@ -146,12 +146,24 @@ def get_fingerprint(certificate, algo='sha512'): return 'sha256$' + hashlib.sha256(certificate).hexdigest() elif algo == 'sha512': return 'sha512$' + hashlib.sha512(certificate).hexdigest() + elif algo == 'sha384': + return 'sha384$' + hashlib.sha384(certificate).hexdigest() raise Exception('Unsupported fingerprint algorithm ' + algo) +hashlens = { + 48: hashlib.sha384, + 64: hashlib.sha512, + 32: hashlib.sha256 +} + def cert_matches(fingerprint, certificate): if not fingerprint or not certificate: return False + if '$' not in fingerprint: + fingerprint = base64.b64decode(certificate) + algo = hashlens[len(fingerprint)] + return algo(certificate).digest() == fingerprint algo, _, fp = fingerprint.partition('$') newfp = None if algo in ('sha512', 'sha256'):