mirror of
				https://github.com/xcat2/confluent.git
				synced 2025-10-31 11:22:28 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			1319 lines
		
	
	
		
			52 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			1319 lines
		
	
	
		
			52 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| # Copyright 2016-2017 Lenovo
 | |
| #
 | |
| # Licensed under the Apache License, Version 2.0 (the "License");
 | |
| # you may not use this file except in compliance with the License.
 | |
| # You may obtain a copy of the License at
 | |
| #
 | |
| #     http://www.apache.org/licenses/LICENSE-2.0
 | |
| #
 | |
| # Unless required by applicable law or agreed to in writing, software
 | |
| # distributed under the License is distributed on an "AS IS" BASIS,
 | |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | |
| # See the License for the specific language governing permissions and
 | |
| # limitations under the License.
 | |
| 
 | |
| # This manages the detection and auto-configuration of nodes.
 | |
| # Discovery sources may implement scans and may be passive or may provide
 | |
| # both.
 | |
| 
 | |
| # The phases and actions:
 | |
| # - Detect - Notice the existance of a potentially supported target
 | |
| #        - Potentially apply a secure replacement for default credential
 | |
| #           (perhaps using some key identifier combined with some string
 | |
| #            denoting temporary use, and use confluent master integrity key
 | |
| #            to generate a password in a formulaic way?)
 | |
| #       - Do some universal reconfiguration if applicable (e.g. if something is
 | |
| #         part of an enclosure with an optionally enabled enclosure manager,
 | |
| #         check and request enclosure manager enablement
 | |
| #       - Throughout all of this, at this phase no sensitive data is divulged,
 | |
| #         only using credentials that are factory default or equivalent to
 | |
| #         factory default
 | |
| #       - Request transition to Locate
 | |
| # - Locate - Use available cues to ascertain the physical location.  This may
 | |
| #         be mac address lookup through switch or correlated by a server
 | |
| #         enclosure manager.  If the location data suggests a node identity,
 | |
| #         then proceed to the 'verify' state
 | |
| # - Verify - Given the current information and candidate upstream verifier,
 | |
| #            verify the authenticity of the servers claim in an automated way
 | |
| #            if possible.  A few things may happen at this juncture
 | |
| #               - Verification outright fails (confirmed negative response)
 | |
| #                    - Audit log entry created, element is not *allowed* to
 | |
| #                      proceed
 | |
| #               - Verification not possible (neither good or bad)
 | |
| #                   - If security policy is set to low, proceed to 'Manage'
 | |
| #                   - Otherwise, log the detection event and stop (user
 | |
| #                     would then manually bless the endpoint if applicable
 | |
| #               - Verification succeeds
 | |
| #                   - If security policy is set to strict (or manual, whichever
 | |
| #                     word works best, note the successfull verification, but
 | |
| #                     do not manage
 | |
| #                   - Otherwise, proceed to 'Manage'
 | |
| #  -Pre-configure - Given data up to this point, try to do some pre-config.
 | |
| #                   For example, if located and X, then check for S, enable S
 | |
| #                   This happens regardless of verify, as verify may depend on
 | |
| #                   S
 | |
| #  - Manage
 | |
| #     - Create the node if autonode (Deferred)
 | |
| #     - If there is not a defined ip address, collect the current LLA and use
 | |
| #       that value.
 | |
| #     - If no username/password defined, generate a unique password, 20 bytes
 | |
| #       long, written to pass most complexity rules (15 random bytes, base64,
 | |
| #       retry until uppercase, lowercase, digit, and symbol all present)
 | |
| #     - Apply defined configuration to endpoint
 | |
| 
 | |
| import base64
 | |
| import confluent.config.configmanager as cfm
 | |
| import confluent.collective.manager as collective
 | |
| import confluent.discovery.protocols.pxe as pxe
 | |
| import confluent.discovery.protocols.ssdp as ssdp
 | |
| import confluent.discovery.protocols.slp as slp
 | |
| import confluent.discovery.handlers.imm as imm
 | |
| import confluent.discovery.handlers.cpstorage as cpstorage
 | |
| import confluent.discovery.handlers.tsm as tsm
 | |
| import confluent.discovery.handlers.pxe as pxeh
 | |
| import confluent.discovery.handlers.smm as smm
 | |
| import confluent.discovery.handlers.xcc as xcc
 | |
| import confluent.exceptions as exc
 | |
| import confluent.log as log
 | |
| import confluent.messages as msg
 | |
| import confluent.networking.macmap as macmap
 | |
| import confluent.noderange as noderange
 | |
| import confluent.util as util
 | |
| import eventlet
 | |
| import traceback
 | |
| import socket as nsocket
 | |
| webclient = eventlet.import_patched('pyghmi.util.webclient')
 | |
| 
 | |
| 
 | |
| import eventlet
 | |
| import eventlet.greenpool
 | |
| import eventlet.semaphore
 | |
| 
 | |
| autosensors = set()
 | |
| scanner = None
 | |
| 
 | |
| try:
 | |
|     unicode
 | |
| except NameError:
 | |
|     unicode = str
 | |
| 
 | |
| class nesteddict(dict):
 | |
| 
 | |
|     def __missing__(self, key):
 | |
|         v = self[key] = nesteddict()
 | |
|         return v
 | |
| 
 | |
| nodehandlers = {
 | |
|     'service:lenovo-smm': smm,
 | |
|     'service:lenovo-smm2': smm,
 | |
|     'service:management-hardware.Lenovo:lenovo-xclarity-controller': xcc,
 | |
|     'service:management-hardware.IBM:integrated-management-module2': imm,
 | |
|     'pxe-client': pxeh,
 | |
|     'onie-switch': None,
 | |
|     'cumulus-switch': None,
 | |
|     'service:io-device.Lenovo:management-module': None,
 | |
|     'service:thinkagile-storage': cpstorage,
 | |
|     'service:lenovo-tsm': tsm,
 | |
| }
 | |
| 
 | |
| servicenames = {
 | |
|     'pxe-client': 'pxe-client',
 | |
|     'onie-switch': 'onie-switch',
 | |
|     'cumulus-switch': 'cumulus-switch',
 | |
|     'service:lenovo-smm': 'lenovo-smm',
 | |
|     'service:lenovo-smm2': 'lenovo-smm2',
 | |
|     'service:management-hardware.Lenovo:lenovo-xclarity-controller': 'lenovo-xcc',
 | |
|     'service:management-hardware.IBM:integrated-management-module2': 'lenovo-imm2',
 | |
|     'service:io-device.Lenovo:management-module': 'lenovo-switch',
 | |
|     'service:thinkagile-storage': 'thinkagile-storagebmc',
 | |
|     'service:lenovo-tsm': 'lenovo-tsm',
 | |
| }
 | |
| 
 | |
| servicebyname = {
 | |
|     'pxe-client': 'pxe-client',
 | |
|     'onie-switch': 'onie-switch',
 | |
|     'cumulus-switch': 'cumulus-switch',
 | |
|     'lenovo-smm': 'service:lenovo-smm',
 | |
|     'lenovo-smm2': 'service:lenovo-smm2',
 | |
|     'lenovo-xcc': 'service:management-hardware.Lenovo:lenovo-xclarity-controller',
 | |
|     'lenovo-imm2': 'service:management-hardware.IBM:integrated-management-module2',
 | |
|     'lenovo-switch': 'service:io-device.Lenovo:management-module',
 | |
|     'thinkagile-storage': 'service:thinkagile-storagebmc',
 | |
|     'lenovo-tsm': 'service:lenovo-tsm',
 | |
| }
 | |
| 
 | |
| discopool = eventlet.greenpool.GreenPool(500)
 | |
| runningevals = {}
 | |
| # Passive-only auto-detection protocols:
 | |
| # PXE
 | |
| 
 | |
| # Both passive and active
 | |
| # SLP (passive mode listens for SLP DA and unicast interrogation of the system)
 | |
| # mDNS
 | |
| # SSD
 | |
| 
 | |
| # Also there are location providers
 | |
| # Switch
 | |
| # chassis
 | |
| # chassis may in turn describe more chassis
 | |
| 
 | |
| # We normalize discovered node data to the following pieces of information:
 | |
| # * Detected node name (if available, from switch discovery or similar or
 | |
| #   auto generated node name.
 | |
| # * Model number
 | |
