mirror of
https://github.com/xcat2/confluent.git
synced 2026-09-21 16:39:32 +00:00
Add EUREKA discovery handler
This commit is contained in:
@@ -0,0 +1,242 @@
|
||||
# Copyright 2026 MEGWARE Computer Vertrieb und Service GmbH
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""
|
||||
Discovery handler for Megware EUREKA chassis Redfish service.
|
||||
|
||||
This is a software-only Redfish service (not a BMC). It provides
|
||||
chassis-level information, node inventory, power supply data, and
|
||||
sensor readings. It does NOT support IPMI, NIC configuration,
|
||||
firmware update, or other BMC-specific operations.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import confluent.discovery.handlers.generic as generic
|
||||
import confluent.util as util
|
||||
import aiohmi.util.webclient as webclient
|
||||
|
||||
|
||||
class NodeHandler(generic.NodeHandler):
|
||||
devname = 'EUREKA'
|
||||
is_enclosure = True
|
||||
maxmacs = 42
|
||||
https_supported = True
|
||||
DEFAULT_USER = 'admin'
|
||||
DEFAULT_PASS = 'admin'
|
||||
|
||||
def __init__(self, info, configmanager):
|
||||
self.trieddefault = None
|
||||
self.targuser = None
|
||||
self.curruser = None
|
||||
self.currpass = None
|
||||
self.targpass = None
|
||||
self.nodename = None
|
||||
self.xauthtoken = None
|
||||
self.atdefault = True
|
||||
super(NodeHandler, self).__init__(info, configmanager)
|
||||
|
||||
def get_firmware_default_account_info(self):
|
||||
return (self.DEFAULT_USER, self.DEFAULT_PASS)
|
||||
|
||||
async def scan(self):
|
||||
await self.get_https_cert()
|
||||
try:
|
||||
wc = webclient.WebConnection(
|
||||
self.ipaddr, 443, verifycallback=self.validate_cert)
|
||||
root = await wc.grab_json_response('/redfish/v1/')
|
||||
uuid = root.get('UUID', None)
|
||||
if uuid:
|
||||
self.info['uuid'] = uuid.lower()
|
||||
|
||||
# Read chassis info for model/serial
|
||||
chassis_url = root.get('Chassis', {}).get('@odata.id')
|
||||
if chassis_url:
|
||||
try:
|
||||
chassis_coll = await wc.grab_json_response(chassis_url)
|
||||
members = chassis_coll.get('Members', [])
|
||||
if members:
|
||||
cinfo = await wc.grab_json_response(members[0]['@odata.id'])
|
||||
if cinfo.get('Model'):
|
||||
self.info['modelname'] = cinfo['Model']
|
||||
if cinfo.get('SerialNumber'):
|
||||
self.info['serialnumber'] = cinfo['SerialNumber']
|
||||
if cinfo.get('Manufacturer'):
|
||||
self.info['modelnumber'] = cinfo['Manufacturer']
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
async def _get_wc(self):
|
||||
await self.get_https_cert()
|
||||
defuser, defpass = self.get_firmware_default_account_info()
|
||||
wc = webclient.WebConnection(
|
||||
self.ipaddr, 443, verifycallback=self.validate_cert)
|
||||
wc.set_header('Content-Type', 'application/json')
|
||||
wc.set_header('Accept', 'application/json')
|
||||
wc.set_header('OData-Version', '4.0')
|
||||
|
||||
if not self.trieddefault:
|
||||
# Attempt authentication with default credentials
|
||||
wc.set_basic_credentials(defuser, defpass)
|
||||
body, status, headers = await wc.grab_response_with_status(
|
||||
'/redfish/v1/SessionService/Sessions',
|
||||
{'UserName': defuser, 'Password': defpass})
|
||||
if status == 201:
|
||||
token = headers.get('X-Auth-Token')
|
||||
if token:
|
||||
self.curruser = defuser
|
||||
self.currpass = defpass
|
||||
self.xauthtoken = token
|
||||
if 'Authorization' in wc.stdheaders:
|
||||
del wc.stdheaders['Authorization']
|
||||
wc.stdheaders['X-Auth-Token'] = token
|
||||
return wc
|
||||
elif status == 401:
|
||||
self.trieddefault = True
|
||||
try:
|
||||
errinfo = util.json_loads(body)
|
||||
for msg in errinfo.get('@Message.ExtendedInfo', []):
|
||||
if 'PasswordChangeRequired' in msg.get('MessageId', ''):
|
||||
chgurl = msg.get('MessageArgs', [None])[0]
|
||||
if chgurl and self.targpass and self.targpass != defpass:
|
||||
wc.set_basic_credentials(defuser, defpass)
|
||||
wc.set_header('If-Match', '*')
|
||||
rsp, chgstatus = await wc.grab_json_response_with_status(
|
||||
chgurl,
|
||||
{'Password': self.targpass},
|
||||
method='PATCH')
|
||||
if chgstatus >= 200 and chgstatus < 300:
|
||||
body, status, headers = \
|
||||
await wc.grab_response_with_status(
|
||||
'/redfish/v1/SessionService/Sessions',
|
||||
{'UserName': defuser,
|
||||
'Password': self.targpass})
|
||||
if status == 201:
|
||||
token = headers.get('X-Auth-Token')
|
||||
if token:
|
||||
self.curruser = defuser
|
||||
self.currpass = self.targpass
|
||||
self.xauthtoken = token
|
||||
if 'Authorization' in wc.stdheaders:
|
||||
del wc.stdheaders['Authorization']
|
||||
wc.stdheaders['X-Auth-Token'] = token
|
||||
return wc
|
||||
except Exception:
|
||||
pass
|
||||
# If no password change scenario, try target password
|
||||
if self.targuser and self.targpass:
|
||||
wc.set_basic_credentials(self.targuser, self.targpass)
|
||||
body, status, headers = await wc.grab_response_with_status(
|
||||
'/redfish/v1/SessionService/Sessions',
|
||||
{'UserName': self.targuser, 'Password': self.targpass})
|
||||
if status == 201:
|
||||
token = headers.get('X-Auth-Token')
|
||||
if token:
|
||||
self.curruser = self.targuser
|
||||
self.currpass = self.targpass
|
||||
self.xauthtoken = token
|
||||
if 'Authorization' in wc.stdheaders:
|
||||
del wc.stdheaders['Authorization']
|
||||
wc.stdheaders['X-Auth-Token'] = token
|
||||
return wc
|
||||
self.trieddefault = True
|
||||
return None
|
||||
|
||||
# We have previously established credentials, try to reuse
|
||||
if self.curruser and self.currpass:
|
||||
wc.set_basic_credentials(self.curruser, self.currpass)
|
||||
body, status, headers = await wc.grab_response_with_status(
|
||||
'/redfish/v1/SessionService/Sessions',
|
||||
{'UserName': self.curruser, 'Password': self.currpass})
|
||||
if status == 201:
|
||||
token = headers.get('X-Auth-Token')
|
||||
if token:
|
||||
self.xauthtoken = token
|
||||
if 'Authorization' in wc.stdheaders:
|
||||
del wc.stdheaders['Authorization']
|
||||
wc.stdheaders['X-Auth-Token'] = token
|
||||
return wc
|
||||
|
||||
# Try target credentials
|
||||
if self.targuser and self.targpass:
|
||||
wc.set_basic_credentials(self.targuser, self.targpass)
|
||||
body, status, headers = await wc.grab_response_with_status(
|
||||
'/redfish/v1/SessionService/Sessions',
|
||||
{'UserName': self.targuser, 'Password': self.targpass})
|
||||
if status == 201:
|
||||
token = headers.get('X-Auth-Token')
|
||||
if token:
|
||||
self.curruser = self.targuser
|
||||
self.currpass = self.targpass
|
||||
self.xauthtoken = token
|
||||
if 'Authorization' in wc.stdheaders:
|
||||
del wc.stdheaders['Authorization']
|
||||
wc.stdheaders['X-Auth-Token'] = token
|
||||
return wc
|
||||
|
||||
return None
|
||||
|
||||
async def config(self, nodename):
|
||||
self.nodename = nodename
|
||||
creds = self.configmanager.get_node_attributes(
|
||||
nodename,
|
||||
['secret.hardwaremanagementuser',
|
||||
'secret.hardwaremanagementpassword'],
|
||||
True)
|
||||
cd = creds.get(nodename, {})
|
||||
defuser, defpass = self.get_firmware_default_account_info()
|
||||
user, passwd, _ = self.get_node_credentials(
|
||||
nodename, creds, defuser, defpass)
|
||||
user = util.stringify(user)
|
||||
passwd = util.stringify(passwd)
|
||||
self.targuser = user
|
||||
self.targpass = passwd
|
||||
|
||||
wc = await self._get_wc()
|
||||
if wc is None:
|
||||
raise Exception('Unable to authenticate to EUREKA chassis')
|
||||
|
||||
# Update credentials if they differ from current
|
||||
authupdate = {}
|
||||
if user != self.curruser:
|
||||
authupdate['UserName'] = user
|
||||
if passwd != self.currpass:
|
||||
authupdate['Password'] = passwd
|
||||
if authupdate:
|
||||
srvroot = await wc.grab_json_response('/redfish/v1/')
|
||||
asrv = srvroot.get('AccountService', {}).get('@odata.id')
|
||||
if asrv:
|
||||
acctinfo = await wc.grab_json_response(asrv)
|
||||
acctsurl = acctinfo.get('Accounts', {}).get('@odata.id')
|
||||
if acctsurl:
|
||||
accts = await wc.grab_json_response(acctsurl)
|
||||
for acctref in accts.get('Members', []):
|
||||
accturl = acctref.get('@odata.id', '')
|
||||
if accturl:
|
||||
acctdata, acctstatus = \
|
||||
await wc.grab_json_response_with_status(accturl)
|
||||
if acctdata.get('UserName') == self.curruser:
|
||||
wc.set_header('If-Match', '*')
|
||||
rsp, status = await wc.grab_json_response_with_status(
|
||||
accturl, authupdate, method='PATCH')
|
||||
if status >= 200 and status < 300:
|
||||
self.curruser = user
|
||||
self.currpass = passwd
|
||||
break
|
||||
|
||||
# Store link-local address if applicable
|
||||
if self.ipaddr.startswith('fe80::'):
|
||||
await self.configmanager.set_node_attributes(
|
||||
{nodename: {'hardwaremanagement.manager': self.ipaddr}})
|
||||
Reference in New Issue
Block a user