2
0
mirror of https://github.com/xcat2/confluent.git synced 2026-09-21 16:39:32 +00:00
Files
confluent/confluent_server/confluent/plugins/hardwaremanagement/geist.py
T
Markus Hilger 8a3fce85c0 Fix undefined names (F821)
Every one of these raises NameError if its code path is reached:

- nodeapply: run_automation accumulated into an exitcode that only existed
  in run(), so any automation error crashed instead of being reported.  It
  now keeps and returns its own, tracked separately from the exit code of
  the ssh commands: the early exit after the spawn loop tests that one,
  and folding automation failures into it would exit with children already
  running and their pipes abandoned.  Both are reported at the real exits.
- nodeconsole: redraw() reads firstnodename, which was local to
  do_screenshot(); promote it to a module global like the other drawing
  state.
- nodedeploy: the redeploy path appended to a lockednodes list that did not
  exist yet.  The block that follows re-reads the same lock state and acts
  on it, so drop the dead duplicate.
- samples/nodeattrib_from_switch.py, misc/filterpasswd: missing import sys.
- xcc3: fixuuid was never imported.  xcc imports xcc3, so take a local copy
  the way the smm handler does instead of creating an import cycle.
- httpapi: the async session call still passed the WSGI-era env and an
  extra argument to handle_async(), which has taken only querydict since
  the aiohttp port.  Calling it correctly exposed that handle_async()
  registers an AsyncSession before raising on the discontinued long poll
  path, so every request to it would leak a session that is never reaped.
  It now only creates one when there is a websocket handler to yield it to.
- messages: the InputFirmwareUpdate.filename property checked
  self.filebynode[node] with no node in scope.  __init__ already validates
  every expanded path and nodefile() rechecks per node, so drop the checks.
- pam: drop the python2 branches referencing unicode and raw_input.  The
  server has been python3 only since the asyncio port.
- cooltera: the sensor-name listing referenced a nonexistent sensors dict.
  The available sensors depend on the model, which is only known after
  reading the device, so list them from the same status data the readings
  use.
- deltapdu, eatonpdu, geist: the not-implemented response in update() used
  node outside the loop, unlike retrieve() in the same files and unlike
  raritan/enlogic.
- confluentdbgcli: stray self. on a module-level socket connect.
2026-08-10 05:32:00 +02:00

349 lines
11 KiB
Python