| # * Model name
 | |
| # * Serial number
 | |
| # * System UUID (in x86 space, specifically whichever UUID would be in DMI)
 | |
| # * Network interfaces and addresses
 | |
| # * Switch connectivity information
 | |
| # * enclosure information
 | |
| # * Management TLS fingerprint if validated (switch publication or enclosure)
 | |
| # * System TLS fingerprint if validated (switch publication or system manager)
 | |
| 
 | |
| 
 | |
| #TODO: by serial, by uuid, by node
 | |
| known_info = {}
 | |
| known_services = {}
 | |
| known_serials = {}
 | |
| known_uuids = nesteddict()
 | |
| known_nodes = nesteddict()
 | |
| unknown_info = {}
 | |
| pending_nodes = {}
 | |
| pending_by_uuid = {}
 | |
| 
 | |
| 
 | |
| def enrich_pxe_info(info):
 | |
|     sn = None
 | |
|     mn = None
 | |
|     nodename = info.get('nodename', None)
 | |
|     uuid = info.get('uuid', '')
 | |
|     if not uuid_is_valid(uuid):
 | |
|         return info
 | |
|     for mac in known_uuids.get(uuid, {}):
 | |
|         if not sn and 'serialnumber' in known_uuids[uuid][mac]:
 | |
|             info['serialnumber'] = known_uuids[uuid][mac]['serialnumber']
 | |
|         if not mn and 'modelnumber' in known_uuids[uuid][mac]:
 | |
|             info['modelnumber'] = known_uuids[uuid][mac]['modelnumber']
 | |
|         if nodename is None and 'nodename' in known_uuids[uuid][mac]:
 | |
|             info['nodename'] = known_uuids[uuid][mac]['nodename']
 | |
| 
 | |
| 
 | |
| 
 | |
| def uuid_is_valid(uuid):
 | |
|     if not uuid:
 | |
|         return False
 | |
|     return uuid.lower() not in ('00000000-0000-0000-0000-000000000000',
 | |
|                                 'ffffffff-ffff-ffff-ffff-ffffffffffff',
 | |
|                                 '00112233-4455-6677-8899-aabbccddeeff',
 | |
|                                 '20202020-2020-2020-2020-202020202020')
 | |
| 
 | |
| def _printable_ip(sa):
 | |
|     return nsocket.getnameinfo(
 | |
|         sa, nsocket.NI_NUMERICHOST|nsocket.NI_NUMERICSERV)[0]
 | |
| 
 | |
| def send_discovery_datum(info):
 | |
|     addresses = info.get('addresses', [])
 | |
|     if info['handler'] == pxeh:
 | |
|         enrich_pxe_info(info)
 | |
|     yield msg.KeyValueData({'nodename': info.get('nodename', '')})
 | |
|     yield msg.KeyValueData({'ipaddrs': [_printable_ip(x) for x in addresses]})
 | |
|     sn = info.get('serialnumber', '')
 | |
|     mn = info.get('modelnumber', '')
 | |
|     uuid = info.get('uuid', '')
 | |
|     if uuid:
 | |
|         relatedmacs = []
 | |
|         for mac in known_uuids.get(uuid, {}):
 | |
|             if mac and mac != info.get('hwaddr', ''):
 | |
|                 relatedmacs.append(mac)
 | |
|         if relatedmacs:
 | |
|             yield msg.KeyValueData({'relatedmacs': relatedmacs})
 | |
|     yield msg.KeyValueData({'serialnumber': sn})
 | |
|     yield msg.KeyValueData({'modelnumber': mn})
 | |
|     yield msg.KeyValueData({'uuid': uuid})
 | |
|     if 'enclosure.bay' in info:
 | |
|         yield msg.KeyValueData({'bay': int(info['enclosure.bay'])})
 | |
|     yield msg.KeyValueData({'macs': [info.get('hwaddr', '')]})
 | |
|     types = []
 | |
|     for infotype in info.get('services', []):
 | |
|         if infotype in servicenames:
 | |
|             types.append(servicenames[infotype])
 | |
|     yield msg.KeyValueData({'types': types})
 | |
|     if 'otheraddresses' in info:
 | |
|         yield msg.KeyValueData({'otheripaddrs': list(info['otheraddresses'])})
 | |
| 
 | |
| 
 | |
| def _info_matches(info, criteria):
 | |
|     model = criteria.get('by-model', None)
 | |
|     devtype = criteria.get('by-type', None)
 | |
|     node = criteria.get('by-node', None)
 | |
|     serial = criteria.get('by-serial', None)
 | |
|     status = criteria.get('by-state', None)
 | |
|     uuid = criteria.get('by-uuid', None)
 | |
|     if model and info.get('modelnumber', None) != model:
 | |
|         return False
 | |
|     if devtype and devtype not in info.get('services', []):
 | |
|             return False
 | |
|     if node and info.get('nodename', None) != node:
 | |
|         return False
 | |
|     if serial and info.get('serialnumber', None) != serial:
 | |
|         return False
 | |
|     if status and info.get('discostatus', None) != status:
 | |
|         return False
 | |
|     if uuid and info.get('uuid', None) != uuid:
 | |
|         return False
 | |
|     return True
 | |
| 
 | |
| 
 | |
| def list_matching_nodes(criteria):
 | |
|     retnodes = []
 | |
|     for node in known_nodes:
 | |
|         for mac in known_nodes[node]:
 | |
|             info = known_info[mac]
 | |
|             if _info_matches(info, criteria):
 | |
|                 retnodes.append(node)
 | |
|                 break
 | |
|     retnodes.sort(key=noderange.humanify_nodename)
 | |
|     return [msg.ChildCollection(node + '/') for node in retnodes]
 | |
| 
 | |
| 
 | |
| def list_matching_serials(criteria):
 | |
|     for serial in sorted(list(known_serials)):
 | |
|         info = known_serials[serial]
 | |
|         if _info_matches(info, criteria):
 | |
|             yield msg.ChildCollection(serial + '/')
 | |
| 
 | |
| def list_matching_uuids(criteria):
 | |
|     for uuid in sorted(list(known_uuids)):
 | |
|         for mac in known_uuids[uuid]:
 | |
|             info = known_uuids[uuid][mac]
 | |
|             if _info_matches(info, criteria):
 | |
|                 yield msg.ChildCollection(uuid + '/')
 | |
|                 break
 | |
| 
 | |
| 
 | |
| def list_matching_states(criteria):
 | |
|     return [msg.ChildCollection(x) for x in ('discovered/', 'identified/',
 | |
|                                              'unidentified/')]
 | |
| 
 | |
| def list_matching_macs(criteria):
 | |
|     for mac in sorted(list(known_info)):
 | |
|         info = known_info[mac]
 | |
|         if _info_matches(info, criteria):
 | |
|             yield msg.ChildCollection(mac.replace(':', '-'))
 | |
| 
 | |
| 
 | |
| def list_matching_types(criteria):
 | |
|     rettypes = []
 | |
|     for infotype in known_services:
 | |
|         typename = servicenames[infotype]
 | |
|         if ('by-model' not in criteria or
 | |
|                 criteria['by-model'] in known_services[infotype]):
 | |
|             rettypes.append(typename)
 | |
|     return [msg.ChildCollection(typename + '/')
 | |
|             for typename in sorted(rettypes)]
 | |
| 
 | |
| 
 | |
| def list_matching_models(criteria):
 | |
|     for model in sorted(list(detected_models())):
 | |
|         if ('by-type' not in criteria or
 | |
|                 model in known_services[criteria['by-type']]):
 | |
|             yield msg.ChildCollection(model + '/')
 | |
| 
 | |
| 
 | |
| def show_info(mac):
 | |
|     mac = mac.replace('-', ':')
 | |
|     if mac not in known_info:
 | |
|         raise exc.NotFoundException(mac + ' not a known mac address')
 | |
|     for i in send_discovery_datum(known_info[mac]):
 | |
|         yield i
 | |
| 
 | |
| 
 | |
| list_info = {
 | |
|     'by-node': list_matching_nodes,
 | |
|     'by-serial': list_matching_serials,
 | |
|     'by-type': list_matching_types,
 | |
|     'by-model': list_matching_models,
 | |
|     'by-mac': list_matching_macs,
 | |
|     'by-state': list_matching_states,
 | |
|     'by-uuid': list_matching_uuids,
 | |
| }
 | |
