mirror of
https://github.com/xcat2/confluent.git
synced 2026-09-21 16:39:32 +00:00
8a3fce85c0
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.
349 lines
11 KiB
Python
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
|