From c96b5f0270488934f8ccd71e69b20805196c560a Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Thu, 25 Oct 2018 14:12:15 -0400 Subject: [PATCH 1/6] Fix spurious trace on immediate exit confetty When confetty exits without doing anything, it causes sockapi to reference an empty request. Check for that before checking if it is a collective request. --- confluent_server/confluent/sockapi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/confluent_server/confluent/sockapi.py b/confluent_server/confluent/sockapi.py index 75da4101..ac4c5786 100644 --- a/confluent_server/confluent/sockapi.py +++ b/confluent_server/confluent/sockapi.py @@ -143,7 +143,7 @@ def sessionhdl(connection, authname, skipauth=False, cert=None): cfm = authdata[1] send_data(connection, {'authpassed': 1}) request = tlvdata.recv(connection) - if 'collective' in request and skipauth: + if request and 'collective' in request and skipauth: if not libssl: tlvdata.send( connection, From 7c006b33bc424d0c16a263fbf64c5bc63478b9a6 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Thu, 25 Oct 2018 14:48:58 -0400 Subject: [PATCH 2/6] Implement functionality to configure disks Disks may now have their state changed between hotspare, unconfigured, and jbod. --- confluent_server/confluent/messages.py | 3 +- .../plugins/hardwaremanagement/ipmi.py | 28 +++++++++++++++++-- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/confluent_server/confluent/messages.py b/confluent_server/confluent/messages.py index 5c740dde..de2459f8 100644 --- a/confluent_server/confluent/messages.py +++ b/confluent_server/confluent/messages.py @@ -1261,6 +1261,7 @@ class Disk(ConfluentMessage): ]) state_aliases = { 'unconfigured good': 'unconfigured', + 'global hot spare': 'hotspare', } def _normalize_state(self, instate): @@ -1269,7 +1270,7 @@ class Disk(ConfluentMessage): return newstate elif newstate in self.state_aliases: return self.state_aliases[newstate] - raise Exception("Unknown state") + raise Exception("Unknown state {0}".format(instate)) def __init__(self, name, label=None, description=None, diff --git a/confluent_server/confluent/plugins/hardwaremanagement/ipmi.py b/confluent_server/confluent/plugins/hardwaremanagement/ipmi.py index 8958ad4e..cbcc1c83 100644 --- a/confluent_server/confluent/plugins/hardwaremanagement/ipmi.py +++ b/confluent_server/confluent/plugins/hardwaremanagement/ipmi.py @@ -29,6 +29,7 @@ import eventlet.support.greendns from fnmatch import fnmatch import pyghmi.constants as pygconstants import pyghmi.exceptions as pygexc +import pyghmi.storage as storage console = eventlet.import_patched('pyghmi.ipmi.console') ipmicommand = eventlet.import_patched('pyghmi.ipmi.command') import socket @@ -391,8 +392,8 @@ def perform_request(operator, node, element, results.put(msg.ConfluentTargetTimeout(node, str(tu))) except ssl.SSLEOFError: results.put(msg.ConfluentNodeError( - self.node, 'Unable to communicate with the https server on ' - 'the target BMC')) + node, 'Unable to communicate with the https server on ' + 'the target BMC')) except exc.PubkeyInvalid: results.put(msg.ConfluentNodeError( node, @@ -936,6 +937,19 @@ class IpmiHandler(object): if self.element[-1] == '': self.element = self.element[:-1] storelem = self.element[2:] + if 'read' == self.op: + return self._show_storage(storelem) + elif 'update' == self.op: + return self._update_storage(storelem) + + def _update_storage(self, storelem): + if storelem[0] == 'disks': + if len(storelem) == 1: + raise exc.InvalidArgumentException('Must target a disk') + self.set_disk(storelem[-1], + self.inputdata.inputbynode[self.node]) + + def _show_storage(self, storelem): if storelem[0] == 'disks': if len(storelem) == 1: return self.list_disks() @@ -973,6 +987,15 @@ class IpmiHandler(object): return True return False + def set_disk(self, name, state): + scfg = self.ipmicmd.get_storage_configuration() + for disk in scfg.disks: + if (name == 'all' or simplify_name(disk.name) == name or + disk == name): + disk.status = state + self.ipmicmd.apply_storage_configuration( + storage.ConfigSpec(disks=scfg.disks)) + def show_disk(self, name): scfg = self.ipmicmd.get_storage_configuration() for disk in scfg.disks: @@ -1282,3 +1305,4 @@ def delete(nodes, element, configmanager, inputdata): element, type='ffdc') return perform_requests( 'delete', nodes, element, configmanager, inputdata) + From 3945ccd5c3df429e1a4b6221ac6be6dc0410c513 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Thu, 25 Oct 2018 16:07:07 -0400 Subject: [PATCH 3/6] Advance storage configuration API Enumerate disks in an array in disks list. Add array associated with disk to disk data. Show detailed data on arrays and volumes. --- confluent_server/confluent/messages.py | 30 +++++++++++++++- .../plugins/hardwaremanagement/ipmi.py | 36 +++++++++++++++++++ 2 files changed, 65 insertions(+), 1 deletion(-) diff --git a/confluent_server/confluent/messages.py b/confluent_server/confluent/messages.py index de2459f8..6badb1d4 100644 --- a/confluent_server/confluent/messages.py +++ b/confluent_server/confluent/messages.py @@ -1253,11 +1253,37 @@ class KeyValueData(ConfluentMessage): else: self.kvpairs = {name: kvdata} +class Array(ConfluentMessage): + def __init__(self, name, disks=None, raid=None, volumes=None, + id=None, capacity=None, available=None): + self.kvpairs = { + name: { + 'disks': disks, + 'raid': raid, + 'id': id, + 'volumes': volumes, + 'capacity': capacity, + 'available': available, + } + } + +class Volume(ConfluentMessage): + def __init__(self, name, volname, size, state, array): + self.kvpairs = { + name: { + 'name': volname, + 'size': size, + 'state': state, + 'array': array, + } + } + class Disk(ConfluentMessage): valid_states = set([ 'jbod', 'unconfigured', 'hotspare', + 'online', ]) state_aliases = { 'unconfigured good': 'unconfigured', @@ -1274,7 +1300,8 @@ class Disk(ConfluentMessage): def __init__(self, name, label=None, description=None, - diskid=None, state=None, serial=None, fru=None): + diskid=None, state=None, serial=None, fru=None, + array=None): state = self._normalize_state(state) self.kvpairs = { name: { @@ -1284,6 +1311,7 @@ class Disk(ConfluentMessage): 'state': state, 'serial': serial, 'fru': fru, + 'array': array, } } diff --git a/confluent_server/confluent/plugins/hardwaremanagement/ipmi.py b/confluent_server/confluent/plugins/hardwaremanagement/ipmi.py index cbcc1c83..d236c55c 100644 --- a/confluent_server/confluent/plugins/hardwaremanagement/ipmi.py +++ b/confluent_server/confluent/plugins/hardwaremanagement/ipmi.py @@ -1005,12 +1005,24 @@ class IpmiHandler(object): msg.Disk(self.node, disk.name, disk.description, disk.id, disk.status, disk.serial, disk.fru)) + for arr in scfg.arrays: + arrname = '{0}-{1}'.format(*arr.id) + for disk in arr.disks: + if (name == 'all' or simplify_name(disk.name) == name or + disk == name): + self.output.put( + msg.Disk(self.node, disk.name, disk.description, + disk.id, disk.status, disk.serial, + disk.fru, arrname)) def list_disks(self): scfg = self.ipmicmd.get_storage_configuration() self.output.put(msg.ChildCollection('all')) for disk in scfg.disks: self.output.put(msg.ChildCollection(simplify_name(disk.name))) + for arr in scfg.arrays: + for disk in arr.disks: + self.output.put(msg.ChildCollection(simplify_name(disk.name))) def list_arrays(self): scfg = self.ipmicmd.get_storage_configuration() @@ -1018,6 +1030,30 @@ class IpmiHandler(object): for arr in scfg.arrays: self.output.put(msg.ChildCollection('{0}-{1}'.format(*arr.id))) + def show_array(self, name): + scfg = self.ipmicmd.get_storage_configuration() + for arr in scfg.arrays: + arrname = '{0}-{1}'.format(*arr.id) + if arrname == name: + vols = [] + for vol in arr.volumes: + vols.append(simplify_name(vol.name)) + disks = [] + for disk in arr.disks: + disks.append(simplify_name(disk.name)) + self.output.put(msg.Array(self.node, disks, arr.raid, + vols, arrname, arr.capacity, + arr.available_capacity)) + + def show_volume(self, name): + scfg = self.ipmicmd.get_storage_configuration() + for arr in scfg.arrays: + arrname = '{0}-{1}'.format(*arr.id) + for vol in arr.volumes: + if name == simplify_name(vol.name): + self.output.put(msg.Volume(self.node, vol.name, vol.size, + vol.status, arrname)) + def list_volumes(self): scfg = self.ipmicmd.get_storage_configuration() self.output.put(msg.ChildCollection('all')) From fd17559a73099f05ad857e1a0a53ba45613b3982 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Thu, 25 Oct 2018 16:11:54 -0400 Subject: [PATCH 4/6] Show result of changing disk state. When changing disk state, let the caller know the result explicitly. --- confluent_server/confluent/plugins/hardwaremanagement/ipmi.py | 1 + 1 file changed, 1 insertion(+) diff --git a/confluent_server/confluent/plugins/hardwaremanagement/ipmi.py b/confluent_server/confluent/plugins/hardwaremanagement/ipmi.py index d236c55c..d2f5f427 100644 --- a/confluent_server/confluent/plugins/hardwaremanagement/ipmi.py +++ b/confluent_server/confluent/plugins/hardwaremanagement/ipmi.py @@ -948,6 +948,7 @@ class IpmiHandler(object): raise exc.InvalidArgumentException('Must target a disk') self.set_disk(storelem[-1], self.inputdata.inputbynode[self.node]) + self._show_storage(storelem) def _show_storage(self, storelem): if storelem[0] == 'disks': From c2d52d4f8372496ac6fa71652ac7684b2745361a Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Thu, 25 Oct 2018 16:31:32 -0400 Subject: [PATCH 5/6] Add functionality to delete volumes Further enriching the storage api by adding the ability to delete a volume. --- .../confluent/plugins/hardwaremanagement/ipmi.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/confluent_server/confluent/plugins/hardwaremanagement/ipmi.py b/confluent_server/confluent/plugins/hardwaremanagement/ipmi.py index d2f5f427..27871955 100644 --- a/confluent_server/confluent/plugins/hardwaremanagement/ipmi.py +++ b/confluent_server/confluent/plugins/hardwaremanagement/ipmi.py @@ -941,6 +941,22 @@ class IpmiHandler(object): return self._show_storage(storelem) elif 'update' == self.op: return self._update_storage(storelem) + elif 'delete' == self.op: + return self._delete_storage(storelem) + + def _delete_storage(self, storelem): + if len(storelem) < 2 or storelem[0] != 'volumes': + raise exc.InvalidArgumentException('Must target a specific volume') + volname = storelem[-1] + curr = self.ipmicmd.get_storage_configuration() + volumes = [] + toremove = storage.ConfigSpec(arrays=[storage.Array(volumes=volumes)]) + for pool in curr.arrays: + for vol in pool.volumes: + if simplify_name(vol.name) == volname: + volumes.append(vol) + self.ipmicmd.remove_storage_configuration(toremove) + def _update_storage(self, storelem): if storelem[0] == 'disks': From 73cab3774dbf5c0c465cfee17a3cc057a27b9408 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Fri, 26 Oct 2018 14:31:28 -0400 Subject: [PATCH 6/6] Add support for volume creation --- confluent_client/bin/confetty | 3 +- confluent_server/confluent/messages.py | 57 ++++++++++++++++++ .../plugins/hardwaremanagement/ipmi.py | 59 +++++++++++++++---- 3 files changed, 107 insertions(+), 12 deletions(-) diff --git a/confluent_client/bin/confetty b/confluent_client/bin/confetty index 6ad1896c..a321a141 100755 --- a/confluent_client/bin/confetty +++ b/confluent_client/bin/confetty @@ -478,7 +478,8 @@ def createresource(args): collection = targpath else: collection, _, resname = targpath.rpartition('/') - keydata['name'] = resname + if 'name' not in keydata: + keydata['name'] = resname makecall(session.create, (collection, keydata)) diff --git a/confluent_server/confluent/messages.py b/confluent_server/confluent/messages.py index 6badb1d4..28dbfb34 100644 --- a/confluent_server/confluent/messages.py +++ b/confluent_server/confluent/messages.py @@ -422,6 +422,9 @@ def get_input_message(path, operation, inputdata, nodes=None, multinode=False, elif (path[:3] == ['configuration', 'storage', 'disks'] and operation != 'retrieve'): return InputDisk(path, nodes, inputdata) + elif (path[:3] == ['configuration', 'storage', 'volumes'] and + operation in ('update', 'create')): + return InputVolumes(path, nodes, inputdata) elif 'inventory/firmware/updates/active' in '/'.join(path) and inputdata: return InputFirmwareUpdate(path, nodes, inputdata) elif '/'.join(path).startswith('media/detach'): @@ -713,6 +716,60 @@ class InputDisk(ConfluentInputMessage): keyname = 'state' +class InputVolumes(ConfluentInputMessage): + def __init__(self, path, nodes, inputdata): + self.inputbynode = {} + self.stripped = False + if not inputdata: + raise exc.InvalidArgumentException('missing input data') + if isinstance(inputdata, dict): + volnames = [None] + if len(path) == 6: + volnames = path[-1] + volnames = inputdata.get('name', volnames) + if not isinstance(volnames, list): + volnames = volnames.split(',') + sizes = inputdata.get('size', [None]) + if not isinstance(sizes, list): + sizes = sizes.split(',') + disks = inputdata.get('disks', []) + if not disks: + raise exc.InvalidArgumentException( + 'disks are currently required to create a volume') + raidlvl = inputdata.get('raidlevel', None) + inputdata = [] + for size in sizes: + if volnames: + currname = volnames.pop(0) + else: + currname = None + inputdata.append( + {'name': currname, 'size': size, + 'disks': disks, + 'raidlevel': raidlvl}) + for node in nodes: + self.inputbynode[node] = [] + for input in inputdata: + volname = None + if len(path) == 6: + volname = path[-1] + volname = input.get('name', volname) + if not volname: + volname = None + volsize = input.get('size', None) + if isinstance(input['disks'], list): + disks = input['disks'] + else: + disks = input['disks'].split(',') + raidlvl = input.get('raidlevel', None) + for node in nodes: + self.inputbynode[node].append({'name': volname, + 'size': volsize, + 'disks': disks, + 'raidlevel': raidlvl, + }) + + class InputPowerMessage(ConfluentInputMessage): valid_values = set([ 'on', diff --git a/confluent_server/confluent/plugins/hardwaremanagement/ipmi.py b/confluent_server/confluent/plugins/hardwaremanagement/ipmi.py index 27871955..6b6ba079 100644 --- a/confluent_server/confluent/plugins/hardwaremanagement/ipmi.py +++ b/confluent_server/confluent/plugins/hardwaremanagement/ipmi.py @@ -341,7 +341,7 @@ class IpmiConsole(conapi.Console): self.solconnection.send_break() -def perform_requests(operator, nodes, element, cfg, inputdata): +def perform_requests(operator, nodes, element, cfg, inputdata, realop): cryptit = cfg.decrypt cfg.decrypt = True configdata = cfg.get_node_attributes(nodes, _configattributes) @@ -351,7 +351,7 @@ def perform_requests(operator, nodes, element, cfg, inputdata): for node in nodes: livingthreads.add(_ipmiworkers.spawn( perform_request, operator, node, element, configdata, inputdata, - cfg, resultdata)) + cfg, resultdata, realop)) while livingthreads: try: datum = resultdata.get(timeout=10) @@ -377,10 +377,10 @@ def perform_requests(operator, nodes, element, cfg, inputdata): def perform_request(operator, node, element, - configdata, inputdata, cfg, results): + configdata, inputdata, cfg, results, realop): try: return IpmiHandler(operator, node, element, configdata, inputdata, - cfg, results).handle_request() + cfg, results, realop).handle_request() except pygexc.IpmiException as ipmiexc: excmsg = str(ipmiexc) if excmsg in ('Session no longer connected', 'timeout'): @@ -410,7 +410,8 @@ def perform_request(operator, node, element, persistent_ipmicmds = {} class IpmiHandler(object): - def __init__(self, operation, node, element, cfd, inputdata, cfg, output): + def __init__(self, operation, node, element, cfd, inputdata, cfg, output, + realop): self.sensormap = {} self.invmap = {} self.output = output @@ -424,6 +425,7 @@ class IpmiHandler(object): self.node = node self.element = element self.op = operation + self.realop = realop connparams = get_conn_params(node, self.cfg) self.ipmicmd = None self.inputdata = inputdata @@ -939,10 +941,12 @@ class IpmiHandler(object): storelem = self.element[2:] if 'read' == self.op: return self._show_storage(storelem) - elif 'update' == self.op: + elif 'update' == self.realop: return self._update_storage(storelem) elif 'delete' == self.op: return self._delete_storage(storelem) + elif 'create' == self.realop: + return self._create_storage(storelem) def _delete_storage(self, storelem): if len(storelem) < 2 or storelem[0] != 'volumes': @@ -956,7 +960,39 @@ class IpmiHandler(object): if simplify_name(vol.name) == volname: volumes.append(vol) self.ipmicmd.remove_storage_configuration(toremove) + self.output.put(msg.DeletedResource(volname)) + def _create_storage(self, storelem): + if 'volumes' not in storelem: + raise exc.InvalidArgumentException('Can only create volumes') + vols = [] + thedisks = None + currcfg = self.ipmicmd.get_storage_configuration() + disks = [] + vols = [] + vol = self.inputdata.inputbynode[self.node][0] + raidlvl = vol['raidlevel'] + for disk in currcfg.disks: + if simplify_name(disk.name) in vol['disks']: + disks.append(disk) + elif (disk.status == 'Unconfigured Good' and + vol['disks'][0] in ('remainder', 'rest')): + disks.append(disk) + elif vol['disks'][0] == 'all': + disks.append(disk) + for vol in self.inputdata.inputbynode[self.node]: + if thedisks and thedisks != vol['disks']: + raise exc.InvalidArgumentException( + 'Not currently supported to create multiple arrays ' + 'in a single request') + if raidlvl and vol['raidlevel'] != raidlvl: + raise exc.InvalidArgumentException('Cannot mix raid levels in ' + 'a single array') + vols.append(storage.Volume(name=vol['name'], size=vol['size'])) + newcfg = storage.ConfigSpec( + arrays=(storage.Array(raid=raidlvl, disks=disks, volumes=vols),)) + self.ipmicmd.apply_storage_configuration(newcfg) + return self._show_storage(storelem) def _update_storage(self, storelem): if storelem[0] == 'disks': @@ -1315,7 +1351,7 @@ def initthread(): _ipmithread = eventlet.spawn(_ipmi_evtloop) -def create(nodes, element, configmanager, inputdata): +def create(nodes, element, configmanager, inputdata, realop='create'): initthread() if element == ['_console', 'session']: if len(nodes) > 1: @@ -1323,12 +1359,12 @@ def create(nodes, element, configmanager, inputdata): return IpmiConsole(nodes[0], configmanager) else: return perform_requests( - 'update', nodes, element, configmanager, inputdata) + 'update', nodes, element, configmanager, inputdata, realop) def update(nodes, element, configmanager, inputdata): initthread() - return create(nodes, element, configmanager, inputdata) + return create(nodes, element, configmanager, inputdata, 'update') def retrieve(nodes, element, configmanager, inputdata): @@ -1343,7 +1379,8 @@ def retrieve(nodes, element, configmanager, inputdata): return firmwaremanager.list_updates(nodes, configmanager.tenant, element, 'ffdc') else: - return perform_requests('read', nodes, element, configmanager, inputdata) + return perform_requests('read', nodes, element, configmanager, + inputdata, 'read') def delete(nodes, element, configmanager, inputdata): initthread() @@ -1357,5 +1394,5 @@ def delete(nodes, element, configmanager, inputdata): return firmwaremanager.remove_updates(nodes, configmanager.tenant, element, type='ffdc') return perform_requests( - 'delete', nodes, element, configmanager, inputdata) + 'delete', nodes, element, configmanager, inputdata, 'delete')