# Copyright 2014 IBM Corporation
# Copyright 2015-2018 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.exceptions as exc
from confluent.exceptions import exc_text
import confluent.firmwaremanager as firmwaremanager
import confluent.interface.console as conapi
import confluent.messages as msg
import confluent.util as util
import copy
import errno
from fnmatch import fnmatch
import io
import os
import pwd
import aiohmi.constants as pygconstants
import aiohmi.exceptions as pygexc
import aiohmi.storage as storage
import aiohmi.ipmi.console as console
import aiohmi.ipmi.command as ipmicommand
import socket
import ssl
import traceback
import confluent.vinzmanager as vinzmanager


if not hasattr(ssl, 'SSLEOFError'):
    ssl.SSLEOFError = None

try:
    range = xrange
except NameError:
    pass

pci_cache = {}

class RetainedIO(io.BytesIO):
    # Need to retain buffer after close
    def __init__(self):
        self.resultbuffer = None
    def close(self):
        buf = self.getbuffer()
        self.resultbuffer = bytes(buf)
        del buf
        super().close()

def get_dns_txt(qstring):
    return None
    # return support.greendns.resolver.query(
    #    qstring, 'TXT')[0].strings[0].replace('i=', '')


def match_aliases(first, second):
    aliases = {
        ('bmc', 'xcc')
        }
    for alias in aliases:
        if first in alias and second in alias:
            return True
    return False

def get_pci_text_from_ids(subdevice, subvendor, device, vendor):
    fqpi = '{0}.{1}.{2}.{3}'.format(subdevice, subvendor, device, vendor)
    if fqpi in pci_cache:
        return pci_cache[fqpi]
    vendorstr = None
    try:
        vendorstr = get_dns_txt('{0}.pci.id.ucw.cz'.format(subvendor))
    except Exception:
        try:
            vendorstr = get_dns_txt('{0}.pci.id.ucw.cz'.format(vendor))
        except Exception:
            pass
    devstr = None
    try:
        devstr = get_dns_txt(fqpi + '.pci.id.ucw.cz')
    except Exception:
        try:
            devstr = get_dns_txt('{0}.{1}.pci.id.ucw.cz'.format(
                device, vendor))
        except Exception:
            pass
    if vendorstr and devstr:
        pci_cache[fqpi] = vendorstr, devstr
    return vendorstr, devstr


_ipmithread = None
_ipmiwaiters = []

sensor_categories = {
    'temperature': frozenset(['Temperature']),
    'energy': frozenset(['Energy']),
    'power': frozenset(['Power', 'Current']),
    'fans': frozenset(['Fan', 'Cooling Device']),
}


class EmptySensor(object):
    def __init__(self, name):
        self.name = name
        self.value = None
        self.states = ['Unavailable']
        self.units = None
        self.health = 'ok'


def hex2bin(hexstring):
    hexvals = hexstring.split(':')
    if len(hexvals) < 2:
        hexvals = hexstring.split(' ')
    if len(hexvals) < 2:
        hexvals = [hexstring[i:i+2] for i in range(0, len(hexstring), 2)]
    bytedata = [int(i, 16) for i in hexvals]
    return bytearray(bytedata)


def simplify_name(name):
    return name.lower().replace(' ', '_').replace('/', '-').replace(
        '_-_', '-')


def sanitize_invdata(indata):
    """Sanitize pyghmi data

    pyghmi will return bytearrays when it has no idea what to do.  In our
    case, we will change those to hex strings.  Additionally, ignore 'extra'
    fields if the oem_parser is set
    """
    if 'oem_parser' in indata and indata['oem_parser'] is not None:
        if 'board_extra' in indata:
            del indata['board_extra']
        if 'chassis_extra' in indata:
            del indata['chassis_extra']
        if 'product_extra' in indata:
            del indata['product_extra']
    for k in indata:
        if isinstance(indata[k], bytearray):
            indata[k] = '0x' + ''.join(format(x, '02x') for x in indata[k])
        elif isinstance(indata[k], dict):
            sanitize_invdata(indata[k])
        elif isinstance(indata[k], list):
            for idx, value in enumerate(indata[k]):
                if isinstance(value, bytearray):
                    indata[k][idx] = '0x' + ''.join(
                        format(x, '02x') for x in indata[k][idx])


class IpmiCommandWrapper(ipmicommand.Command):
    @classmethod
    async def create(cls, node, cfm, **kwargs):
        kwargs['keepalive'] = False
        self = await super().create(**kwargs)
        self.confluentbmcname = kwargs['bmc']
        self.cfm = cfm
        self.node = node
        self.sensormap = {}
        self._inhealth = False
        self._lasthealth = None
        self._attribwatcher = cfm.watch_attributes(
            (node,), ('secret.hardwaremanagementuser', 'collective.manager',
                      'secret.hardwaremanagementpassword', 'secret.ipmikg',
                      'hardwaremanagement.manager'), self._attribschanged)
        self.setup_confluent_keyhandler()
        try:
            os.makedirs('/var/cache/confluent/ipmi/')
        except OSError as e:
            if e.errno != errno.EEXIST or not os.path.isdir(
                    '/var/cache/confluent/ipmi/'):
                raise
        try:
            self.set_sdr_cachedir('/var/cache/confluent/ipmi/')
        except Exception:
            pass
        return self


    def setup_confluent_keyhandler(self):
        self.register_key_handler(util.TLSCertVerifier(
            self.cfm, self.node, 'pubkeys.tls_hardwaremanager').verify_cert)

    def close_confluent(self):
        if self._attribwatcher:
            self.cfm.remove_watcher(self._attribwatcher)
            self._attribwatcher = None

    async def _attribschanged(self, nodeattribs, configmanager, **kwargs):
        try:
            await self.ipmi_session._mark_broken()
        except AttributeError:
            # if ipmi_session doesn't already exist,
            # then do nothing
            pass

    async def get_health(self):
        if self._inhealth:
            while self._inhealth:
                await asyncio.sleep(0.1)
            return self._lasthealth
        self._inhealth = True
        try:
            self._lasthealth = await super(IpmiCommandWrapper, self).get_health()
        except Exception:
            self._inhealth = False
            raise
        self._inhealth = False
        return self._lasthealth


async def _ipmi_evtloop():
    while True:
        try:
            await console.session.Session.wait_for_rsp(timeout=600)
            while _ipmiwaiters:
                waiter = _ipmiwaiters.pop()
                waiter.send()
        except asyncio.CancelledError:
            break
        except:  # TODO(jbjohnso): log the trace into the log

            traceback.print_exc()


def get_conn_params(node, configdata):
    if 'secret.hardwaremanagementuser' in configdata:
        username = configdata['secret.hardwaremanagementuser']['value']
    else:
        username = 'USERID'
    if 'secret.hardwaremanagementpassword' in configdata:
        passphrase = configdata['secret.hardwaremanagementpassword']['value']
    else:
        passphrase = 'PASSW0RD'  # for lack of a better guess
    if 'hardwaremanagement.manager' in configdata:
        bmc = configdata['hardwaremanagement.manager']['value']
    else:
        bmc = node
    bmc = bmc.split('/', 1)[0]
    if 'secret.ipmikg' in configdata:
        kg = configdata['secret.ipmikg']['value']
    else:
        kg = passphrase
    # Read a port off the address, as the redfish plugin does. Brackets come
    # off, unlike there: this address goes to socket.getaddrinfo, which rejects
    # a bracketed literal, rather than into a URL where one is required.
    bmc = bmc.strip()
    port = 623
    if bmc.startswith('['):
        bracket_end = bmc.find(']')
        if bracket_end > 0:
            if len(bmc) > bracket_end + 1 and bmc[bracket_end + 1] == ':':
                try:
                    port = int(bmc[bracket_end + 2:])
                except (ValueError, TypeError):
                    pass
            bmc = bmc[1:bracket_end]
    elif bmc.count(':') == 1:
        hostpart, _, portstr = bmc.rpartition(':')
        try:
            port = int(portstr)
        except (ValueError, TypeError):
            pass
        bmc = hostpart
    if not 0 < port <= 65535:
        # NOTE: Fallback to 623 if port read is not valid
        port = 623

    return {
        'username': username,
        'passphrase': passphrase,
        'kg': kg,
        'bmc': bmc,
        'port': port,
    }