| 
 | |
| multi_selectors = set([
 | |
|     'by-type',
 | |
|     'by-model',
 | |
|     'by-state',
 | |
|     'by-uuid',
 | |
| ])
 | |
| 
 | |
| 
 | |
| node_selectors = set([
 | |
|     'by-node',
 | |
|     'by-serial',
 | |
| ])
 | |
| 
 | |
| 
 | |
| single_selectors = set([
 | |
|     'by-mac',
 | |
| ])
 | |
| 
 | |
| 
 | |
| def _parameterize_path(pathcomponents):
 | |
|     listrequested = False
 | |
|     childcoll = True
 | |
|     if len(pathcomponents) % 2 == 1:
 | |
|         listrequested = pathcomponents[-1]
 | |
|         pathcomponents = pathcomponents[:-1]
 | |
|     pathit = iter(pathcomponents)
 | |
|     keyparams = {}
 | |
|     validselectors = multi_selectors | node_selectors | single_selectors
 | |
|     for key, val in zip(pathit, pathit):
 | |
|         if key not in validselectors:
 | |
|             raise exc.NotFoundException('{0} is not valid here'.format(key))
 | |
|         if key == 'by-type':
 | |
|             keyparams[key] = servicebyname.get(val, '!!!!invalid-type')
 | |
|         else:
 | |
|             keyparams[key] = val
 | |
|         validselectors.discard(key)
 | |
|         if key in single_selectors:
 | |
|             childcoll = False
 | |
|             validselectors = set([])
 | |
|         elif key in node_selectors:
 | |
|             validselectors = single_selectors | set([])
 | |
|     return validselectors, keyparams, listrequested, childcoll
 | |
| 
 | |
| 
 | |
| def handle_autosense_config(operation, inputdata):
 | |
|     autosense = cfm.get_global('discovery.autosense')
 | |
|     autosense = autosense or autosense is None
 | |
|     if operation == 'retrieve':
 | |
|         yield msg.KeyValueData({'enabled': autosense})
 | |
|     elif operation == 'update':
 | |
|         enabled = inputdata['enabled']
 | |
|         if type(enabled) in (unicode, bytes):
 | |
|             enabled = enabled.lower() in ('true', '1', 'y', 'yes', 'enable',
 | |
|                                           'enabled')
 | |
|         if autosense == enabled:
 | |
|             return
 | |
|         cfm.set_global('discovery.autosense', enabled)
 | |
|         if enabled:
 | |
|             start_autosense()
 | |
|         else:
 | |
|             stop_autosense()
 | |
| 
 | |
| 
 | |
| def handle_api_request(configmanager, inputdata, operation, pathcomponents):
 | |
|     if pathcomponents == ['discovery', 'autosense']:
 | |
|         return handle_autosense_config(operation, inputdata)
 | |
|     if operation == 'retrieve':
 | |
|         return handle_read_api_request(pathcomponents)
 | |
|     elif (operation in ('update', 'create') and
 | |
|             pathcomponents == ['discovery', 'rescan']):
 | |
|         if inputdata != {'rescan': 'start'}:
 | |
|             raise exc.InvalidArgumentException()
 | |
|         rescan()
 | |
|         return (msg.KeyValueData({'rescan': 'started'}),)
 | |
| 
 | |
|     elif operation in ('update', 'create'):
 | |
|         if 'node' not in inputdata:
 | |
|             raise exc.InvalidArgumentException('Missing node name in input')
 | |
|         mac = _get_mac_from_query(pathcomponents)
 | |
|         info = known_info[mac]
 | |
|         if info['handler'] is None:
 | |
|             raise exc.NotImplementedException(
 | |
|                 'Unable to {0} to {1}'.format(operation,
 | |
|                                               '/'.join(pathcomponents)))
 | |
|         handler = info['handler'].NodeHandler(info, configmanager)
 | |
|         try:
 | |
|             eval_node(configmanager, handler, info, inputdata['node'],
 | |
|                       manual=True)
 | |
|         except Exception as e:
 | |
|             # or... incorrect passworod provided..
 | |
|             if 'Incorrect password' in str(e) or 'Unauthorized name' in str(e):
 | |
|                 return [msg.ConfluentTargetInvalidCredentials(
 | |
|                     inputdata['node'])]
 | |
|             raise
 | |
|         return [msg.AssignedResource(inputdata['node'])]
 | |
|     elif operation == 'delete':
 | |
|         mac = _get_mac_from_query(pathcomponents)
 | |
|         del known_info[mac]
 | |
|         return [msg.DeletedResource(mac)]
 | |
|     raise exc.NotImplementedException(
 | |
|         'Unable to {0} to {1}'.format(operation, '/'.join(pathcomponents)))
 | |
| 
 | |
| 
 | |
| def _get_mac_from_query(pathcomponents):
 | |
|     _, queryparms, _, _ = _parameterize_path(pathcomponents[1:])
 | |
|     if 'by-mac' not in queryparms:
 | |
|         raise exc.InvalidArgumentException('Must target using "by-mac"')
 | |
|     mac = queryparms['by-mac'].replace('-', ':')
 | |
|     if mac not in known_info:
 | |
|         raise exc.NotFoundException('{0} not found'.format(mac))
 | |
|     return mac
 | |
| 
 | |
| 
 | |
| def handle_read_api_request(pathcomponents):
 | |
|     # TODO(jjohnson2): This should be more generalized...
 | |
|     #  odd indexes into components are 'by-'*, even indexes
 | |
|     # starting at 2 are parameters to previous index
 | |
|     if pathcomponents == ['discovery', 'rescan']:
 | |
|         return (msg.KeyValueData({'scanning': bool(scanner)}),)
 | |
|     subcats, queryparms, indexof, coll = _parameterize_path(pathcomponents[1:])
 | |
|     if len(pathcomponents) == 1:
 | |
|         dirlist = [msg.ChildCollection(x + '/') for x in sorted(list(subcats))]
 | |
|         dirlist.append(msg.ChildCollection('rescan'))
 | |
|         dirlist.append(msg.ChildCollection('autosense'))
 | |
|         return dirlist
 | |
|     if not coll:
 | |
|         return show_info(queryparms['by-mac'])
 | |
|     if not indexof:
 | |
|         return [msg.ChildCollection(x + '/') for x in sorted(list(subcats))]
 | |
|     if indexof not in list_info:
 | |
|         raise exc.NotFoundException('{0} is not found'.format(indexof))
 | |
|     return list_info[indexof](queryparms)
 | |
| 
 | |
| 
 | |
| def detected_services():
 | |
|     for srv in known_services:
 | |
|         yield servicenames[srv]
 | |
| 
 | |
| 
 | |
| def detected_models():
 | |
|     knownmodels = set([])
 | |
|     for info in known_info:
 | |
|         info = known_info[info]
 | |
|         if 'modelnumber' in info and info['modelnumber'] not in knownmodels:
 | |
|             knownmodels.add(info['modelnumber'])
 | |
|             yield info['modelnumber']
 | |
| 
 | |
| 
 | |
| def _recheck_nodes(nodeattribs, configmanager):
 | |
|     if rechecklock.locked():
 | |
|         # if already in progress, don't run again
 | |
|         # it may make sense to schedule a repeat, but will try the easier and less redundant way first
 | |
|         return
 | |
|     with rechecklock:
 | |
|         return _recheck_nodes_backend(nodeattribs, configmanager)
 | |
| 
 | |
| def _recheck_nodes_backend(nodeattribs, configmanager):
 | |
|     global rechecker
 | |
|     _map_unique_ids(nodeattribs)
 | |
|     # for the nodes whose attributes have changed, consider them as potential
 | |
|     # strangers
 | |
|     if nodeattribs:
 | |
|         macmap.vintage = 0  # expire current mac map data, in case
 | |
|         # the attributes changed impacted the result
 | |
|     for node in nodeattribs:
 | |
|         if node in known_nodes:
 | |
|             for somemac in known_nodes[node]:
 | |
|                 unknown_info[somemac] = known_nodes[node][somemac]
 | |
|                 unknown_info[somemac]['discostatus'] = 'unidentified'
 | |
|     # Now we go through ones we did not find earlier
 | |
|     for mac in list(unknown_info):
 | |
|         try:
 | |
