diff --git a/confluent_client/bin/confetty b/confluent_client/bin/confetty index 2e65054c..8cbdaa29 100755 --- a/confluent_client/bin/confetty +++ b/confluent_client/bin/confetty @@ -46,7 +46,6 @@ import math import getpass import optparse import os -import readline import select import shlex import socket @@ -601,9 +600,11 @@ except socket.gaierror: # sys.stdout.write('\x1b[H\x1b[J') # sys.stdout.flush() -readline.parse_and_bind("tab: complete") -readline.parse_and_bind("set bell-style none") -readline.set_completer(completer) +if sys.stdout.isatty(): + import readline + readline.parse_and_bind("tab: complete") + readline.parse_and_bind("set bell-style none") + readline.set_completer(completer) doexit = False inconsole = False pendingcommand = "" diff --git a/confluent_server/confluent/alerts.py b/confluent_server/confluent/alerts.py new file mode 100644 index 00000000..81328470 --- /dev/null +++ b/confluent_server/confluent/alerts.py @@ -0,0 +1,59 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2015 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. + +# This handles incoming unsolicited alerts over the network. For the moment +# we'll link into ipmi.py to do PET alerts, with the assumption that more +# typical .mib based handling will be used for other events and confluent's +# event service is to handle the peculiarities of an IPMI PET. In the future +# it may make sense to extend this in a more general case, but that rework can +# be deferred for now. + +# Phase 1 is to be facilitating some application doing http calls to get help +# decoding data. + +# Phase 2 is to be able to bind a port and have snmptrapd just forward the +# packet rather than have something block snmptrapd at all for things confluent +# can handle. + +__author__ = 'jjohnson2' + +import confluent.exceptions as exc +import confluent.lookuptools as lookuptools +import confluent.core + +def decode_alert(varbinds, configmanager): + """Decode an SNMP alert for a server + + Given the agentaddr, OID for the trap, and a dict of varbinds, + ascertain the node identity and then request a decode + + :param varbinds: A dictionary of OID to value varbinds. Also supported + are special keywords 'enterprise' and 'specificTrap' for + SNMPv1 traps. + + """ + try: + agentaddr = varbinds['.1.3.6.1.6.3.18.1.3.0'] + except KeyError: + agentaddr = varbinds['1.3.6.1.6.3.18.1.3.0'] + node = lookuptools.node_by_manager(agentaddr) + if node is None: + raise exc.InvalidArgumentException( + 'Unable to find a node with specified manager') + return confluent.core.handle_path( + '/nodes/{0}/events/hardware/decode'.format(node), 'update', + configmanager, varbinds, autostrip=False) + diff --git a/confluent_server/confluent/core.py b/confluent_server/confluent/core.py index 28c5294a..f8ff23e2 100644 --- a/confluent_server/confluent/core.py +++ b/confluent_server/confluent/core.py @@ -33,6 +33,7 @@ # functions. Console is special and just get's passed through # see API.txt +import confluent.alerts as alerts import confluent.config.attributes as attrscheme import confluent.interface.console as console import confluent.exceptions as exc @@ -93,7 +94,7 @@ def load_plugins(): pluginmap[plugin] = tmpmod -rootcollections = ['noderange/', 'nodes/', 'nodegroups/', 'users/'] +rootcollections = ['noderange/', 'nodes/', 'nodegroups/', 'users/', 'events/'] class PluginRoute(object): @@ -142,7 +143,11 @@ noderesources = { 'log': PluginRoute({ 'pluginattrs': ['hardwaremanagement.method'], 'default': 'ipmi', - }) + }), + 'decode': PluginRoute({ + 'pluginattrs': ['hardwaremanagement.method'], + 'default': 'ipmi', + }), }, }, 'health': { @@ -386,7 +391,7 @@ def handle_nodegroup_request(configmanager, inputdata, def handle_node_request(configmanager, inputdata, operation, - pathcomponents): + pathcomponents, autostrip=True): iscollection = False routespec = None if pathcomponents[0] == 'noderange': @@ -491,7 +496,7 @@ def handle_node_request(configmanager, inputdata, operation, nodes=nodesbyhandler[hfunc], element=pathcomponents, configmanager=configmanager, inputdata=inputdata)) - if isnoderange: + if isnoderange or not autostrip: return itertools.chain(*passvalues) elif isinstance(passvalues[0], console.Console): return passvalues[0] @@ -499,7 +504,7 @@ def handle_node_request(configmanager, inputdata, operation, return stripnode(passvalues[0], nodes[0]) -def handle_path(path, operation, configmanager, inputdata=None): +def handle_path(path, operation, configmanager, inputdata=None, autostrip=True): """Given a full path request, return an object. The plugins should generally return some sort of iterator. @@ -514,7 +519,7 @@ def handle_path(path, operation, configmanager, inputdata=None): return enumerate_collections(rootcollections) elif pathcomponents[0] == 'noderange': return handle_node_request(configmanager, inputdata, operation, - pathcomponents) + pathcomponents, autostrip) elif pathcomponents[0] == 'nodegroups': return handle_nodegroup_request(configmanager, inputdata, pathcomponents, @@ -522,7 +527,7 @@ def handle_path(path, operation, configmanager, inputdata=None): elif pathcomponents[0] == 'nodes': # single node request of some sort return handle_node_request(configmanager, inputdata, - operation, pathcomponents) + operation, pathcomponents, autostrip) elif pathcomponents[0] == 'users': # TODO: when non-administrator accounts exist, # they must only be allowed to see their own user @@ -546,5 +551,16 @@ def handle_path(path, operation, configmanager, inputdata=None): pathcomponents, operation, inputdata) update_user(user, inputdata.attribs, configmanager) return show_user(user, configmanager) + elif pathcomponents[0] == 'events': + try: + element = pathcomponents[1] + except IndexError: + if operation != 'retrieve': + raise exc.InvalidArgumentException('Target is read-only') + return (msg.ChildCollection('decode'),) + if element != 'decode': + raise exc.NotFoundException() + if operation == 'update': + return alerts.decode_alert(inputdata, configmanager) else: raise exc.NotFoundException() diff --git a/confluent_server/confluent/lookuptools.py b/confluent_server/confluent/lookuptools.py new file mode 100644 index 00000000..6dbde2b2 --- /dev/null +++ b/confluent_server/confluent/lookuptools.py @@ -0,0 +1,74 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2015 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. + +# Utility library for interesting lookups of nodes. +# Examples: +# looking up a node by a hardwaremanagement.manager address +# looking up a node by uuid (actually pretty straightforward +# looking up a node by mac address +# These are generally in the context of coming in from some unstructured +# direction (alerts, PXE attempt) and for now will only look at the null +# tenant (all baremetal tenants that are expected to receive alert/pxe +# service should have a null tenant and a tenant entry that correlates) +__author__ = 'jjohnson2' + +import confluent.config.configmanager as configmanager +import itertools +import socket + +manager_to_nodemap = {} + +def node_by_manager(manager): + """Lookup a node by manager + + Search for a node according to a given network address. + Rather than do a simple equality, it uses getaddrinfo + to allow name or ip and different forms of ip. For + example, 'fe80::0001' will match 'fe80::01' and + '127.000.000.001' will match '127.0.0.1' + + :param manager: The ip or resolvable name of the manager + + :returns: The node name (if any) + """ + + manageraddresses = [] + for tmpaddr in socket.getaddrinfo(manager, None): + manageraddresses.append(tmpaddr[4][0]) + cfm = configmanager.ConfigManager(None) + if manager in manager_to_nodemap: + # We have a stored hint as to the most probably correct answer + # put that node at the head of the list in hopes of reducing + # iterations for a lookup in a large environment + # However we don't trust the answer either, since + # reconfiguration could have changed it and this mapping + # is not hooked into getting updates + check_nodes = itertools.chain( + (manager_to_nodemap[manager],), cfm.list_nodes()) + else: + check_nodes = cfm.list_nodes() + hmattribs = cfm.get_node_attributes(check_nodes, + ('hardwaremanagement.manager',)) + for node in hmattribs: + currhm = hmattribs[node]['hardwaremanagement.manager']['value'] + if currhm in manageraddresses: + manager_to_nodemap[manager] = node + return node + for curraddr in socket.getaddrinfo(currhm, None): + curraddr = curraddr[4][0] + if curraddr in manageraddresses: + manager_to_nodemap[manager] = node + return node \ No newline at end of file diff --git a/confluent_server/confluent/messages.py b/confluent_server/confluent/messages.py index eddf795b..1483120a 100644 --- a/confluent_server/confluent/messages.py +++ b/confluent_server/confluent/messages.py @@ -320,10 +320,34 @@ def get_input_message(path, operation, inputdata, nodes=None, multinode=False): return InputAlertDestination(path, nodes, inputdata, multinode) elif path == ['identify'] and operation != 'retrieve': return InputIdentifyMessage(path, nodes, inputdata) + elif path == ['events', 'hardware', 'decode']: + return InputAlertData(path, inputdata, nodes) elif inputdata: raise exc.InvalidArgumentException() +class InputAlertData(ConfluentMessage): + + def __init__(self, path, inputdata, nodes=None): + self.alertparams = inputdata + # first migrate snmpv1 input to snmpv2 format + if 'specifictrap' in self.alertparams: + # If we have a 'specifictrap', convert to SNMPv2 per RFC 2576 + # This way + enterprise = self.alertparams['enterprise'] + specifictrap = self.alertparams['specifictrap'] + self.alertparams['.1.3.6.1.6.3.1.1.4.1.0'] = enterprise + '.0.' + \ + str(specifictrap) + if '1.3.6.1.6.3.1.1.4.1.0' in self.alertparams: + self.alertparams['.1.3.6.1.6.3.1.1.4.1.0'] = \ + self.alertparams['1.3.6.1.6.3.1.1.4.1.0'] + if '.1.3.6.1.6.3.1.1.4.1.0' not in self.alertparams: + raise exc.InvalidArgumentException('Missing SNMP Trap OID') + + def get_alert(self, node=None): + return self.alertparams + + class InputAttributes(ConfluentMessage): def __init__(self, path, inputdata, nodes=None): self.nodeattribs = {} diff --git a/confluent_server/confluent/plugins/hardwaremanagement/ipmi.py b/confluent_server/confluent/plugins/hardwaremanagement/ipmi.py index 4f6dff04..cdb02153 100644 --- a/confluent_server/confluent/plugins/hardwaremanagement/ipmi.py +++ b/confluent_server/confluent/plugins/hardwaremanagement/ipmi.py @@ -48,6 +48,14 @@ sensor_categories = { 'fans': frozenset(['Fan', 'Cooling Device']), } +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 xrange(0, len(hexstring), 2)] + bytedata = [int(i, 16) for i in hexvals] + return bytearray(bytedata) def simplify_name(name): return name.lower().replace(' ', '_') @@ -355,6 +363,8 @@ class IpmiHandler(object): self.handle_inventory() elif self.element == ['events', 'hardware', 'log']: self.do_eventlog() + elif self.element == ['events', 'hardware', 'decode']: + self.decode_alert() else: raise Exception('Not Implemented') @@ -363,6 +373,18 @@ class IpmiHandler(object): return self.handle_alerts() raise Exception('Not implemented') + 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 = self.ipmicmd.decode_pet(specifictrap, varbinddata) + self.pyghmi_event_to_confluent(event) + self.output.put(msg.EventCollection((event,), name=self.node)) + def handle_alerts(self): if self.element[3] == 'destinations': if len(self.element) == 4: @@ -399,20 +421,22 @@ class IpmiHandler(object): return raise Exception('Not implemented') - def do_eventlog(self): eventout = [] for event in self.ipmicmd.get_event_log(): - 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']) + self.pyghmi_event_to_confluent(event) eventout.append(event) self.output.put(msg.EventCollection(eventout, name=self.node)) + def pyghmi_event_to_confluent(self, event): + 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']) + def make_inventory_map(self): invnames = self.ipmicmd.get_inventory_descriptions() for name in invnames: @@ -637,4 +661,4 @@ def update(nodes, element, configmanager, inputdata): def retrieve(nodes, element, configmanager, inputdata): initthread() - return perform_requests('read', nodes, element, configmanager, inputdata) \ No newline at end of file + return perform_requests('read', nodes, element, configmanager, inputdata)