_configattributes = ('secret.hardwaremanagementuser',
                     'secret.hardwaremanagementpassword',
                     'secret.ipmikg', 'hardwaremanagement.manager')


def _donothing(data):
    # a dummy function to avoid some awkward exceptions from
    # zombie pyghmi console objects
    pass


class IpmiConsole(conapi.Console):
    configattributes = frozenset(_configattributes)
    bmctonodemapping = {}
    # Whether this instance is the one that put its endpoint in the mapping
    # above. False until it does, so that an instance rejected as a duplicate,
    # or one that failed earlier in __init__, does not remove on the way out an
    # entry that belongs to the node holding the endpoint.
    claimedbmc = False

    def __init__(self, node, config):
        self.error = None
        self.datacallback = None
        crypt = config.decrypt
        self.solconnection = None
        config.decrypt = True
        self.broken = False
        configdata = config.get_node_attributes([node], _configattributes)
        connparams = get_conn_params(node, configdata[node])
        config.decrypt = crypt
        self.username = connparams['username']
        self.password = connparams['passphrase']
        self.kg = connparams['kg']
        self.bmc = connparams['bmc']
        self.port = connparams['port']
        # The port is part of the identity: several bmcs may sit behind one
        # address on different ports, and they are distinct devices.
        self.bmckey = (self.bmc, self.port)
        self.connected = False
        # ok, is this bmc unique among nodes already
        # Cannot actually create console until 'connect', when we get callback
        if (self.bmckey in self.bmctonodemapping and
                self.bmctonodemapping[self.bmckey] != node):
            raise Exception(
                "Duplicate hardwaremanagement.manager attribute for {0} and {1}".format(
                    node, self.bmctonodemapping[self.bmckey]))
        self.bmctonodemapping[self.bmckey] = node
        self.claimedbmc = True

    def __del__(self):
        self.solconnection = None
        if not self.claimedbmc:
            return
        try:
            del self.bmctonodemapping[self.bmckey]
        except KeyError:
            pass

    async def handle_data(self, data):
        if type(data) == dict:
            if 'error' in data:
                self.solconnection = None
                self.broken = True
                self.error = data['error']
                if self.connected:
                    self.connected = False
                    await self.datacallback(conapi.ConsoleEvent.Disconnect)
        else:
            await self.datacallback(data)

    async def connect(self, callback):
        self.datacallback = callback
        # we provide a weak reference to pyghmi as otherwise we'd
        # have a circular reference and reference counting would never get
        # out...
        try:
            self.solconnection = console.Console(bmc=self.bmc, port=self.port,
                                                 userid=self.username,
                                                 password=self.password,
                                                 kg=self.kg, force=True,
                                                 iohandler=self.handle_data)
            try:
                await self.solconnection.connect()
            except pygexc.IpmiException as e:
                raise exc.TargetEndpointUnreachable(str(e))
            if (self.broken or not self.solconnection or
                    self.solconnection.broken or
                    self.solconnection.ipmi_session.broken):
                if not self.error:
                    self.error = 'Unknown error'
                if (self.error.startswith('Incorrect password') or
                        self.error.startswith('Unauthorized name')):
                    raise exc.TargetEndpointBadCredentials()
                else:
                    raise exc.TargetEndpointUnreachable(self.error)
            self.connected = True
        except socket.gaierror as err:
            raise exc.TargetEndpointUnreachable(str(err))

    async def close(self):
        if self.solconnection is not None:
            # break the circular reference here
            self.solconnection.out_handler = _donothing
            await self.solconnection.close()
        self.solconnection = None
        self.datacallback = None
        self.broken = True
        self.error = "closed"

    async def write(self, data):
        if self.solconnection:
            await self.solconnection.send_data(data)

    async def send_break(self):
        if self.solconnection:
            await self.solconnection.send_break()


async def perform_requests(operator, nodes, element, cfg, inputdata, realop):
    cryptit = cfg.decrypt
    cfg.decrypt = True
    configdata = cfg.get_node_attributes(nodes, _configattributes)
    cfg.decrypt = cryptit
    resultdata = asyncio.Queue()
    livingthreads = set([])
    numnodes = len(nodes)
    for node in nodes:
        livingthreads.add(asyncio.create_task(perform_request(operator, node, element, configdata, inputdata,
            cfg, resultdata, realop)))
        #livingthreads.add(_ipmiworkers.spawn(
        #    perform_request, operator, node, element, configdata, inputdata,
        #    cfg, resultdata, realop))
    while livingthreads:
        try:
            bundle = []
            datum = await asyncio.wait_for(resultdata.get(), timeout=10.0)
            while datum:
                if datum == 'Done':
                    datum = None
                    break
                if isinstance(datum, Exception):
                    raise datum
                if (hasattr(datum, 'kvpairs') and datum.kvpairs and
                        len(datum.kvpairs) == 1):
                    bundle.append((list(datum.kvpairs)[0], datum))
                    numnodes -= 1
                else:
                    yield datum
                timeout = 0.1 if numnodes else 0.001
                datum = await asyncio.wait_for(resultdata.get(), timeout=timeout)
        except asyncio.QueueEmpty:
            pass
        except asyncio.TimeoutError:
            # the timeout is just a sign to push out pending bundled stuff
            pass
        finally:
            for datum in sorted(
                    bundle, key=lambda x: util.naturalize_string(x[0])):
                yield datum[1]
        for t in list(livingthreads):
            if t.done():
                livingthreads.discard(t)
    try:
        # drain queue if a thread put something on the queue and died
        while True:
            datum = resultdata.get_nowait()
            if datum != 'Done':
                yield datum
            await asyncio.sleep(0)
    except asyncio.QueueEmpty:
        pass


async def perform_request(operator, node, element,
                          configdata, inputdata, cfg, results, realop):
    try:
        ih = await IpmiHandler.create(
            operator, node, element, configdata, inputdata, cfg, results,
            realop)
        return await ih.handle_request()
    except pygexc.UnsupportedFunctionality as ue:
        # Not an unexpected error, just something this target cannot do, so say
        # so plainly and without a traceback in the log
        await results.put(msg.ConfluentNodeError(node, exc_text(ue)))
    except pygexc.IpmiException as ipmiexc:
        excmsg = exc_text(ipmiexc)
        if excmsg in ('Session no longer connected', 'timeout'):
            await results.put(msg.ConfluentTargetTimeout(node))
        elif 'Unauthorized name' in excmsg or 'Incorrect password provided' in excmsg:
            await results.put(msg.ConfluentTargetInvalidCredentials(node))
        else:
            await results.put(msg.ConfluentNodeError(node, excmsg))
            #raise
    except exc.TargetEndpointUnreachable as tu:
        await results.put(msg.ConfluentTargetTimeout(node, exc_text(tu)))
    except ssl.SSLEOFError:
        await results.put(msg.ConfluentNodeError(
            node, 'Unable to communicate with the https server on '
                    'the target BMC'))
    except exc.PubkeyInvalid:
        await results.put(msg.ConfluentNodeError(
            node,
            'Mismatch detected between target certificate fingerprint '
            'and pubkeys.tls_hardwaremanager attribute'))
    except pygexc.InvalidParameterValue as e:
        await results.put(msg.ConfluentNodeError(node, exc_text(e)))
    except Exception as e:
        await results.put(msg.ConfluentNodeError(
            node, 'Unexpected Error: {0}'.format(exc_text(e))))
        traceback.print_exc()
    finally:
        await results.put('Done')

persistent_ipmicmds = {}