|             _recheck_single_unknown(configmanager, mac)
 | |
|         except Exception:
 | |
|             traceback.print_exc()
 | |
|             continue
 | |
|     # now we go through ones that were identified, but could not pass
 | |
|     # policy or hadn't been able to verify key
 | |
|     for nodename in pending_nodes:
 | |
|         info = pending_nodes[nodename]
 | |
|         try:
 | |
|             if info['handler'] is None:
 | |
|                 next
 | |
|             handler = info['handler'].NodeHandler(info, configmanager)
 | |
|             discopool.spawn_n(eval_node, configmanager, handler, info, nodename)
 | |
|         except Exception:
 | |
|             traceback.print_exc()
 | |
|             log.log({'error': 'Unexpected error during discovery of {0}, check debug '
 | |
|                               'logs'.format(nodename)})
 | |
| 
 | |
| 
 | |
| def _recheck_single_unknown(configmanager, mac):
 | |
|     info = unknown_info.get(mac, None)
 | |
|     _recheck_single_unknown_info(configmanager, info)
 | |
| 
 | |
| 
 | |
| def _recheck_single_unknown_info(configmanager, info):
 | |
|     global rechecker
 | |
|     global rechecktime
 | |
|     if not info or info['handler'] is None:
 | |
|         return
 | |
|     if info['handler'] != pxeh and not info.get('addresses', None):
 | |
|         #log.log({'info': 'Missing address information in ' + repr(info)})
 | |
|         return
 | |
|     handler = info['handler'].NodeHandler(info, configmanager)
 | |
|     if handler.https_supported and not handler.https_cert:
 | |
|         if handler.cert_fail_reason == 'unreachable':
 | |
|             log.log(
 | |
|                 {
 | |
|                     'info': '{0} with hwaddr {1} is not reachable at {2}'
 | |
|                             ''.format(
 | |
|                         handler.devname, info['hwaddr'], handler.ipaddr
 | |
|                     )})
 | |
|             # addresses data is bad, delete the offending ip
 | |
|             info['addresses'] = [x for x in info.get('addresses', []) if x != handler.ipaddr]
 | |
|             # TODO(jjohnson2):  rescan due to bad peer addr data?
 | |
|             # not just wait around for the next announce
 | |
|             return
 | |
|         log.log(
 | |
|             {
 | |
|                 'info': '{0} with hwaddr {1} at address {2} is not yet running '
 | |
|                         'https, will examine later'.format(
 | |
|                     handler.devname, info['hwaddr'], handler.ipaddr
 | |
|                 )})
 | |
|         if rechecker is not None and rechecktime > util.monotonic_time() + 300:
 | |
|             rechecker.cancel()
 | |
|         # if cancel did not result in dead, then we are in progress
 | |
|         if rechecker is None or rechecker.dead:
 | |
|             rechecktime = util.monotonic_time() + 300
 | |
|             rechecker = eventlet.spawn_after(300, _periodic_recheck,
 | |
|                                              configmanager)
 | |
|         return
 | |
|     nodename, info['maccount'] = get_nodename(configmanager, handler, info)
 | |
|     if nodename:
 | |
|         if handler.https_supported:
 | |
|             dp = configmanager.get_node_attributes([nodename],
 | |
|                                          ('pubkeys.tls_hardwaremanager',))
 | |
|             lastfp = dp.get(nodename, {}).get('pubkeys.tls_hardwaremanager',
 | |
|                                               {}).get('value', None)
 | |
|             if util.cert_matches(lastfp, handler.https_cert):
 | |
|                 info['nodename'] = nodename
 | |
|                 known_nodes[nodename][info['hwaddr']] = info
 | |
|                 info['discostatus'] = 'discovered'
 | |
|                 return  # already known, no need for more
 | |
|         discopool.spawn_n(eval_node, configmanager, handler, info, nodename)
 | |
| 
 | |
| 
 | |
| def safe_detected(info):
 | |
|     if 'hwaddr' not in info or not info['hwaddr']:
 | |
|         return
 | |
|     if info['hwaddr'] in runningevals:
 | |
|         # Do not evaluate the same mac multiple times at once
 | |
|         return
 | |
|     runningevals[info['hwaddr']] = discopool.spawn(eval_detected, info)
 | |
| 
 | |
| 
 | |
| def eval_detected(info):
 | |
|     try:
 | |
|         detected(info)
 | |
|     except Exception as e:
 | |
|         traceback.print_exc()
 | |
|     del runningevals[info['hwaddr']]
 | |
| 
 | |
| 
 | |
| def detected(info):
 | |
|     global rechecker
 | |
|     global rechecktime
 | |
|     # later, manual and CMM discovery may act on SN and/or UUID
 | |
|     for service in info['services']:
 | |
|         if service in nodehandlers:
 | |
|             if service not in known_services:
 | |
|                 known_services[service] = set([])
 | |
|             handler = nodehandlers[service]
 | |
|             info['handler'] = handler
 | |
|             break
 | |
|     else:  # no nodehandler, ignore for now
 | |
|         return
 | |
|     if (handler and not handler.NodeHandler.adequate(info) and
 | |
|             info.get('protocol', None)):
 | |
|         eventlet.spawn_after(10, info['protocol'].fix_info, info,
 | |
|                              safe_detected)
 | |
|         return
 | |
|     try:
 | |
|         snum = info['attributes']['enclosure-serial-number'][0].strip()
 | |
|         if snum:
 | |
|             info['serialnumber'] = snum
 | |
|             known_serials[info['serialnumber']] = info
 | |
|     except (KeyError, IndexError):
 | |
|         pass
 | |
|     try:
 | |
|         info['modelnumber'] = info['attributes']['enclosure-machinetype-model'][0]
 | |
|         known_services[service].add(info['modelnumber'])
 | |
|     except (KeyError, IndexError):
 | |
|         pass
 | |
|     if info['hwaddr'] in known_info and 'addresses' in info:
 | |
|         # we should tee these up for parsing when an enclosure comes up
 | |
|         # also when switch config parameters change, should discard
 | |
|         # and there's also if wiring is fixed...
 | |
|         # of course could periodically revisit known_nodes
 | |
|         # replace potentially stale address info
 | |
|         #TODO(jjohnson2): remove this
 | |
|         # temporary workaround for XCC not doing SLP DA over dedicated port
 | |
|         # bz 93219, fix submitted, but not in builds yet
 | |
|         # strictly speaking, going ipv4 only legitimately is mistreated here,
 | |
|         # but that should be an edge case
 | |
|         oldaddr = known_info[info['hwaddr']].get('addresses', [])
 | |
|         for addr in info['addresses']:
 | |
|             if addr[0].startswith('fe80::'):
 | |
|                 break
 | |
|         else:
 | |
|             for addr in oldaddr:
 | |
|                 if addr[0].startswith('fe80::'):
 | |
|                     info['addresses'].append(addr)
 | |
|         if known_info[info['hwaddr']].get(
 | |
|                 'addresses', []) == info['addresses']:
 | |
|             # if the ip addresses match, then assume no changes
 | |
|             # now something resetting to defaults could, in theory
 | |
|             # have the same address, but need to be reset
 | |
|             # in that case, however, a user can clear pubkeys to force a check
 | |
|             return
 | |
|     known_info[info['hwaddr']] = info
 | |
|     cfg = cfm.ConfigManager(None)
 | |
|     if handler:
 | |
|         handler = handler.NodeHandler(info, cfg)
 | |
|         handler.scan()
 | |
|     uuid = info.get('uuid', None)
 | |
|     if uuid_is_valid(uuid):
 | |
|         known_uuids[uuid][info['hwaddr']] = info
 | |
|     info['otheraddresses'] = set([])
 | |
|     for i4addr in info.get('attributes', {}).get('ipv4-address', []):
 | |
|         info['otheraddresses'].add(i4addr)
 | |
|     if handler and handler.https_supported and not handler.https_cert:
 | |
|         if handler.cert_fail_reason == 'unreachable':
 | |
|             log.log(
 | |
|                 {
 | |
|                     'info': '{0} with hwaddr {1} is not reachable by https '
 | |
|                             'at address {2}'.format(
 | |
|                         handler.devname, info['hwaddr'], handler.ipaddr
 | |
|                     )})
 | |
|             info['addresses'] = [x for x in info.get('addresses', []) if x != handler.ipaddr]
 | |
