From 6229cb23e8a7af143f05e48b63e2d2bff5fc7f1c Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Tue, 10 May 2022 16:00:08 -0400 Subject: [PATCH] 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))