class IpmiHandler:
    @classmethod
    async def create(cls, operation, node, element, cfd, inputdata, cfg, output,
                 realop):
        self = cls()
        self.cfm = cfg
        self.invmap = {}
        self.output = output
        self.sensorcategory = None
        self.broken = False
        self.error = None
        await asyncio.sleep(0)
        self.cfg = cfd[node]
        self.current_user = cfg.current_user
        self.loggedin = False
        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
        self.tenant = cfg.tenant
        tenant = cfg.tenant
        while ((node, tenant) not in persistent_ipmicmds or
                not (persistent_ipmicmds[(node, tenant)].ipmi_session.logged or persistent_ipmicmds[(node, tenant)].ipmi_session.logging) or
                persistent_ipmicmds[(node, tenant)].ipmi_session.broken):
            try:
                persistent_ipmicmds[(node, tenant)].close_confluent()
                await persistent_ipmicmds[(node, tenant)].ipmi_session._mark_broken()
                persistent_ipmicmds[(node, tenant)].ipmi_session.logonwaiters = []
            except KeyError:  # was no previous session
                pass
            try:
                persistent_ipmicmds[(node, tenant)] = await IpmiCommandWrapper.create(
                    node, cfg, bmc=connparams['bmc'],
                    userid=connparams['username'],
                    password=connparams['passphrase'], kg=connparams['kg'],
                    port=connparams['port'])
                ipmisess = persistent_ipmicmds[(node, tenant)].ipmi_session
                self.ipmicmd = persistent_ipmicmds[(node, tenant)]
                self.loggedin = True
                begin = util.monotonic_time()
                while ((not (ipmisess.broken or self.loggedin)) and
                               (util.monotonic_time() - begin) < 30):
                    await ipmisess.wait_for_rsp(31 - (util.monotonic_time() - begin))
                if self.broken or self.loggedin:
                    break
                cfd = cfg.get_node_attributes(node, _configattributes, decrypt=True)
                self.cfg = cfd[node]
                connparams = get_conn_params(node, self.cfg)
                await ipmisess._mark_broken()
                #    raise exc.TargetEndpointUnreachable(
                #        "Login process to " + connparams['bmc'] + " died")
            except socket.gaierror as ge:
                if ge.errno == -2:
                    raise exc.TargetEndpointUnreachable(ge.strerror)
                raise
        self.ipmicmd = persistent_ipmicmds[(node, tenant)]
        giveup = util.monotonic_time() + 60
        while not self.ipmicmd.ipmi_session.broken and not self.ipmicmd.ipmi_session.logged and self.ipmicmd.ipmi_session.logging:
            await self.ipmicmd.ipmi_session.wait_for_rsp(3)
            if util.monotonic_time() > giveup:
                self.ipmicmd.ipmi_session.broken = True
        return self

    bootdevices = {
        'optical': 'cd'
    }

    def logged(self, response, ipmicmd):
        if 'error' in response:
            self.broken = True
            self.error = response['error']
        else:
            self.ipmicmd = ipmicmd
            self.loggedin = True

    def _unsupported(self):
        """Say that the requested resource has no ipmi implementation.

        Falling through to a bare exception would be reported as an unexpected
        error and logged with a traceback, when all that happened is that this
        transport does not offer the resource.
        """
        return pygexc.UnsupportedFunctionality(
            '"{0}" is not implemented for ipmi'.format(
                '/'.join([str(x) for x in self.element])))

    async def handle_request(self):
        if self.broken:
            if (self.error == 'timeout' or
                    'Insufficient resources' in self.error):
                self.error = self.error.replace(' reported in RAKP4', '')
                await self.output.put(msg.ConfluentTargetTimeout(
                    self.node, self.error))
                return
            elif 'Invalid Session ID' in self.error:
                await self.output.put(msg.ConfluentTargetTimeout(
                    self.node, 'Temporary Login Error'))
                return
            elif ('Unauthorized' in self.error or
                    'Incorrect password' in self.error):
                await self.output.put(
                    msg.ConfluentTargetInvalidCredentials(self.node))
                return
            else:
                raise Exception(self.error)
        if self.element == ['power', 'state']:
            await self.power()
        elif self.element == ['_enclosure', 'reseat_bay']:
            await self.reseat_bay()
        elif self.element == ['boot', 'nextdevice']:
            await self.bootdevice()
        elif self.element == ['health', 'hardware']:
            await self.health()
        elif self.element == ['identify']:
            await self.identify()
        elif self.element[0] == 'sensors':
            await self.handle_sensors()
        elif self.element[:2] == ['configuration', 'storage']:
            await self.handle_storage()
        elif self.element[0] == 'configuration':
            await self.handle_configuration()
        elif self.element[:3] == ['inventory', 'firmware', 'updates']:
            await self.handle_update()
        elif self.element[:3] == ['inventory', 'firmware', 'updatestatus']:
            await self.handle_update_status()
        elif self.element[:3] == ['inventory', 'firmware', 'updatetypes']:
            # Otherwise this falls through to the inventory handler and is read
            # as the name of a firmware component
            raise self._unsupported()
        elif self.element[0] == 'inventory':
            await self.handle_inventory()
        elif self.element == ['media', 'attach']:
            await self.handle_attach_media()
        elif self.element == ['media', 'detach']:
            await self.handle_detach_media()
        elif self.element == ['media', 'uploads']:
            await self.handle_media_upload()
        elif self.element == ['media', 'current']:
            await self.handle_list_media()
        elif self.element == ['events', 'hardware', 'log']:
            await self.do_eventlog()
        elif self.element == ['events', 'hardware', 'decode']:
            await self.decode_alert()
        elif self.element == ['console', 'license']:
            await self.handle_license()
        elif self.element == ['console', 'graphical']:
            await self.handle_graphical_console()
        elif self.element == ['support', 'servicedata']:
            await self.handle_servicedata_fetch()
        elif self.element == ['description']:
            await self.handle_description()
        elif self.element == ['console', 'ikvm_methods']:
            await self.handle_ikvm_methods()
        elif self.element == ['console', 'ikvm_screenshot']:
            await self.handle_ikvm_screenshot()
        elif self.element == ['console', 'ikvm']:
            await self.handle_ikvm()
        else:
            raise self._unsupported()

    async def handle_update(self):
        u = firmwaremanager.Updater(self.node, self.ipmicmd.update_firmware,
                                    self.inputdata.nodefile(self.node), self.tenant,
                                    bank=self.inputdata.bank, configmanager=self.cfm)
        await self.output.put(
            msg.CreatedResource(
                'nodes/{0}/inventory/firmware/updates/active/{1}'.format(
                    self.node, u.name)))

    async def handle_media_upload(self):
        u = firmwaremanager.Updater(self.node, self.ipmicmd.upload_media,
                                     self.inputdata.nodefile(self.node), self.tenant,
                                     type='mediaupload', configmanager=self.cfm)
        await self.output.put(msg.CreatedResource(
            'nodes/{0}/media/uploads/{1}'.format(self.node, u.name)))

    def get_diags(self, savefile, progress, data=None):
        return self.ipmicmd.get_diagnostic_data(
            savefile, progress=progress, autosuffix=True)

    async def handle_servicedata_fetch(self):
        u = firmwaremanager.Updater(
            self.node, self.get_diags,
            self.inputdata.nodefile(self.node), self.tenant, type='ffdc',
            owner=self.current_user)
        await self.output.put(msg.CreatedResource(
            'nodes/{0}/support/servicedata/{1}'.format(self.node, u.name)))

    async def handle_attach_media(self):
        try:
            await self.ipmicmd.attach_remote_media(self.inputdata.nodefile(
                self.node))
        except pygexc.UnsupportedFunctionality as uf:
            await self.output.put(msg.ConfluentNodeError(self.node, str(uf)))

    async def handle_detach_media(self):
        await self.ipmicmd.detach_remote_media()

    async def handle_list_media(self):
        async for media in self.ipmicmd.list_media():
            await self.output.put(msg.Media(self.node, media))

    async def handle_configuration(self):
        if self.element[1:3] == ['management_controller', 'alerts']:
            return await self.handle_alerts()
        elif self.element[1:3] == ['management_controller', 'users']:
            return await self.handle_users()
        elif self.element[1:3] == ['management_controller', 'net_interfaces']:
            return await self.handle_nets()
        elif self.element[1:3] == ['management_controller', 'reset']:
            return await self.handle_reset()
        elif self.element[1:3] == ['management_controller', 'identifier']:
            return await self.handle_identifier()
        elif self.element[1:3] == ['management_controller', 'hostname']:
            return await self.handle_hostname()
        elif self.element[1:3] == ['management_controller', 'domain_name']:
            return await self.handle_domain_name()
        elif self.element[1:3] == ['management_controller', 'ntp']:
            return await self.handle_ntp()
        elif self.element[1:4] == ['management_controller', 'extended', 'all']:
            return await self.handle_bmcconfig()
        elif self.element[1:4] == ['management_controller', 'extended', 'advanced']:
            return await self.handle_bmcconfig(True)
        elif self.element[1:4] == ['management_controller', 'extended', 'extra']:
            return await self.handle_bmcconfig(advanced=False, extended=True)
        elif self.element[1:4] == ['management_controller', 'extended', 'extra_advanced']:
            return await self.handle_bmcconfig(advanced=True, extended=True)
        elif self.element[1:3] == ['system', 'all']:
            return await self.handle_sysconfig()
        elif self.element[1:3] == ['system', 'advanced']:
            return await self.handle_sysconfig(True)
        elif self.element[1:3] == ['system', 'clear']:
            return await self.handle_sysconfigclear()
        elif self.element[1:3] == ['management_controller', 'clear']:
            return await self.handle_bmcconfigclear()
        elif self.element[1:3] == ['management_controller', 'licenses']:
            return await self.handle_licenses()
        elif self.element[1:3] == ['management_controller', 'save_licenses']:
            return await self.save_licenses()
        raise self._unsupported()

    async def decode_alert(self):
        inputdata = self.inputdata.get_alert(self.node)
        specifictrap = int(inputdata['.1.3.6.1.6.3.1.1.4.1.0'].rpartition(
            '.')[-1])
        for tmpvarbind in inputdata:
            if tmpvarbind.endswith('3183.1.1'):
                varbinddata = inputdata[tmpvarbind]
        varbinddata = hex2bin(varbinddata)
        event = await self.ipmicmd.decode_pet(specifictrap, varbinddata)
        self.pyghmi_event_to_confluent(event)
        await self.output.put(msg.EventCollection((event,), name=self.node))

    async def handle_alerts(self):
        if self.element[3] == 'destinations':
            if len(self.element) == 4:
                # A list of destinations
                maxdest = await self.ipmicmd.get_alert_destination_count()
                for alertidx in range(0, maxdest + 1):
                    await self.output.put(msg.ChildCollection(alertidx))
                return
            elif len(self.element) == 5:
                alertidx = int(self.element[-1])
                if self.op == 'read':
                    destdata = await self.ipmicmd.get_alert_destination(alertidx)
                    await self.output.put(msg.AlertDestination(
                        ip=destdata['address'],
                        acknowledge=destdata['acknowledge_required'],
                        acknowledge_timeout=destdata.get('acknowledge_timeout', None),
                        retries=destdata['retries'],
                        name=self.node))
                    return
                elif self.op == 'update':
                    alertparms = self.inputdata.alert_params_by_node(
                        self.node)
                    alertargs = {}
                    if 'acknowledge' in alertparms:
                        alertargs['acknowledge_required'] = alertparms['acknowledge']
                    if 'acknowledge_timeout' in alertparms:
                        alertargs['acknowledge_timeout'] = alertparms['acknowledge_timeout']
                    if 'ip' in alertparms:
                        alertargs['ip'] = alertparms['ip']
                    if 'retries' in alertparms:
                        alertargs['retries'] = alertparms['retries']
                    await self.ipmicmd.set_alert_destination(destination=alertidx,
                                                       **alertargs)
                    return
                elif self.op == 'delete':
                    await self.ipmicmd.clear_alert_destination(alertidx)
                    return
        raise self._unsupported()

    async def handle_nets(self):
        if len(self.element) == 3:
            if self.op != 'read':
                await self.output.put(
                    msg.ConfluentNodeError(self.node, 'Unsupported operation'))
                return
            await self.output.put(msg.ChildCollection('management'))
        elif len(self.element) == 4 and self.element[-1] == 'management':
            if self.op == 'read':
                lancfg = await self.ipmicmd.get_net_configuration()
                v6cfg = await self.ipmicmd.get_net6_configuration()
                await self.output.put(msg.NetworkConfiguration(
                    self.node, ipv4addr=lancfg['ipv4_address'],
                    ipv4gateway=lancfg['ipv4_gateway'],
                    ipv4cfgmethod=lancfg['ipv4_configuration'],
                    hwaddr=lancfg['mac_address'],
                    staticv6addrs=v6cfg.get('static_addrs', ''),
                    staticv6gateway=v6cfg.get('static_gateway', ''),
                    vlan_id=lancfg.get('vlan_id', None)
                ))
            elif self.op == 'update':
                config = self.inputdata.netconfig(self.node)
                try:
                    await self.ipmicmd.set_net_configuration(
                        ipv4_address=config['ipv4_address'],
                        ipv4_configuration=config['ipv4_configuration'],
                        ipv4_gateway=config['ipv4_gateway'],
                        vlan_id=config.get('vlan_id', None))
                    v6addrs = config.get('static_v6_addresses', None)
                    if v6addrs is not None:
                        v6addrs = v6addrs.split(',')
                    v6gw = config.get('static_v6_gateway', None)
                    await self.ipmicmd.set_net6_configuration(static_addresses=v6addrs, static_gateway=v6gw)
                except socket.error as se:
                    await self.output.put(msg.ConfluentNodeError(self.node,
                                                           se.message))
                except ValueError as e:
                    if e.message == 'negative shift count':
                        await self.output.put(msg.ConfluentNodeError(
                            self.node, 'Invalid prefix length given'))
                    else:
                        raise
        elif len(self.element) == 4 and self.element[-1] != 'management':
            await self.output.put(
                    msg.ConfluentTargetNotFound(self.node,
                                                'Interface not found'))

    async def handle_users(self):
        # Create user
        if len(self.element) == 3:
            if self.op == 'update':
                user = self.inputdata.credentials[self.node]
                await self.ipmicmd.create_user(uid=user['uid'], name=user['username'],
                                    password=user['password'],
                                    callback=True,link_auth=True, ipmi_msg=True,
                                    privilege_level=user['privilege_level'])
            # A list of users
            await self.output.put(msg.ChildCollection('all'))
            for user in await self.ipmicmd.get_users():
                # The uids are numbers, and a link relation needs a string
                await self.output.put(
                    msg.ChildCollection(str(user), candelete=True))
            return
        # List all users
        elif len(self.element) == 4 and self.element[-1] == 'all':
            users = []
            usersfrombmc = await self.ipmicmd.get_users()
            for user in usersfrombmc:
                users.append(await self.ipmicmd.get_user(uid=user))
            await self.output.put(msg.UserCollection(users=users, name=self.node))
            return
        # Update user
        elif len(self.element) == 4:
            # ipmi users really are numbered slots, unlike redfish accounts, so
            # say that rather than letting the conversion fail as a surprise
            try:
                user = int(self.element[-1])
            except ValueError:
                raise pygexc.InvalidParameterValue(
                    'ipmi users are numbered, and "{0}" is not a user '
                    'number'.format(self.element[-1]))
            if self.op == 'read':
                data = await self.ipmicmd.get_user(uid=user)
                await self.output.put(msg.User(
                    uid=data['uid'],
                    username=data['name'],
                    privilege_level=data['access']['privilege_level'],
                    expiration=data['expiration'],
                    name=self.node))
                return
            elif self.op == 'update':
                user = self.inputdata.credentials[self.node]

                if 'username' in user:
                    await self.ipmicmd.set_user_name(uid=user['uid'],
                                               name=user['username'])

                if 'password' in user:
                    await self.ipmicmd.set_user_password(uid=user['uid'],
                                                   password=user['password'])
                    await self.ipmicmd.set_user_password(uid=user['uid'],
                                    mode='enable', password=user['password'])
                if 'privilege_level' in user:
                    await self.ipmicmd.set_user_access(uid=user['uid'],
                                                    privilege_level=user[
                                                        'privilege_level'])
                if 'enabled' in user:
                    if user['enabled'] == 'yes':
                        mode = 'enable'
                    else:
                        mode = 'disable'
                    await self.ipmicmd.disable_user(user['uid'], mode)
                return
            elif self.op == 'delete':
                await self.ipmicmd.user_delete(uid=user)
                return

    async def do_eventlog(self):
        eventout = []
        clear = False
        if self.op == 'delete':
            clear = True
        async for event in self.ipmicmd.get_event_log(clear):
            self.pyghmi_event_to_confluent(event)
            eventout.append(event)
        await self.output.put(msg.EventCollection(eventout, name=self.node))

    def pyghmi_event_to_confluent(self, event):
        # An ipmi platform keeps its events in one place and the spec has a
        # name for it, so say so rather than leaving the source blank and
        # having a caller asking for a named log get nothing back
        event.setdefault('log_id', 'SEL')
        event['severity'] = _str_health(event.get('severity', 'unknown'))
        if 'event_data' in event:
            event['event'] = '{0} - {1}'.format(
                event['event'], event['event_data'])
        if 'event_id' in event:
            event['id'] = '{0}.{1}'.format(event['event_id'],
                                           event['component_type_id'])

    async def make_inventory_map(self):
        invnames = self.ipmicmd.get_inventory_descriptions()
        async for name in invnames:
            self.invmap[simplify_name(name)] = name

    async def make_sensor_map(self, sensors=None):
        if sensors is None:
            sensors = self.ipmicmd.get_sensor_descriptions()
        async for sensor in sensors:
            resourcename = sensor['name']
            self.ipmicmd.sensormap[simplify_name(resourcename)] = resourcename

    async def read_normalized(self, sensorname):
        readings = None
        if sensorname == 'average_cpu_temp':
            cputemp = await self.ipmicmd.get_average_processor_temperature()
            readings = [cputemp]
        elif sensorname == 'inlet_temp':
            inltemp = await self.ipmicmd.get_inlet_temperature()
            readings = [inltemp]
        elif sensorname == 'total_power':
            sensor = EmptySensor('Total Power')
            sensor.states = []
            sensor.units = 'W'
            sensor.value = await self.ipmicmd.get_system_power_watts()
            readings = [sensor]
        if readings:
            await self.output.put(msg.SensorReadings(readings, name=self.node))

    async def read_sensors(self, sensorname):
        if sensorname == 'all':
            sensors = [x async for x in self.ipmicmd.get_sensor_descriptions()]
            readings = []
            for sensor in filter(self.match_sensor, sensors):
                try:
                    reading = await self.ipmicmd.get_sensor_reading(
                        sensor['name'])
                except pygexc.IpmiException as ie:
                    if ie.ipmicode == 203:
                        await self.output.put(msg.SensorReadings([EmptySensor(
                            sensor['name'])], name=self.node))
                        continue
                    raise
                if hasattr(reading, 'health'):
                    reading.health = _str_health(reading.health)
                if hasattr(reading, 'unavailable') and reading.unavailable:
                    await self.output.put(msg.SensorReadings([EmptySensor(
                        reading.name)], name=self.node))
                    continue
                readings.append(reading)
            await self.output.put(msg.SensorReadings(readings, name=self.node))
        else:
            if sensorname not in self.ipmicmd.sensormap:
                await self.make_sensor_map()
            if sensorname not in self.ipmicmd.sensormap:
                await self.output.put(
                    msg.ConfluentTargetNotFound(self.node,
                                                'Sensor not found'))
                return
            try:
                reading = await self.ipmicmd.get_sensor_reading(
                    self.ipmicmd.sensormap[sensorname])
                if hasattr(reading, 'health'):
                    reading.health = _str_health(reading.health)
                if hasattr(reading, 'unavailable') and reading.unavailable:
                    await self.output.put(msg.SensorReadings([EmptySensor(
                        reading.name)], name=self.node))
                else:
                    await self.output.put(
                        msg.SensorReadings([reading],
                                           name=self.node))
            except pygexc.IpmiException as ie:
                if ie.ipmicode == 203:
                    await self.output.put(msg.ConfluentResourceUnavailable(
                        self.node, 'Unavailable'
                    ))
                else:
                    await self.output.put(msg.ConfluentTargetTimeout(self.node))

    async def list_inventory(self):
        components = []
        try:
            async for component in self.ipmicmd.get_inventory_descriptions():
                components.append(component)
        except pygexc.IpmiException:
            await self.output.put(msg.ConfluentTargetTimeout(self.node))
            return
        await self.output.put(msg.ChildCollection('all'))
        for component in components:
            await self.output.put(msg.ChildCollection(simplify_name(component)))

    async def list_firmware(self):
        await self.output.put(msg.ChildCollection('all'))
        async for id, data in self.ipmicmd.get_firmware():
            await self.output.put(msg.ChildCollection(simplify_name(id)))

    async def read_firmware(self, component, category):
        items = []
        errorneeded = False
        try:
            complist = () if component == 'all' else (component,)
            async for id, data in self.ipmicmd.get_firmware(complist, category):
                if (component in ('core', 'all') or
                        component == simplify_name(id) or
                        match_aliases(component, simplify_name(id))):
                    items.append({id: data})
        except ssl.SSLEOFError:
            errorneeded = msg.ConfluentNodeError(
                self.node, 'Unable to communicate with the https server on '
                           'the target BMC while trying to read extended '
                           'information')
        except exc.PubkeyInvalid:
            errorneeded = msg.ConfluentNodeError(
                self.node,
                'Extended information unavailable, mismatch detected between '
                'target certificate fingerprint and '
                'pubkeys.tls_hardwaremanager attribute')
        except pygexc.TemporaryError as e:
                errorneeded = msg.ConfluentNodeError(
                self.node, str(e))
        await self.output.put(msg.Firmware(items, self.node))
        if errorneeded:
            await self.output.put(errorneeded)

    async def handle_update_status(self):
        activeupdates = list(firmwaremanager.list_updates([self.node], None, []))
        if activeupdates:
            await self.output.put(msg.KeyValueData({'status': 'active'}, self.node))
        else:
            status = await self.ipmicmd.get_update_status()
            await self.output.put(msg.KeyValueData({'status': status}, self.node))

    async def handle_inventory(self):
        if self.element[1] == 'firmware':
            if len(self.element) == 3:
                return await self.list_firmware()
            elif len(self.element) == 4:
                return await self.read_firmware(self.element[-1], self.element[-2])
        elif self.element[1] == 'hardware':
            if len(self.element) == 3:  # list things in inventory
                return await self.list_inventory()
            elif len(self.element) == 4:  # actually read inventory data
                return await self.read_inventory(self.element[-1])
        raise Exception('Unsupported scenario...')

    async def list_leds(self):
        await self.output.put(msg.ChildCollection('all'))
        async for category, info in self.ipmicmd.get_leds():
            await self.output.put(msg.ChildCollection(simplify_name(category)))

    async def read_leds(self, component):
        led_categories = []
        async for category, info in self.ipmicmd.get_leds():
            if component == 'all' or component == simplify_name(category):
                led_categories.append({category: info})
        await self.output.put(msg.LEDStatus(led_categories, self.node))

    async def read_inventory(self, component):
        errorneeded = False
        try:
            invitems = []
            if component == 'all':
                async for invdata in self.ipmicmd.get_inventory():
                    if invdata[1] is None:
                        newinf = {'present': False, 'information': None,
                                  'name': invdata[0]}

                    else:
                        sanitize_invdata(invdata[1])
                        newinf = {'present': True, 'information': invdata[1]}
                        newinf['name'] = invdata[1].get('name', invdata[0])
                    self.add_invitem(invitems, newinf)
            else:
                await self.make_inventory_map()
                compname = self.invmap.get(component, None)
                if compname is None:
                    await self.output.put(msg.ConfluentTargetNotFound(
                        self.node,
                        "No component named '{0}' found".format(component)))
                    return
                invdata = await self.ipmicmd.get_inventory_of_component(compname)
                if invdata is None:
                    newinf = {'present': False, 'information': None,
                              'name': compname}
                else:
                    sanitize_invdata(invdata)
                    newinf = {'present': True, 'information': invdata,
                              'name': invdata.get('name', compname)}
                self.add_invitem(invitems, newinf)
        except ssl.SSLEOFError:
            errorneeded = msg.ConfluentNodeError(
                self.node, 'Unable to communicate with the https server on '
                           'the target BMC while trying to read extended '
                           'information')
        except exc.PubkeyInvalid:
            errorneeded = msg.ConfluentNodeError(
                self.node,
                'Extended information unavailable, mismatch detected between '
                'target certificate fingerprint and '
                'pubkeys.tls_hardwaremanager attribute')
        newinvdata = {'inventory': invitems}
        await self.output.put(msg.KeyValueData(newinvdata, self.node))
        if errorneeded:
            await self.output.put(errorneeded)

    def add_invitem(self, invitems, newinf):
        if newinf.get('information', None) and 'name' in newinf['information']:
            newinf = copy.deepcopy(newinf)
            del newinf['information']['name']
        if (fnmatch(newinf['name'], 'Adapter ??:??:??') or fnmatch(
                newinf['name'], 'PCIeGen? x*') or not newinf['name']):
            myinf = newinf.get('information', {})
            sdid = myinf.get('PCI Subsystem Device ID', None)
            svid = myinf.get('PCI Subsystem Vendor ID', None)
            did = myinf.get('PCI Device ID', None)
            vid = myinf.get('PCI Vendor ID', None)
            vstr, dstr = get_pci_text_from_ids(sdid, svid, did, vid)
            if vstr:
                newinf['information']['PCI Vendor'] = vstr
            if dstr:
                newinf['name'] = dstr
        invitems.append(newinf)

    async def handle_storage(self):
        if self.element[-1] == '':
            self.element = self.element[:-1]
        storelem = self.element[2:]
        if 'read' == self.op:
            return await self._show_storage(storelem)
        elif 'update' == self.realop:
            return await self._update_storage(storelem)
        elif 'delete' == self.op:
            return await self._delete_storage(storelem)
        elif 'create' == self.realop:
            return await self._create_storage(storelem)

    async def _delete_storage(self, storelem):
        if len(storelem) < 2:
            storelem.append('')
        if len(storelem) < 2 or storelem[0] != 'volumes':
            raise exc.InvalidArgumentException('Must target a specific volume')
        volname = storelem[-1]
        curr = await self.ipmicmd.get_storage_configuration()
        volumes = []
        volsfound = False
        toremove = storage.ConfigSpec(arrays=[storage.Array(volumes=volumes)])
        for pool in curr.arrays:
            for vol in pool.volumes:
                if simplify_name(vol.name) == volname:
                    volsfound = True
                    volumes.append(vol)
        if not volsfound:
            await self.output.put(msg.ConfluentTargetNotFound(
                self.node, "No volume named '{0}' found".format(volname)))
            return
        await self.ipmicmd.remove_storage_configuration(toremove)
        await self.output.put(msg.DeletedResource(volname))

    async def _create_storage(self, storelem):
        if 'volumes' not in storelem:
            raise exc.InvalidArgumentException('Can only create volumes')
        vols = []
        thedisks = None
        currcfg = await self.ipmicmd.get_storage_configuration()
        currnames = []
        for arr in currcfg.arrays:
            arrname = '{0}-{1}'.format(*arr.id)
            for vol in arr.volumes:
                currnames.append(vol.name)
        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'], stripsize=vol['stripsize']))
        newcfg = storage.ConfigSpec(
            arrays=(storage.Array(raid=raidlvl, disks=disks, volumes=vols),))
        await self.ipmicmd.apply_storage_configuration(newcfg)
        for vol in self.inputdata.inputbynode[self.node]:
            if vol['name'] is None:
                newcfg = await self.ipmicmd.get_storage_configuration()
                for arr in newcfg.arrays:
                    arrname = '{0}-{1}'.format(*arr.id)
                    for vol in arr.volumes:
                        if vol.name not in currnames:
                            await self.output.put(
                                msg.Volume(self.node, vol.name, vol.size,
                                           vol.status, arrname))
                return
            else:
                await self._show_storage(storelem[:1] + [vol['name']])

    async def _update_storage(self, storelem):
        if storelem[0] == 'disks':
            if len(storelem) == 1:
                raise exc.InvalidArgumentException('Must target a disk')
            await self.set_disk(storelem[-1],
                          self.inputdata.inputbynode[self.node])
        await self._show_storage(storelem)

    async def _show_storage(self, storelem):
        if storelem[0] == 'disks':
            if len(storelem) == 1:
                return await self.list_disks()
            return await self.show_disk(storelem[1])
        elif storelem[0] == 'arrays':
            if len(storelem) == 1:
                return await self.list_arrays()
            return await self.show_array(storelem[1])
        elif storelem[0] == 'volumes':
            if len(storelem) == 1:
                return await self.list_volumes()
            return await self.show_volume(storelem[1])
        elif storelem[0] == 'all':
            return await self._show_all_storage()


    async def handle_sensors(self):
        if self.element[-1] == '':
            self.element = self.element[:-1]
        if len(self.element) < 3:
            return
        self.sensorcategory = self.element[2]
        if self.sensorcategory == 'normalized':
            return await self.read_normalized(self.element[-1])
        # list sensors per category
        if len(self.element) == 3 and self.element[-2] == 'hardware':
            if self.sensorcategory == 'leds':
                return await self.list_leds()
            return await self.list_sensors()
        elif len(self.element) == 4:  # resource requested
            if self.sensorcategory == 'leds':
                return await self.read_leds(self.element[-1])
            return await self.read_sensors(self.element[-1])

    def match_sensor(self, sensor):
        if self.sensorcategory == 'all':
            return True
        if sensor['type'] in sensor_categories[self.sensorcategory]:
            return True
        return False

    async def set_disk(self, name, state):
        scfg = await 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
        await self.ipmicmd.apply_storage_configuration(
            storage.ConfigSpec(disks=scfg.disks))

    async def _show_all_storage(self):
        scfg = await self.ipmicmd.get_storage_configuration()
        for disk in scfg.disks:
            await self.output.put(
                msg.Disk(self.node, disk.name, disk.description,
                         disk.id, disk.status, disk.serial,
                         disk.fru))
        for arr in scfg.arrays:
            for disk in arr.disks:
                await self.output.put(
                    msg.Disk(self.node, disk.name, disk.description,
                             disk.id, disk.status, disk.serial,
                             disk.fru, array='{0}-{1}'.format(*arr.id)))
            for disk in arr.hotspares:
                await self.output.put(
                    msg.Disk(self.node, disk.name, disk.description,
                             disk.id, disk.status, disk.serial,
                             disk.fru, array='{0}-{1}'.format(*arr.id)))
        for arr in scfg.arrays:
            arrname = '{0}-{1}'.format(*arr.id)
            await self._detail_array(arr, arrname, True)

    async def show_disk(self, name):
        scfg = await self.ipmicmd.get_storage_configuration()
        for disk in scfg.disks:
            if simplify_name(disk.name) == name or disk == name:
                await self.output.put(
                    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):
                    await self.output.put(
                        msg.Disk(self.node, disk.name, disk.description,
                                 disk.id, disk.status, disk.serial,
                                 disk.fru, arrname))
            for disk in arr.hotspares:
                if (name == 'all' or simplify_name(disk.name) == name or
                        disk == name):
                    await self.output.put(
                        msg.Disk(self.node, disk.name, disk.description,
                                 disk.id, disk.status, disk.serial,
                                 disk.fru, arrname))

    async def list_disks(self):
        scfg = await self.ipmicmd.get_storage_configuration()
        for disk in scfg.disks:
            await self.output.put(msg.ChildCollection(simplify_name(disk.name)))
        for arr in scfg.arrays:
            for disk in arr.disks:
                await self.output.put(msg.ChildCollection(simplify_name(disk.name)))
            for disk in arr.hotspares:
                await self.output.put(msg.ChildCollection(simplify_name(disk.name)))

    async def list_arrays(self):
        scfg = await self.ipmicmd.get_storage_configuration()
        for arr in scfg.arrays:
            await self.output.put(msg.ChildCollection('{0}-{1}'.format(*arr.id)))

    async def show_array(self, name):
        scfg = await self.ipmicmd.get_storage_configuration()
        for arr in scfg.arrays:
            arrname = '{0}-{1}'.format(*arr.id)
            if arrname == name:
                await self._detail_array(arr, arrname)

    async def _detail_array(self, arr, arrname, detailvol=False):
        vols = []
        for vol in arr.volumes:
            vols.append(simplify_name(vol.name))
        disks = []
        for disk in arr.disks:
            disks.append(simplify_name(disk.name))
        for disk in arr.hotspares:
            disks.append(simplify_name(disk.name))
        await self.output.put(msg.Array(self.node, disks, arr.raid,
                                  vols, arrname, arr.capacity,
                                  arr.available_capacity))
        if detailvol:
            for vol in arr.volumes:
                await self.output.put(msg.Volume(self.node, vol.name, vol.size,
                                           vol.status, arrname))

    async def show_volume(self, name):
        scfg = await 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):
                    await self.output.put(msg.Volume(self.node, vol.name, vol.size,
                                               vol.status, arrname))

    async def list_volumes(self):
        scfg = await self.ipmicmd.get_storage_configuration()
        for arr in scfg.arrays:
            for vol in arr.volumes:
                await self.output.put(msg.ChildCollection(simplify_name(vol.name)))

    async def list_sensors(self):
        try:
            sensors = [sensor async for sensor in self.ipmicmd.get_sensor_descriptions()]
        except pygexc.IpmiException:
            await self.output.put(msg.ConfluentTargetTimeout(self.node))
            return
        await self.output.put(msg.ChildCollection('all'))
        for sensor in filter(self.match_sensor, sensors):
            await self.output.put(msg.ChildCollection(simplify_name(sensor['name'])))

    async def health(self):
        if 'read' == self.op:
            try:
                response = await self.ipmicmd.get_health()
            except pygexc.IpmiException:
                await self.output.put(msg.ConfluentTargetTimeout(self.node))
                return
            health = response['health']
            health = _str_health(health)
            await self.output.put(msg.HealthSummary(health, self.node))
            if 'badreadings' in response:
                badsensors = []
                for reading in response['badreadings']:
                    if hasattr(reading, 'health'):
                        reading.health = _str_health(reading.health)
                    badsensors.append(reading)
                await self.output.put(msg.SensorReadings(badsensors, name=self.node))
        else:
            raise exc.InvalidArgumentException('health is read-only')

    async def reseat_bay(self):
        bay = self.inputdata.inputbynode[self.node]
        try:
            await self.ipmicmd.reseat_bay(bay)
            await self.output.put(msg.ReseatResult(self.node, 'success'))
        except pygexc.UnsupportedFunctionality as uf:
            await self.output.put(uf)

    async def bootdevice(self):
        if 'read' == self.op:
            bootdev = await self.ipmicmd.get_bootdev()
            if bootdev['bootdev'] in self.bootdevices:
                bootdev['bootdev'] = self.bootdevices[bootdev['bootdev']]
            bootmode = 'unspecified'
            if 'uefimode' in bootdev:
                if bootdev['uefimode']:
                    bootmode = 'uefi'
                else:
                    bootmode = 'bios'
            persistent = False
            if 'persistent' in bootdev:
                persistent = bootdev['persistent']
            await self.output.put(msg.BootDevice(node=self.node,
                                           device=bootdev['bootdev'],
                                           bootmode=bootmode,
                                           persistent=persistent))
            return
        elif 'update' == self.op:
            bootdev = self.inputdata.bootdevice(self.node)
            douefi = False
            if self.inputdata.bootmode(self.node) == 'uefi':
                douefi = True
            persistent = self.inputdata.persistent(self.node)
            bootdev = await self.ipmicmd.set_bootdev(bootdev, uefiboot=douefi,
                                               persist=persistent)
            if bootdev['bootdev'] in self.bootdevices:
                bootdev['bootdev'] = self.bootdevices[bootdev['bootdev']]
            await self.output.put(msg.BootDevice(node=self.node,
                                           device=bootdev['bootdev']))

    async def identify(self):
        if 'update' == self.op:
            identifystate = self.inputdata.inputbynode[self.node] == 'on'
            blinkstate = self.inputdata.inputbynode[self.node] == 'blink'
            await self.ipmicmd.set_identify(on=identifystate, blink=blinkstate)
            await self.output.put(msg.IdentifyState(
                node=self.node, state=self.inputdata.inputbynode[self.node]))
            return
        elif 'read' == self.op:
            # IPMI can set the identify light but has no command to read it
            # back, so aiohmi offers no get_identify. This used to answer an
            # empty state, which nodeidentify prints as the node name and
            # nothing else, exit 0.
            await self.output.put(msg.ConfluentNodeError(
                self.node,
                'IPMI provides no way to read the identify state'))
            return

    async def power(self):
        if 'read' == self.op:
            power = await self.ipmicmd.get_power()
            await self.output.put(msg.PowerState(node=self.node,
                                           state=power['powerstate']))
            return
        elif 'update' == self.op:
            powerstate = self.inputdata.powerstate(self.node)
            oldpower = None
            waitamount = 30
            if powerstate == 'boot':
                oldpower = await self.ipmicmd.get_power()
                if 'powerstate' in oldpower:
                    oldpower = oldpower['powerstate']
            elif powerstate == 'shutdown':
                waitamount = True
            await self.ipmicmd.set_power(powerstate, wait=waitamount)
            if powerstate == 'boot' and oldpower == 'on':
                power = {'powerstate': 'reset'}
            else:
                power = await self.ipmicmd.get_power()
                if powerstate == 'reset' and power['powerstate'] == 'on':
                    power['powerstate'] = 'reset'

            await self.output.put(
                msg.PowerState(node=self.node,
                               state=power['powerstate'],
                               oldstate=oldpower))
            return

    async def handle_reset(self):
        if 'read' == self.op:
            await self.output.put(msg.BMCReset(node=self.node,
                                         state='reset'))
            return
        elif 'update' == self.op:
            await self.ipmicmd.reset_bmc()
            return

    async def handle_identifier(self):
        if 'read' == self.op:
            mci = await self.ipmicmd.get_mci()
            await self.output.put(msg.MCI(self.node, mci))
            return
        elif 'update' == self.op:
            mci = self.inputdata.mci(self.node)
            await self.ipmicmd.set_mci(mci)
            return

    async def handle_hostname(self):
        if 'read' == self.op:
            hostname = await self.ipmicmd.get_hostname()
            await self.output.put(msg.Hostname(self.node, hostname))
            return
        elif 'update' == self.op:
            hostname = self.inputdata.hostname(self.node)
            await self.ipmicmd.set_hostname(hostname)
            return

    async def handle_domain_name(self):
        if 'read' == self.op:
            dn = await self.ipmicmd.get_domain_name()
            await self.output.put(msg.DomainName(self.node, dn))
            return
        elif 'update' == self.op:
            dn = self.inputdata.domain_name(self.node)
            await self.ipmicmd.set_domain_name(dn)
            return

    async def handle_bmcconfigclear(self):
        if 'read' == self.op:
            raise exc.InvalidArgumentException(
                'Cannot read the "clear" resource')
        await self.ipmicmd.clear_bmc_configuration()

    async def handle_sysconfigclear(self):
        if 'read' == self.op:
            raise exc.InvalidArgumentException(
                'Cannot read the "clear" resource')
        await self.ipmicmd.clear_system_configuration()

    async def handle_bmcconfig(self, advanced=False, extended=False):
        if 'read' == self.op:
            try:
                if extended:
                    bmccfg = await self.ipmicmd.get_extended_bmc_configuration(
                        hideadvanced=(not advanced))
                else:
                    bmccfg = await self.ipmicmd.get_bmc_configuration()
                await self.output.put(msg.ConfigSet(self.node, bmccfg))
            except Exception as e:
                await self.output.put(
                    msg.ConfluentNodeError(self.node, str(e)))
        elif 'update' == self.op:
            await self.ipmicmd.set_bmc_configuration(
                self.inputdata.get_attributes(self.node))

    async def handle_sysconfig(self, advanced=False):
        if 'read' == self.op:
            try:
                syscfg = await self.ipmicmd.get_system_configuration(
                    hideadvanced=not advanced)
                await self.output.put(msg.ConfigSet(
                    self.node, syscfg))
            except Exception as e:
                await self.output.put(
                    msg.ConfluentNodeError(self.node, str(e)))
        elif 'update' == self.op:
            await self.ipmicmd.set_system_configuration(
                self.inputdata.get_attributes(self.node))

    async def handle_ntp(self):
        if self.element[3] == 'enabled':
            if 'read' == self.op:
                enabled = await self.ipmicmd.get_ntp_enabled()
                if enabled is None:
                    # None is how the platform says it cannot tell us, and
                    # reporting that as the text "None" tells the caller nothing
                    raise pygexc.UnsupportedFunctionality(
                        'NTP state is not reported by this platform')
                await self.output.put(msg.NTPEnabled(self.node, enabled))
                return
            elif 'update' == self.op:
                enabled = self.inputdata.ntp_enabled(self.node)
                await self.ipmicmd.set_ntp_enabled(enabled == 'True')
                return
        elif self.element[3] == 'servers':
            if len(self.element) == 4:
                await self.output.put(msg.ChildCollection('all'))
                size = len(await self.ipmicmd.get_ntp_servers())
                for idx in range(1, size + 1):
                    await self.output.put(msg.ChildCollection(idx))
            else:
                if 'read' == self.op:
                    if self.element[-1] == 'all':
                        servers = await self.ipmicmd.get_ntp_servers()
                        await self.output.put(msg.NTPServers(self.node, servers))
                        return
                    else:
                        idx = int(self.element[-1]) - 1
                        servers = await self.ipmicmd.get_ntp_servers()
                        if len(servers) > idx:
                            await self.output.put(msg.NTPServer(self.node, servers[idx]))
                        else:
                            await self.output.put(
                                msg.ConfluentTargetNotFound(
                                    self.node, 'Requested NTP configuration not found'))
                        return
                elif self.op in ('update', 'create'):
                    if self.element[-1] == 'all':
                        servers = self.inputdata.ntp_servers(self.node)
                        for idx in servers:
                            await self.ipmicmd.set_ntp_server(servers[idx],
                                                        int(idx[-1])-1)
                        return
                    else:
                        idx = int(self.element[-1]) - 1
                        server = self.inputdata.ntp_server(self.node)
                        await self.ipmicmd.set_ntp_server(server, idx)
                        return

    async def handle_license(self):
        available = await self.ipmicmd.get_remote_kvm_available()
        await self.output.put(msg.License(self.node, available))
        return

    async def save_licenses(self):
        directory = self.inputdata.nodefile(self.node)
        checkdir = directory
        if not os.access(directory, os.W_OK):
            raise exc.InvalidArgumentException(
                'The confluent system user/group is unable to write to '
                'directory {0}, check ownership and permissions'.format(
                    checkdir))
        async for saved in self.ipmicmd.save_licenses(directory):
            if self.current_user:
                try:
                    pwent = pwd.getpwnam(self.current_user)
                    os.chown(saved, pwent.pw_uid, pwent.pw_gid)
                except KeyError:
                    pass
            await self.output.put(msg.SavedFile(self.node, saved))

    async def handle_licenses(self):
        if self.element[-1] == '':
            self.element = self.element[:-1]
        if self.op in ('create', 'update'):
            filename = self.inputdata.nodefile(self.node)
            datfile = None
            if filename in self.cfm.clientfiles:
                cf = self.cfm.clientfiles[filename]
                datfile = os.fdopen(os.dup(cf.fileno()), cf.mode)
            if datfile is None and not os.access(filename, os.R_OK):
                errstr =  ('{0} is not readable by confluent on {1} '
                           '(ensure confluent user or group can access file '
                           'and parent directories)').format(
                               filename, socket.gethostname())
                await self.output.put(msg.ConfluentNodeError(self.node, errstr))
                return
            try:
                async for x in self.ipmicmd.apply_license(filename, data=datfile):
                    pass
            finally:
                if datfile is not None:
                    datfile.close()
        if len(self.element) == 3:
            await self.output.put(msg.ChildCollection('all'))
            i = 1
            async for lic in self.ipmicmd.get_licenses():
                await self.output.put(msg.ChildCollection(str(i)))
                i += 1
            return
        licname = self.element[3]
        if licname == 'all':
            async for lic in self.ipmicmd.get_licenses():
                if self.op == 'delete':
                    await self.ipmicmd.delete_license(lic['name'])
                else:
                    await self.output.put(msg.License(self.node, feature=lic['name'], state=lic.get('state', 'Active')))
        else:
            index = int(licname)
            lic = list([x async for x in self.ipmicmd.get_licenses()])[index - 1]
            if self.op == 'delete':
                await self.ipmicmd.delete_license(lic['name'])
            else:
                await self.output.put(msg.License(self.node, feature=lic['name'], state=lic.get('state', 'Active')))

    async def handle_description(self):
        dsc = await self.ipmicmd.get_description()
        await self.output.put(msg.KeyValueData(dsc, self.node))

    async def handle_ikvm_methods(self):
        dsc = await self.ipmicmd.get_ikvm_methods()
        dsc = {'ikvm_methods': dsc}
        await self.output.put(msg.KeyValueData(dsc, self.node))
    
    async def handle_ikvm_screenshot(self):
        # good background for the webui, and kitty
        imgdata = RetainedIO()
        imgformat = await self.ipmicmd.get_screenshot(imgdata)
        imgdata = imgdata.resultbuffer
        if imgdata:
            await self.output.put(msg.ScreenShot(imgdata, self.node, imgformat=imgformat))

    async def handle_ikvm(self):
        methods = await self.ipmicmd.get_ikvm_methods()
        if 'openbmc' in methods:
            url = await vinzmanager.get_url(self.node, self.inputdata)
            await self.output.put(msg.ChildCollection(url))
            return
        launchdata = await self.ipmicmd.get_ikvm_launchdata()
        if 'url' in launchdata and not launchdata['url'].startswith('https://'):
            mybmc = self.ipmicmd.confluentbmcname
            if mybmc.startswith('fe80::'):  # link local, need to adjust
                lancfg = await self.ipmicmd.get_net_configuration()
                mybmc = lancfg['ipv4_address'].split('/')[0]
            if ':' in mybmc and '[' not in mybmc:
                mybmc = '[{}]'.format(mybmc)
            launchdata['url'] = 'https://{}{}'.format(mybmc, launchdata['url'])
        await self.output.put(msg.KeyValueData(launchdata, self.node))


    async def handle_graphical_console(self):
        args = await self.ipmicmd.get_graphical_console()
        m = msg.GraphicalConsole(self.node, *args)
        await self.output.put(m)
        return