|             return
 | |
|         log.log(
 | |
|             {'info':  '{0} with hwaddr {1} at address {2} is not yet running '
 | |
|                       'https, will examine later'.format(
 | |
|                         handler.devname, info['hwaddr'], handler.ipaddr
 | |
|             )})
 | |
|         if rechecker is not None and rechecktime > util.monotonic_time() + 300:
 | |
|             rechecker.cancel()
 | |
|         if rechecker is None or rechecker.dead:
 | |
|             rechecktime = util.monotonic_time() + 300
 | |
|             rechecker = eventlet.spawn_after(300, _periodic_recheck, cfg)
 | |
|         unknown_info[info['hwaddr']] = info
 | |
|         info['discostatus'] = 'unidentfied'
 | |
|         #TODO, eventlet spawn after to recheck sooner, or somehow else
 | |
|         # influence periodic recheck to shorten delay?
 | |
|         return
 | |
|     nodename, info['maccount'] = get_nodename(cfg, handler, info)
 | |
|     if nodename and handler and handler.https_supported:
 | |
|         dp = cfg.get_node_attributes([nodename],
 | |
|                                      ('pubkeys.tls_hardwaremanager',))
 | |
|         lastfp = dp.get(nodename, {}).get('pubkeys.tls_hardwaremanager',
 | |
|                                           {}).get('value', None)
 | |
|         if util.cert_matches(lastfp, handler.https_cert):
 | |
|             info['nodename'] = nodename
 | |
|             known_nodes[nodename][info['hwaddr']] = info
 | |
|             info['discostatus'] = 'discovered'
 | |
|             return  # already known, no need for more
 | |
|     #TODO(jjohnson2): We might have to get UUID for certain searches...
 | |
|     #for now defer probe until inside eval_node.  We might not have
 | |
|     #a nodename without probe in the future.
 | |
|     if nodename and handler:
 | |
|         eval_node(cfg, handler, info, nodename)
 | |
|     elif handler:
 | |
|         #log.log(
 | |
|         #    {'info': 'Detected unknown {0} with hwaddr {1} at '
 | |
|         #             'address {2}'.format(
 | |
|         #                handler.devname, info['hwaddr'], handler.ipaddr
 | |
|         #              )})
 | |
|         info['discostatus'] = 'unidentified'
 | |
|         unknown_info[info['hwaddr']] = info
 | |
| 
 | |
| 
 | |
| 
 | |
| def b64tohex(b64str):
 | |
|     bd = base64.b64decode(b64str)
 | |
|     bd = bytearray(bd)
 | |
|     return ''.join(['{0:02x}'.format(x) for x in bd])
 | |
| 
 | |
| 
 | |
| def get_enclosure_chain_head(nodename, cfg):
 | |
|     ne = True
 | |
|     members = [nodename]
 | |
|     while ne:
 | |
|         ne = cfg.get_node_attributes(
 | |
|             nodename, 'enclosure.extends').get(nodename, {}).get(
 | |
|             'enclosure.extends', {}).get('value', None)
 | |
|         if not ne:
 | |
|             return nodename
 | |
|         if ne in members:
 | |
|             raise exc.InvalidArgumentException(
 | |
|                 'Circular chain that includes ' + nodename)
 | |
|         if not cfg.is_node(ne):
 | |
|             raise exc.InvalidArgumentException(
 | |
|                 '{0} is chained to nonexistent node {1} '.format(
 | |
|                     nodename, ne))
 | |
|         nodename = ne
 | |
|         members.append(nodename)
 | |
|     return nodename
 | |
| 
 | |
| 
 | |
| def get_chained_smm_name(nodename, cfg, handler, nl=None, checkswitch=True):
 | |
|     # nodename is the head of the chain, cfg is a configmanager, handler
 | |
|     # is the handler of the current candidate, nl is optional indication
 | |
|     # of the next link in the chain, checkswitch can disable the switch
 | |
|     # search if not indicated by current situation
 | |
|     # returns the new name and whether it has been securely validated or not
 | |
|     # first we check to see if directly connected
 | |
|     mycert = handler.https_cert
 | |
|     if checkswitch:
 | |
|         fprints = macmap.get_node_fingerprints(nodename, cfg)
 | |
|         for fprint in fprints:
 | |
|             if util.cert_matches(fprint[0], mycert):
 | |
|                 # ok we have a direct match, it is this node
 | |
|                 return nodename, fprint[1]
 | |
|     # ok, unable to get it, need to traverse the chain from the beginning
 | |
|     if not nl:
 | |
|         nl = list(cfg.filter_node_attributes(
 | |
|             'enclosure.extends=' + nodename))
 | |
|     while nl:
 | |
|         if len(nl) != 1:
 | |
|             raise exc.InvalidArgumentException('Multiple enclosures trying to '
 | |
|                                                'extend a single enclosure')
 | |
|         cd = cfg.get_node_attributes(nodename, ['hardwaremanagement.manager',
 | |
|                                                 'pubkeys.tls_hardwaremanager'])
 | |
|         smmaddr = cd[nodename]['hardwaremanagement.manager']['value']
 | |
|         pkey = cd[nodename].get('pubkeys.tls_hardwaremanager', {}).get(
 | |
|             'value', None)
 | |
|         if not pkey:
 | |
|             # We cannot continue through a break in the chain
 | |
|             return None, False
 | |
|         if pkey:
 | |
|             cv = util.TLSCertVerifier(
 | |
|                 cfg, nodename, 'pubkeys.tls_hardwaremanager').verify_cert
 | |
|             for fprint in get_smm_neighbor_fingerprints(smmaddr, cv):
 | |
|                 if util.cert_matches(fprint, mycert):
 | |
|                     # a trusted chain member vouched for the cert
 | |
|                     # so it's validated
 | |
|                     return nl[0], True
 | |
|             # advance down the chain by one and try again
 | |
|         nodename = nl[0]
 | |
|         nl = list(cfg.filter_node_attributes(
 | |
|             'enclosure.extends=' + nodename))
 | |
|     return None, False
 | |
| 
 | |
| 
 | |
| def get_smm_neighbor_fingerprints(smmaddr, cv):
 | |
|     if ':' in smmaddr:
 | |
|         smmaddr = '[{0}]'.format(smmaddr)
 | |
|     wc = webclient.SecureHTTPConnection(smmaddr, verifycallback=cv)
 | |
|     neighs = wc.grab_json_response('/scripts/neighdata.json')
 | |
|     if not neighs:
 | |
|         return
 | |
|     for idx in (4, 5):
 | |
|         if 'sha256' not in neighs[idx]:
 | |
|             continue
 | |
|         yield 'sha256$' + b64tohex(neighs[idx]['sha256'])
 | |
| 
 | |
| 
 | |
| def get_nodename(cfg, handler, info):
 | |
|     nodename = None
 | |
|     maccount = None
 | |
|     info['verified'] = False
 | |
|     if not handler:
 | |
|         return None, None
 | |
|     if handler.https_supported:
 | |
|         currcert = handler.https_cert
 | |
|         if not currcert:
 | |
|             info['discofailure'] = 'nohttps'
 | |
|             return None, None
 | |
|         currprint = util.get_fingerprint(currcert, 'sha256')
 | |
|         nodename = nodes_by_fprint.get(currprint, None)
 | |
|         if not nodename:
 | |
|             # Try SHA512 as well
 | |
|             currprint = util.get_fingerprint(currcert)
 | |
|             nodename = nodes_by_fprint.get(currprint, None)
 | |
|     if not nodename:
 | |
|         curruuid = info.get('uuid', None)
 | |
|         if uuid_is_valid(curruuid):
 | |
|             nodename = nodes_by_uuid.get(curruuid, None)
 | |
|             if nodename is None:
 | |
|                 _map_unique_ids()
 | |
|                 nodename = nodes_by_uuid.get(curruuid, None)
 | |
|     if not nodename:
 | |
|         # Ok, see if it is something with a chassis-uuid and discover by
 | |
|         # chassis
 | |
|         nodename = get_nodename_from_enclosures(cfg, info)
 | |
|     if not nodename and handler.devname == 'SMM':
 | |
|         nodename = get_nodename_from_chained_smms(cfg, handler, info)
 | |
|     if not nodename:  # as a last resort, search switches for info
 | |