# 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 asyncio
import confluent.util as util
import confluent.messages as msg
import aiohmi.util.webclient as wc
import confluent.tasks as tasks
import time
def simplify_name(name):
return name.lower().replace(' ', '_').replace('/', '-').replace('_-_', '-')
pdupool = tasks.TaskPool(128)
def data_by_type(indata):
databytype = {}
for keyname in indata:
obj = indata[keyname]
objtype = obj.get('type', None)
if not objtype:
continue
if objtype in databytype:
raise Exception(
'Multiple instances of type {} not yet supported'.format(objtype)
)
databytype[objtype] = obj
obj['keyname'] = keyname
return databytype
class GeistClient(object):
def __init__(self, pdu, configmanager):
self.node = pdu
self.configmanager = configmanager
self._token = None
self._wc = None
self.username = None
async def token(self):
if not self._token:
self._token = await 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
target = target.split('/', 1)[0]
cv = util.TLSCertVerifier(
self.configmanager, self.node, 'pubkeys.tls_hardwaremanager'
).verify_cert
self._wc = wc.WebConnection(target, port=443, verifycallback=cv)
return self._wc
async 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')
self.username = username
rsp = await self.wc.grab_json_response(
'/api/auth/{0}'.format(username),
{'cmd': 'login', 'data': {'password': passwd}},
)
token = rsp['data']['token']
return token
async def logout(self):
if self._token:
await self.wc.grab_json_response(
'/api/auth/{0}'.format(self.username),
{'cmd': 'logout', 'token': await self.token()},
)
self._token = None
async def get_outlet(self, outlet):
rsp = await self.wc.grab_json_response('/api/dev')
rsp = rsp['data']
dbt = data_by_type(rsp)
if 't3hd' in dbt:
del dbt['t3hd']
if len(dbt) != 1:
raise Exception('Multiple PDUs not supported per pdu')
pdutype = list(dbt)[0]
outlet = dbt[pdutype]['outlet'][str(int(outlet) - 1)]
state = outlet['state'].split('2')[-1]
return state
async def set_outlet(self, outlet, state):
rsp = await self.wc.grab_json_response('/api/dev')
dbt = data_by_type(rsp['data'])
if 't3hd' in dbt:
del dbt['t3hd']
if len(dbt) != 1:
await self.logout()
raise Exception('Multiple PDUs per endpoint not supported')
pdu = dbt[list(dbt)[0]]['keyname']
outlet = int(outlet) - 1
rsp = await self.wc.grab_json_response(
'/api/dev/{0}/outlet/{1}'.format(pdu, outlet),
{
'cmd': 'control',
'token': await self.token(),
'data': {'action': state, 'delay': False},
},
)
def process_measurement(
keyname, name, enttype, entname, measurement, readings, category
):
if measurement['type'] == 'realPower':
if category not in ('all', 'power'):
return
readtype = 'Real Power'
elif measurement['type'] == 'apparentPower':
if category not in ('all', 'power'):
return
readtype = 'Apparent Power'
elif measurement['type'] == 'energy':
if category not in ('all', 'energy'):
return
readtype = 'Energy'
elif measurement['type'] == 'voltage':
if category not in ('all',):
return
readtype = 'Voltage'
elif measurement['type'] == 'current':
if category not in ('all',):
return
readtype = 'Current'
elif measurement['type'] == 'temperature':
readtype = 'Temperature'
elif measurement['type'] == 'dewpoint':
readtype = 'Dewpoint'
elif measurement['type'] == 'humidity':
readtype = 'Humidity'
else:
return
myname = entname + ' ' + readtype
if name != 'all' and simplify_name(myname) != name:
return
readings.append(
{
'name': myname,
'value': float(measurement['value']),
'units': measurement['units'],
'type': readtype.split()[-1],
}
)
def process_measurements(name, category, measurements, enttype, readings):
for measure in util.natural_sort(list(measurements)):
measurement = measurements[measure]['measurement']
entname = measurements[measure]['name']
for measureid in measurement:
process_measurement(
measure,
name,
enttype,
entname,
measurement[measureid],
readings,
category,
)
_sensors_by_node = {}
async def read_sensors(element, node, configmanager):
category, name = element[-2:]
justnames = False
if len(element) == 3:
# just get names
category = name
name = 'all'
justnames = True
if category in ('leds, fans', 'temperature'):
return
sn = _sensors_by_node.get(node, None)
if not sn or sn[1] < time.time():
gc = GeistClient(node, configmanager)
adev = await gc.wc.grab_json_response('/api/dev')
_sensors_by_node[node] = (adev, time.time() + 1)
sn = _sensors_by_node.get(node, None)
dbt = data_by_type(sn[0]['data'])
readings = []
for datatype in dbt:
datum = dbt[datatype]
process_measurements(name, category, datum['entity'], 'entity', readings)
if 'outlet' in datum:
process_measurements(name, category, datum['outlet'], 'outlet', readings)
if justnames:
for reading in readings:
return msg.ChildCollection(simplify_name(reading['name']))
else:
return msg.SensorReadings(readings, name=node)
async def get_outlet(element, node, configmanager):
gc = GeistClient(node, configmanager)
state = await gc.get_outlet(element[-1])
return msg.PowerState(node=node, state=state)
async def read_firmware(node, configmanager):
gc = GeistClient(node, configmanager)
adev = await gc.wc.grab_json_response('/api/sys')
myversion = adev['data']['version']
return msg.Firmware([{'PDU Firmware': {'version': myversion}}], node)
async def read_inventory(element, node, configmanager):
_inventory = {}
inventory = {}
gc = GeistClient(node, configmanager)
adev = await gc.wc.grab_json_response('/api/sys')
basedata = adev['data']
inventory['present'] = True
inventory['name'] = 'PDU'
for elem in basedata.items():
if (
elem[0] != 'component'
and elem[0] != 'locale'
and elem[0] != 'state'
and elem[0] != 'contact'
and elem[0] != 'appVersion'
and elem[0] != 'build'
and elem[0] != 'version'
and elem[0] != 'apiVersion'
):
temp = elem[0]
if elem[0] == 'serialNumber':
temp = 'Serial'
elif elem[0] == 'partNumber':
temp = 'P/N'
elif elem[0] == 'modelNumber':
temp = 'Lenovo P/N and Serial'
_inventory[temp] = elem[1]
elif elem[0] == 'component':
tempname = ''
for component in basedata['component'].items():
for item in component:
if type(item) == str:
tempname = item
else:
for entry in item.items():
temp = entry[0]
if temp == 'sn':
temp = 'Serial'
_inventory[tempname + ' ' + temp] = entry[1]
inventory['information'] = _inventory
return msg.KeyValueData({'inventory': [inventory]}, node)
async def retrieve(nodes, element, configmanager, inputdata):
if 'outlets' in element:
gp = tasks.TaskPile(pdupool)
for node in nodes:
gp.spawn(get_outlet, element, node, configmanager)
async for res in gp:
yield res
return
elif element[0] == 'sensors':
gp = tasks.TaskPile(pdupool)
for node in nodes:
gp.spawn(read_sensors, element, node, configmanager)
async for rsp in gp:
yield rsp
return
elif '/'.join(element).startswith('inventory/firmware/all'):
gp = tasks.TaskPile(pdupool)
for node in nodes:
gp.spawn(read_firmware, node, configmanager)
async for rsp in gp:
yield rsp
elif '/'.join(element).startswith('inventory/hardware/all'):
gp = tasks.TaskPile(pdupool)
for node in nodes:
gp.spawn(read_inventory, element, node, configmanager)
async for rsp in gp:
yield rsp
else:
for node in nodes:
yield msg.ConfluentResourceUnavailable(node, 'Not implemented')
return
async def update(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 = GeistClient(node, configmanager)
newstate = inputdata.powerstate(node)
await gc.set_outlet(element[-1], newstate)
await asyncio.sleep(1)
async for res in retrieve(nodes, element, configmanager, inputdata):
yield res