def _str_health(health):
    if isinstance(health, str):
        return health
    if pygconstants.Health.Failed & health:
        health = 'failed'
    elif pygconstants.Health.Critical & health:
        health = 'critical'
    elif pygconstants.Health.Warning & health:
        health = 'warning'
    else:
        health = 'ok'
    return health


def initthread():
    global _ipmithread
    if _ipmithread is None:
        _ipmithread = asyncio.create_task(_ipmi_evtloop())


async def create(nodes, element, configmanager, inputdata, realop='create'):
    initthread()
    if element == ['_console', 'session']:
        if len(nodes) > 1:
            raise Exception("_console/session does not support multiple nodes")
        yield IpmiConsole(nodes[0], configmanager)
        return
    else:
        async for ret in perform_requests(
                'update', nodes, element, configmanager, inputdata, realop):
            yield ret


def update(nodes, element, configmanager, inputdata):
    initthread()
    return create(nodes, element, configmanager, inputdata, 'update')


async def retrieve(nodes, element, configmanager, inputdata):
    initthread()
    if '/'.join(element).startswith('inventory/firmware/updates/active'):
        for ret in firmwaremanager.list_updates(nodes, configmanager.tenant,
                                            element):
            yield ret
    elif '/'.join(element).startswith('media/uploads'):
        for ret in firmwaremanager.list_updates(nodes, configmanager.tenant,
                                            element, 'mediaupload'):
            yield ret
    elif '/'.join(element).startswith('support/servicedata'):
        for ret in firmwaremanager.list_updates(nodes, configmanager.tenant,
                                            element, 'ffdc'):
            yield ret
    else:
        async for ret in perform_requests('read', nodes, element, configmanager,
                                          inputdata, 'read'):
            yield ret

def delete(nodes, element, configmanager, inputdata):
    initthread()
    if '/'.join(element).startswith('inventory/firmware/updates/active'):
        return firmwaremanager.remove_updates(nodes, configmanager.tenant,
                                              element)
    elif '/'.join(element).startswith('media/uploads'):
        return firmwaremanager.remove_updates(nodes, configmanager.tenant,
                                              element, type='mediaupload')
    elif '/'.join(element).startswith('support/servicedata'):
        return firmwaremanager.remove_updates(nodes, configmanager.tenant,
                                              element, type='ffdc')
    return perform_requests(
        'delete', nodes, element, configmanager, inputdata, 'delete')