|         # This is the slowest potential operation, so we hope for the
 | |
|         # best to occur prior to this
 | |
|         nodename, macinfo = macmap.find_nodeinfo_by_mac(info['hwaddr'], cfg)
 | |
|         maccount = macinfo['maccount']
 | |
|         if nodename:
 | |
|             if handler.devname == 'SMM':
 | |
|                 nl = list(cfg.filter_node_attributes(
 | |
|                             'enclosure.extends=' + nodename))
 | |
|                 if nl:
 | |
|                     # We found an SMM, and it's in a chain per configuration
 | |
|                     # we need to ask the switch for the fingerprint to see
 | |
|                     # if we have a match or not
 | |
|                     newnodename, v = get_chained_smm_name(nodename, cfg,
 | |
|                                                           handler, nl)
 | |
|                     if newnodename:
 | |
|                         # while this started by switch, it was disambiguated
 | |
|                         info['verified'] = v
 | |
|                         return newnodename, None
 | |
|         if (nodename and
 | |
|                 not handler.discoverable_by_switch(macinfo['maccount'])):
 | |
|             if handler.devname == 'SMM':
 | |
|                 errorstr = 'Attempt to discover SMM by switch, but chained ' \
 | |
|                            'topology or incorrect net attributes detected, ' \
 | |
|                            'which is not compatible with switch discovery ' \
 | |
|                            'of SMM, nodename would have been ' \
 | |
|                            '{0}'.format(nodename)
 | |
|                 log.log({'error': errorstr})
 | |
|                 return None, None
 | |
|     return nodename, maccount
 | |
| 
 | |
| 
 | |
| def get_nodename_from_chained_smms(cfg, handler, info):
 | |
|     nodename = None
 | |
|     for fprint in get_smm_neighbor_fingerprints(
 | |
|             handler.ipaddr, lambda x: True):
 | |
|         if fprint in nodes_by_fprint:
 | |
|             # need to chase the whole chain
 | |
|             # to support either direction
 | |
|             chead = get_enclosure_chain_head(nodes_by_fprint[fprint],
 | |
|                                              cfg)
 | |
|             newnodename, v = get_chained_smm_name(
 | |
|                 chead, cfg, handler, checkswitch=False)
 | |
|             if newnodename:
 | |
|                 info['verified'] = v
 | |
|                 nodename = newnodename
 | |
|     return nodename
 | |
| 
 | |
| def get_node_by_uuid(uuid):
 | |
|     return nodes_by_uuid.get(uuid, None)
 | |
| 
 | |
| def get_nodename_from_enclosures(cfg, info):
 | |
|     nodename = None
 | |
|     cuuid = info.get('attributes', {}).get('chassis-uuid', [None])[0]
 | |
|     if cuuid and cuuid in nodes_by_uuid:
 | |
|         encl = nodes_by_uuid[cuuid]
 | |
|         bay = info.get('enclosure.bay', None)
 | |
|         if bay:
 | |
|             tnl = cfg.filter_node_attributes('enclosure.manager=' + encl)
 | |
|             tnl = list(
 | |
|                 cfg.filter_node_attributes('enclosure.bay={0}'.format(bay),
 | |
|                                            tnl))
 | |
|             if len(tnl) == 1:
 | |
|                 # This is not a secure assurance, because it's by
 | |
|                 # uuid instead of a key
 | |
|                 nodename = tnl[0]
 | |
|     return nodename
 | |
| 
 | |
| 
 | |
| def eval_node(cfg, handler, info, nodename, manual=False):
 | |
|     try:
 | |
|         handler.probe()  # unicast interrogation as possible to get more data
 | |
|         # switch concurrently
 | |
|         # do some preconfig, for example, to bring a SMM online if applicable
 | |
|         handler.preconfig(nodename)
 | |
|     except Exception as e:
 | |
|         unknown_info[info['hwaddr']] = info
 | |
|         info['discostatus'] = 'unidentified'
 | |
|         errorstr = 'An error occured during discovery, check the ' \
 | |
|                    'trace and stderr logs, mac was {0} and ip was {1}' \
 | |
|                    ', the node or the containing enclosure was {2}' \
 | |
|                    ''.format(info['hwaddr'], handler.ipaddr, nodename)
 | |
|         traceback.print_exc()
 | |
|         if manual:
 | |
|             raise exc.InvalidArgumentException(errorstr)
 | |
|         log.log({'error': errorstr})
 | |
|         return
 | |
|     # first, if had a bay, it was in an enclosure.  If it was discovered by
 | |
|     # switch, it is probably the enclosure manager and not
 | |
|     # the node directly.  switch is ambiguous and we should leave it alone
 | |
|     if 'enclosure.bay' in info and handler.is_enclosure:
 | |
|         unknown_info[info['hwaddr']] = info
 | |
|         info['discostatus'] = 'unidentified'
 | |
|         log.log({'error': 'Something that is an enclosure reported a bay, '
 | |
|                           'not possible'})
 | |
|         if manual:
 | |
|             raise exc.InvalidArgumentException()
 | |
|         return
 | |
|     nl = list(cfg.filter_node_attributes('enclosure.manager=' + nodename))
 | |
|     if not handler.is_enclosure and nl:
 | |
|         # The specified node is an enclosure (has nodes mapped to it), but
 | |
|         # what we are talking to is *not* an enclosure
 | |
|         # might be ambiguous, need to match chassis-uuid as well..
 | |
|         if 'enclosure.bay' not in info:
 | |
|             unknown_info[info['hwaddr']] = info
 | |
|             info['discostatus'] = 'unidentified'
 | |
|             errorstr = '{2} with mac {0} is in {1}, but unable to ' \
 | |
|                        'determine bay number'.format(info['hwaddr'],
 | |
|                                                      nodename,
 | |
|                                                      handler.ipaddr)
 | |
|             if manual:
 | |
|                 raise exc.InvalidArgumentException(errorstr)
 | |
|             log.log({'error': errorstr})
 | |
|             return
 | |
|         enl = list(cfg.filter_node_attributes('enclosure.extends=' + nodename))
 | |
|         if enl:
 | |
|             # ambiguous SMM situation according to the configuration, we need
 | |
|             # to match uuid
 | |
|             encuuid = info['attributes'].get('chassis-uuid', None)
 | |
|             if encuuid:
 | |
|                 encuuid = encuuid[0]
 | |
|                 enl = list(cfg.filter_node_attributes('id.uuid=' + encuuid))
 | |
|                 if len(enl) != 1:
 | |
|                     # errorstr = 'No SMM by given UUID known, *yet*'
 | |
|                     # if manual:
 | |
|                     #     raise exc.InvalidArgumentException(errorstr)
 | |
|                     # log.log({'error': errorstr})
 | |
|                     if encuuid in pending_by_uuid:
 | |
|                         pending_by_uuid[encuuid].append(info)
 | |
|                     else:
 | |
|                         pending_by_uuid[encuuid] = [info]
 | |
|                     return
 | |
|                 # We found the real smm, replace the list with the actual smm
 | |
|                 # to continue
 | |
|                 nl = list(cfg.filter_node_attributes(
 | |
|                     'enclosure.manager=' + enl[0]))
 | |
|             else:
 | |
|                 errorstr = 'Chained SMM configuration with older XCC, ' \
 | |
|                            'unable to perform zero power discovery'
 | |
|                 if manual:
 | |
|                     raise exc.InvalidArgumentException(errorstr)
 | |
|                 log.log({'error': errorstr})
 | |
|                 return
 | |
|         # search for nodes fitting our description using filters
 | |
|         # lead with the most specific to have a small second pass
 | |
|         nl = list(cfg.filter_node_attributes(
 | |
|             'enclosure.bay={0}'.format(info['enclosure.bay']), nl))
 | |
|         if len(nl) != 1:
 | |
|             info['discofailure'] = 'ambigconfig'
 | |
|             if len(nl):
 | |
|                 errorstr = 'The following nodes have duplicate ' \
 | |
|                            'enclosure attributes: ' + ','.join(nl)
 | |
| 
 | |
|             else:
 | |
|                 errorstr = 'The {0} in enclosure {1} bay {2} does not ' \
 | |
|                            'seem to be a defined node ({3})'.format(
 | |
|                                         handler.devname, nodename,
 | |
|                                         info['enclosure.bay'],
 | |
|                                         handler.ipaddr,
 | |
|                                     )
 | |
|             if manual:
 | |
|                 raise exc.InvalidArgumentException(errorstr)
 | |
|             log.log({'error': errorstr})
 | |
|             unknown_info[info['hwaddr']] = info
 | |
|             info['discostatus'] = 'unidentified'
 | |
|             return
 | |
|         nodename = nl[0]
 | |
|         if not discover_node(cfg, handler, info, nodename, manual):
 | |
|             # store it as pending, assuming blocked on enclosure
 | |
|             # assurance...
 | |
|             pending_nodes[nodename] = info
 | |
|     else:
 | |
|         # we can and did accurately discover by switch or in enclosure
 | |
|         # but... is this really ok?  could be on an upstream port or
 | |
|         # erroneously put in the enclosure with no nodes yet
 | |
|         # so first, see if the candidate node is a chain host
 | |
|         if not manual:
 | |
|             if info.get('maccount', False):
 | |
|                 # discovery happened through switch
 | |
|                 nl = list(cfg.filter_node_attributes(
 | |
|                     'enclosure.extends=' + nodename))
 | |
|                 if nl:
 | |
|                     # The candidate nodename is the head of a chain, we must
 | |
|                     # validate the smm certificate by the switch
 | |
|                     macmap.get_node_fingerprints(nodename, cfg)
 | |
|                     util.handler.cert_matches(fprint, handler.https_cert)
 | |
|                     return
 | |
|             if (info.get('maccount', False) and
 | |
|                     not handler.discoverable_by_switch(info['maccount'])):
 | |
|                 errorstr = 'The detected node {0} was detected using switch, ' \
 | |
|                            'however the relevant port has too many macs learned ' \
 | |
|                            'for this type of device ({1}) to be discovered by ' \
 | |
|                            'switch.'.format(nodename, handler.devname)
 | |
|                 log.log({'error': errorstr})
 | |
|                 return
 | |
|         if not discover_node(cfg, handler, info, nodename, manual):
 | |
|             pending_nodes[nodename] = info
 | |
| 
 | |
| 
 | |
| def discover_node(cfg, handler, info, nodename, manual):
 | |
|     known_nodes[nodename][info['hwaddr']] = info
 | |
|     if info['hwaddr'] in unknown_info:
 | |
|         del unknown_info[info['hwaddr']]
 | |
|     info['discostatus'] = 'identified'
 | |
|     dp = cfg.get_node_attributes(
 | |
|         [nodename], ('discovery.policy',
 | |
|                      'pubkeys.tls_hardwaremanager'))
 | |
|     policy = dp.get(nodename, {}).get('discovery.policy', {}).get(
 | |
|         'value', None)
 | |
|     if policy is None:
 | |
|         policy = ''
 | |
|     policies = set(policy.split(','))
 | |
|     lastfp = dp.get(nodename, {}).get('pubkeys.tls_hardwaremanager',
 | |
|                                       {}).get('value', None)
 | |
|     # TODO(jjohnson2): permissive requires we guarantee storage of
 | |
|     # the pubkeys, which is deferred for a little bit
 | |
|     # Also, 'secure', when we have the needed infrastructure done
 | |
|     # in some product or another.
 | |
|     curruuid = info.get('uuid', False)
 | |
|     if 'pxe' in policies and info['handler'] == pxeh:
 | |
|         return do_pxe_discovery(cfg, handler, info, manual, nodename, policies)
 | |
|     elif ('permissive' in policies and handler.https_supported and lastfp and
 | |
|             not util.cert_matches(lastfp, handler.https_cert) and not manual):
 | |
|         info['discofailure'] = 'fingerprint'
 | |
|         log.log({'info': 'Detected replacement of {0} with existing '
 | |
|                          'fingerprint and permissive discovery policy, not '
 | |
|                          'doing discovery unless discovery.policy=open or '
 | |
|                          'pubkeys.tls_hardwaremanager attribute is cleared '
 | |
|                          'first'.format(nodename)})
 | |
|         return False  # With a permissive policy, do not discover new
 | |
|     elif policies & set(('open', 'permissive')) or manual:
 | |
|         info['nodename'] = nodename
 | |
|         if info['handler'] == pxeh:
 | |
|             return do_pxe_discovery(cfg, handler, info, manual, nodename, policies)
 | |
|         elif manual or not util.cert_matches(lastfp, handler.https_cert):
 | |
|             # only 'discover' if it is not the same as last time
 | |
|             try:
 | |
|                 handler.config(nodename)
 | |
|             except Exception as e:
 | |
|                 info['discofailure'] = 'bug'
 | |
|                 if manual:
 | |
|                     raise
 | |
|                 log.log(
 | |
|                     {'error':
 | |
|                          'Error encountered trying to set up {0}, {1}'.format(
 | |
|                              nodename, str(e))})
 | |
|                 traceback.print_exc()
 | |
|                 return False
 | |
|             newnodeattribs = {}
 | |
|             if list(cfm.list_collective()):
 | |
|                 # We are in a collective, check collective.manager
 | |
|                 cmc = cfg.get_node_attributes(nodename, 'collective.manager')
 | |
|                 cm = cmc.get(nodename, {}).get('collective.manager', {}).get('value', None)
 | |
|                 if not cm:
 | |
|                     # Node is being discovered in collective, but no collective.manager, default
 | |
|                     # to the collective member actually able to execute the discovery
 | |
|                     newnodeattribs['collective.manager'] = collective.get_myname()
 | |
|             if 'uuid' in info:
 | |
|                 newnodeattribs['id.uuid'] = info['uuid']
 | |
|             if 'serialnumber' in info:
 | |
|                 newnodeattribs['id.serial'] = info['serialnumber']
 | |
|             if 'modelnumber' in info:
 | |
|                 newnodeattribs['id.model'] = info['modelnumber']
 | |
|             if handler.https_cert:
 | |
|                 newnodeattribs['pubkeys.tls_hardwaremanager'] = \
 | |
|                     util.get_fingerprint(handler.https_cert, 'sha256')
 | |
|             if newnodeattribs:
 | |
|                 cfg.set_node_attributes({nodename: newnodeattribs})
 | |
|             log.log({'info': 'Discovered {0} ({1})'.format(nodename,
 | |
|                                                           handler.devname)})
 | |
|         info['discostatus'] = 'discovered'
 | |
|         for i in pending_by_uuid.get(curruuid, []):
 | |
|             eventlet.spawn_n(_recheck_single_unknown_info, cfg, i)
 | |
|         try:
 | |
|             del pending_by_uuid[curruuid]
 | |
|         except KeyError:
 | |
|             pass
 | |
|         return True
 | |
|     log.log({'info': 'Detected {0}, but discovery.policy is not set to a '
 | |
|                      'value allowing discovery (open or permissive)'.format(
 | |
|                         nodename)})
 | |
|     info['discofailure'] = 'policy'
 | |
|     return False
 | |
| 
 | |
| 
 | |
| def do_pxe_discovery(cfg, handler, info, manual, nodename, policies):
 | |
|     # use uuid based scheme in lieu of tls cert, ideally only
 | |
|     # for stateless 'discovery' targets like pxe, where data does not
 | |
|     # change
 | |
|     uuidinfo = cfg.get_node_attributes(nodename, ['id.uuid', 'id.serial', 'id.model', 'net*.bootable'])
 | |
|     if manual or policies & set(('open', 'pxe')):
 | |
|         enrich_pxe_info(info)
 | |
|         attribs = {}
 | |
|         olduuid = uuidinfo.get(nodename, {}).get('id.uuid', None)
 | |
|         uuid = info.get('uuid', None)
 | |
|         if uuid and uuid != olduuid:
 | |
|             attribs['id.uuid'] = info['uuid']
 | |
|         sn = info.get('serialnumber', None)
 | |
|         mn = info.get('modelnumber', None)
 | |
|         if sn and sn != uuidinfo.get(nodename, {}).get('id.serial', None):
 | |
|             attribs['id.serial'] = sn
 | |
|         if mn and mn != uuidinfo.get(nodename, {}).get('id.model', None):
 | |
|             attribs['id.model'] = mn
 | |
|         for attrname in uuidinfo.get(nodename, {}):
 | |
|             if attrname.endswith('.bootable') and uuidinfo[nodename][attrname].get('value', None):
 | |
|                 newattrname = attrname[:-8] + 'hwaddr'
 | |
|                 attribs[newattrname] = info['hwaddr']
 | |
|         if attribs:
 | |
|             cfg.set_node_attributes({nodename: attribs})
 | |
|     if info['uuid'] in known_pxe_uuids:
 | |
|         return True
 | |
|     if uuid_is_valid(info['uuid']):
 | |
|         known_pxe_uuids[info['uuid']] = nodename
 | |
|     #log.log({'info': 'Detected {0} ({1} with mac {2})'.format(
 | |
|     #    nodename, handler.devname, info['hwaddr'])})
 | |
|     return True
 | |
| 
 | |
| 
 | |
| attribwatcher = None
 | |
| nodeaddhandler = None
 | |
| needaddhandled = False
 | |
| 
 | |
| 
 | |
| def _handle_nodelist_change(configmanager):
 | |
|     global needaddhandled
 | |
|     global nodeaddhandler
 | |
|     macmap.vintage = 0  # the current mac map is probably inaccurate
 | |
|     _recheck_nodes((), configmanager)
 | |
|     if needaddhandled:
 | |
|         needaddhandled = False
 | |
|         nodeaddhandler = eventlet.spawn(_handle_nodelist_change, configmanager)
 | |
|     else:
 | |
|         nodeaddhandler = None
 | |
| 
 | |
| 
 | |
| def newnodes(added, deleting, renamed, configmanager):
 | |
|     global attribwatcher
 | |
|     global needaddhandled
 | |
|     global nodeaddhandler
 | |
|     alldeleting = set(deleting) | set(renamed)
 | |
|     for node in alldeleting:
 | |
|         if node not in known_nodes:
 | |
|             continue
 | |
|         for mac in known_nodes[node]:
 | |
|             if mac in known_info:
 | |
|                 del known_info[mac]
 | |
|         del known_nodes[node]
 | |
|     _map_unique_ids()
 | |
|     configmanager.remove_watcher(attribwatcher)
 | |
|     allnodes = configmanager.list_nodes()
 | |
|     attribwatcher = configmanager.watch_attributes(
 | |
|         allnodes, ('discovery.policy', 'net*.switch',
 | |
|                    'hardwaremanagement.manager', 'net*.switchport',
 | |
|                    'id.uuid', 'pubkeys.tls_hardwaremanager',
 | |
|                    'net*.bootable'), _recheck_nodes)
 | |
|     if nodeaddhandler:
 | |
|         needaddhandled = True
 | |
|     else:
 | |
|         nodeaddhandler = eventlet.spawn(_handle_nodelist_change, configmanager)
 | |
| 
 | |
| 
 | |
| 
 | |
| rechecker = None
 | |
| rechecktime = None
 | |
| rechecklock = eventlet.semaphore.Semaphore()
 | |
| 
 | |
| def _periodic_recheck(configmanager):
 | |
|     global rechecker
 | |
|     global rechecktime
 | |
|     rechecker = None
 | |
|     try:
 | |
|         _recheck_nodes((), configmanager)
 | |
|     except Exception:
 | |
|         traceback.print_exc()
 | |
|         log.log({'error': 'Unexpected error during discovery, check debug '
 | |
|                           'logs'})
 | |
|     # if rechecker is set, it means that an accelerated schedule
 | |
|     # for rechecker was requested in the course of recheck_nodes
 | |
|     if rechecker is None:
 | |
|         rechecktime = util.monotonic_time() + 900
 | |
|         rechecker = eventlet.spawn_after(900, _periodic_recheck,
 | |
|                                          configmanager)
 | |
| 
 | |
| 
 | |
| def rescan():
 | |
|     _map_unique_ids()
 | |
|     global scanner
 | |
|     if scanner:
 | |
|         return
 | |
|     else:
 | |
|         scanner = eventlet.spawn(blocking_scan)
 | |
| 
 | |
| 
 | |
| def blocking_scan():
 | |
|     global scanner
 | |
|     slpscan = eventlet.spawn(slp.active_scan, safe_detected, slp)
 | |
|     ssdpscan = eventlet.spawn(ssdp.active_scan, safe_detected, ssdp)
 | |
|     slpscan.wait()
 | |
|     ssdpscan.wait()
 | |
|     scanner = None
 | |
| 
 | |
| def start_detection():
 | |
|     global attribwatcher
 | |
|     global rechecker
 | |
|     global rechecktime
 | |
|     _map_unique_ids()
 | |
|     cfg = cfm.ConfigManager(None)
 | |
|     allnodes = cfg.list_nodes()
 | |
|     attribwatcher = cfg.watch_attributes(
 | |
|         allnodes, ('discovery.policy', 'net*.switch',
 | |
|                    'hardwaremanagement.manager', 'net*.switchport', 'id.uuid',
 | |
|                    'pubkeys.tls_hardwaremanager'), _recheck_nodes)
 | |
|     cfg.watch_nodecollection(newnodes)
 | |
|     autosense = cfm.get_global('discovery.autosense')
 | |
|     if autosense or autosense is None:
 | |
|         start_autosense()
 | |
|     if rechecker is None:
 | |
|         rechecktime = util.monotonic_time() + 900
 | |
|         rechecker = eventlet.spawn_after(900, _periodic_recheck, cfg)
 | |
|     eventlet.spawn_n(ssdp.snoop, None, None, ssdp, get_node_by_uuid)
 | |
| 
 | |
| def stop_autosense():
 | |
|     for watcher in list(autosensors):
 | |
|         watcher.kill()
 | |
|         autosensors.discard(watcher)
 | |
| 
 | |
| def start_autosense():
 | |
|     autosensors.add(eventlet.spawn(slp.snoop, safe_detected, slp))
 | |
|     autosensors.add(eventlet.spawn(pxe.snoop, safe_detected, pxe))
 | |
| 
 | |
| 
 | |
| nodes_by_fprint = {}
 | |
| nodes_by_uuid = {}
 | |
| known_pxe_uuids = {}
 | |
| 
 | |
| def _map_unique_ids(nodes=None):
 | |
|     global nodes_by_uuid
 | |
|     global nodes_by_fprint
 | |
|     nodes_by_uuid = {}
 | |
|     nodes_by_fprint = {}
 | |
|     # Map current known ids based on uuid and fingperprints for fast lookup
 | |
|     cfg = cfm.ConfigManager(None)
 | |
|     if nodes is None:
 | |
|         nodes = cfg.list_nodes()
 | |
|     bigmap = cfg.get_node_attributes(nodes,
 | |
|                                      ('id.uuid',
 | |
|                                       'pubkeys.tls_hardwaremanager'))
 | |
|     uuid_by_nodes = {}
 | |
|     fprint_by_nodes = {}
 | |
|     for uuid in nodes_by_uuid:
 | |
|         if not uuid_is_valid(uuid):
 | |
|             continue
 | |
|         node = nodes_by_uuid[uuid]
 | |
|         if node in bigmap:
 | |
|             uuid_by_nodes[node] = uuid
 | |
|     for fprint in nodes_by_fprint:
 | |
|         node = nodes_by_fprint[fprint]
 | |
|         if node in bigmap:
 | |
|             fprint_by_nodes[node] = fprint
 | |
|     for node in bigmap:
 | |
|         if node in uuid_by_nodes:
 | |
|             del nodes_by_uuid[uuid_by_nodes[node]]
 | |
|         if node in fprint_by_nodes:
 | |
|             del nodes_by_fprint[fprint_by_nodes[node]]
 | |
|         uuid = bigmap[node].get('id.uuid', {}).get('value', None)
 | |
|         if uuid_is_valid(uuid):
 | |
|             nodes_by_uuid[uuid] = node
 | |
|         fprint = bigmap[node].get(
 | |
|             'pubkeys.tls_hardwaremanager', {}).get('value', None)
 | |
|         if fprint:
 | |
|             nodes_by_fprint[fprint] = node
 | |
|     for uuid in known_pxe_uuids:
 | |
|         if uuid_is_valid(uuid) and uuid not in nodes_by_uuid:
 | |
|             nodes_by_uuid[uuid] = known_pxe_uuids[uuid]
 | |
| 
 | |
| 
 | |
| if __name__ == '__main__':
 | |
|     start_detection()
 | |
|     while True:
 | |
|         eventlet.sleep(30)
 |