diff --git a/confluent_client/bin/nodediscover b/confluent_client/bin/nodediscover index a14b7358..41ecf8b3 100755 --- a/confluent_client/bin/nodediscover +++ b/confluent_client/bin/nodediscover @@ -57,12 +57,15 @@ def print_disco(options, session, currmac, outhandler, columns): procinfo.update(tmpinfo) if 'Switch' in columns or 'Port' in columns: - for tmpinfo in session.read( - '/networking/macs/by-mac/{0}'.format(currmac)): - if 'ports' in tmpinfo: - # The api sorts so that the most specific available value - # is last - procinfo.update(tmpinfo['ports'][-1]) + if 'switch' in procinfo: + procinfo['port'] = procinfo['switchport'] + else: + for tmpinfo in session.read( + '/networking/macs/by-mac/{0}'.format(currmac)): + if 'ports' in tmpinfo: + # The api sorts so that the most specific available value + # is last + procinfo.update(tmpinfo['ports'][-1]) record = [] for col in columns: rawcol = columnmapping[col] diff --git a/confluent_server/confluent/config/attributes.py b/confluent_server/confluent/config/attributes.py index 5444e0d7..c25045c6 100644 --- a/confluent_server/confluent/config/attributes.py +++ b/confluent_server/confluent/config/attributes.py @@ -256,7 +256,7 @@ node = { 'so long as the node has no existing public key. ' '"open" allows discovery even if a known public key ' 'is already stored', - 'validlist': ('manual', 'permissive', 'pxe', 'open'), + 'validlist': ('manual', 'permissive', 'pxe', 'open', 'verified'), }, 'info.note': { 'description': 'A field used for administrators to make arbitrary ' diff --git a/confluent_server/confluent/discovery/core.py b/confluent_server/confluent/discovery/core.py index 11f9b28b..05ab0a21 100644 --- a/confluent_server/confluent/discovery/core.py +++ b/confluent_server/confluent/discovery/core.py @@ -217,7 +217,12 @@ def send_discovery_datum(info): 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]}) + if not info.get('forwarder_server', None): + yield msg.KeyValueData({'ipaddrs': [_printable_ip(x) for x in addresses]}) + switch = info.get('forwarder_server', None) + if switch: + yield msg.KeyValueData({'switch': switch}) + yield msg.KeyValueData({'switchport': info['port']}) sn = info.get('serialnumber', '') mn = info.get('modelnumber', '') uuid = info.get('uuid', '') @@ -429,6 +434,8 @@ def handle_api_request(configmanager, inputdata, operation, pathcomponents): return (msg.KeyValueData({'rescan': 'started'}),) elif operation in ('update', 'create'): + if pathcomponents == ['discovery', 'register']: + return if 'node' not in inputdata: raise exc.InvalidArgumentException('Missing node name in input') mac = _get_mac_from_query(pathcomponents) @@ -688,6 +695,8 @@ def detected(info): info['otheraddresses'] = set([]) for i4addr in info.get('attributes', {}).get('ipv4-address', []): info['otheraddresses'].add(i4addr) + for i4addr in info.get('attributes', {}).get('ipv4-addresses', []): + info['otheraddresses'].add(i4addr) if handler and handler.https_supported and not handler.https_cert: if handler.cert_fail_reason == 'unreachable': log.log( @@ -1144,7 +1153,15 @@ def discover_node(cfg, handler, info, nodename, manual): '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: + elif policies & set(('open', 'permissive', 'verified')) or manual: + if 'verified' in policies: + if not handler.https_supported or not util.cert_matches(info['fingerprint'], handler.https_cert): + log.log({'info': 'Detected replacement of {0} without verified ' + 'fingerprint and discovery policy is setto verified, not ' + 'doing discovery unless discovery.policy=open or ' + 'pubkeys.tls_hardwaremanager attribute is cleared ' + 'first'.format(nodename)}) + return False info['nodename'] = nodename if info['handler'] == pxeh: return do_pxe_discovery(cfg, handler, info, manual, nodename, policies) diff --git a/confluent_server/confluent/discovery/handlers/generic.py b/confluent_server/confluent/discovery/handlers/generic.py index aca0f864..0d41d366 100644 --- a/confluent_server/confluent/discovery/handlers/generic.py +++ b/confluent_server/confluent/discovery/handlers/generic.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +import confluent.util as util import errno import eventlet import socket @@ -31,6 +32,16 @@ class NodeHandler(object): self.info = info self.configmanager = configmanager targsa = [None] + self.ipaddr = None + self.relay_url = None + self.relay_server = None + self.web_ip = None + self.web_port = None + # if this is a remote registered component, prefer to use the agent forwarder + if info.get('forwarder_url', False): + self.relay_url = info['forwarder_url'] + self.relay_server = info['forwarder_server'] + return # first let us prefer LLA if possible, since that's most stable for sa in info['addresses']: if sa[0].startswith('fe80'): @@ -103,11 +114,8 @@ class NodeHandler(object): def https_cert(self): if self._fp: return self._fp - if ':' in self.ipaddr: - ip = '[{0}]'.format(self.ipaddr) - else: - ip = self.ipaddr - wc = webclient.SecureHTTPConnection(ip, verifycallback=self._savecert) + ip, port = self.get_web_port_and_ip() + wc = webclient.SecureHTTPConnection(ip, verifycallback=self._savecert, port=port) try: wc.connect() except IOError as ie: @@ -123,3 +131,32 @@ class NodeHandler(object): self._certfailreason = 2 return None return self._fp + + def get_web_port_and_ip(self): + if self.web_ip: + return self.web_ip, self.web_port + # get target ip and port, either direct or relay as applicable + if self.relay_url: + kv = util.TLSCertVerifier(self.configmanager, self.relay_server, + 'pubkeys.tls_hardwaremanager').verify_cert + w = webclient.SecureHTTPConnection(self.relay_server, verifycallback=kv) + relaycreds = self.configmanager.get_node_attributes(self.relay_server, 'secret.*', decrypt=True) + relaycreds = relaycreds.get(self.relay_server, {}) + relayuser = relaycreds.get('secret.hardwaremanagementuser', {}).get('value', None) + relaypass = relaycreds.get('secret.hardwaremanagementpassword', {}).get('value', None) + if not relayuser or not relaypass: + raise Exception('No credentials for {0}'.format(self.relay_server)) + w.set_basic_credentials(relayuser, relaypass) + w.connect() + w.request('GET', self.relay_url) + r = w.getresponse() + rb = r.read() + if r.code != 302: + raise Exception('Unexpected return from forwarder') + newurl = r.getheader('Location') + self.web_port = int(newurl.rsplit(':', 1)[-1][:-1]) + self.web_ip = self.relay_server + else: + self.web_port = 443 + self.web_ip = self.ipaddr + return self.web_ip, self.web_port diff --git a/confluent_server/confluent/discovery/handlers/smm.py b/confluent_server/confluent/discovery/handlers/smm.py index 6386812f..790ad131 100644 --- a/confluent_server/confluent/discovery/handlers/smm.py +++ b/confluent_server/confluent/discovery/handlers/smm.py @@ -135,7 +135,8 @@ class NodeHandler(bmchandler.NodeHandler): {nodename: {'hardwaremanagement.manager': self.ipaddr}}) def _webconfigcreds(self, username, password): - wc = webclient.SecureHTTPConnection(self.ipaddr, 443, verifycallback=self._validate_cert) + ip, port = self.get_web_port_and_ip() + wc = webclient.SecureHTTPConnection(ip, port, verifycallback=self._validate_cert) wc.connect() authdata = { # start by trying factory defaults 'user': 'USERID', diff --git a/confluent_server/confluent/discovery/handlers/xcc.py b/confluent_server/confluent/discovery/handlers/xcc.py index 32fdb63a..5c236b03 100644 --- a/confluent_server/confluent/discovery/handlers/xcc.py +++ b/confluent_server/confluent/discovery/handlers/xcc.py @@ -67,7 +67,8 @@ class NodeHandler(immhandler.NodeHandler): return None def scan(self): - c = webclient.SecureHTTPConnection(self.ipaddr, 443, + ip, port = self.get_web_port_and_ip() + c = webclient.SecureHTTPConnection(ip, port, verifycallback=self.validate_cert) i = c.grab_json_response('/api/providers/logoninfo') modelname = i.get('items', [{}])[0].get('machine_name', None) @@ -279,8 +280,9 @@ class NodeHandler(immhandler.NodeHandler): isdefault = True errinfo = {} if self._wc is None: + ip, port = self.get_web_port_and_ip() self._wc = webclient.SecureHTTPConnection( - self.ipaddr, 443, verifycallback=self.validate_cert) + ip, port, verifycallback=self.validate_cert) self._wc.connect() nodename = None if self.nodename: diff --git a/confluent_server/confluent/discovery/protocols/ssdp.py b/confluent_server/confluent/discovery/protocols/ssdp.py index 762b643a..85b5d7f1 100644 --- a/confluent_server/confluent/discovery/protocols/ssdp.py +++ b/confluent_server/confluent/discovery/protocols/ssdp.py @@ -382,10 +382,12 @@ def _find_service(service, target): if pi is not None: yield pi -def check_fish(urldata): +def check_fish(urldata, port=443, verifycallback=None): + if not verifycallback: + verifycallback = lambda x: True url, data = urldata try: - wc = webclient.SecureHTTPConnection(_get_svrip(data), 443, verifycallback=lambda x: True) + wc = webclient.SecureHTTPConnection(_get_svrip(data), port, verifycallback=verifycallback) peerinfo = wc.grab_json_response(url) except socket.error: return None diff --git a/confluent_server/confluent/messages.py b/confluent_server/confluent/messages.py index 6f0c3c25..7c6651cd 100644 --- a/confluent_server/confluent/messages.py +++ b/confluent_server/confluent/messages.py @@ -1600,6 +1600,7 @@ class Disk(ConfluentMessage): 'hotspare', 'rebuilding', 'online', + 'offline', ]) state_aliases = { 'unconfigured bad': 'fault', diff --git a/confluent_server/confluent/selfservice.py b/confluent_server/confluent/selfservice.py index c3548cb0..195c2f72 100644 --- a/confluent_server/confluent/selfservice.py +++ b/confluent_server/confluent/selfservice.py @@ -10,6 +10,7 @@ import eventlet.green.socket as socket import eventlet.green.subprocess as subprocess import confluent.discovery.handlers.xcc as xcc import confluent.discovery.handlers.tsm as tsm +import confluent.discovery.core as disco import base64 import hmac import hashlib @@ -18,6 +19,10 @@ import json import os import time import yaml +import confluent.discovery.protocols.ssdp as ssdp +import eventlet +webclient = eventlet.import_patched('pyghmi.util.webclient') + currtz = None keymap = 'us' @@ -112,7 +117,6 @@ def handle_request(env, start_response): start_response('401', []) yield 'Unauthorized' return - ea = cfg.get_node_attributes(nodename, ['crypted.selfapikey', 'deployment.apiarmed']) eak = ea.get( nodename, {}).get('crypted.selfapikey', {}).get('hashvalue', None) @@ -152,6 +156,42 @@ def handle_request(env, start_response): operation = env['REQUEST_METHOD'] if operation not in ('HEAD', 'GET') and 'CONTENT_LENGTH' in env and int(env['CONTENT_LENGTH']) > 0: reqbody = env['wsgi.input'].read(int(env['CONTENT_LENGTH'])) + if env['PATH_INFO'] == '/self/register_discovered': + rb = json.loads(reqbody) + if not rb.get('path', None): + start_response('400 Bad Requst', []) + yield 'Missing Path' + return + targurl = '/hubble/systems/by-port/{0}/webaccess'.format(rb['path']) + tlsverifier = util.TLSCertVerifier(cfg, nodename, 'pubkeys.tls_hardwaremanager') + wc = webclient.SecureHTTPConnection(nodename, 443, verifycallback=tlsverifier.verify_cert) + relaycreds = cfg.get_node_attributes(nodename, 'secret.*', decrypt=True) + relaycreds = relaycreds.get(nodename, {}) + relayuser = relaycreds.get('secret.hardwaremanagementuser', {}).get('value', None) + relaypass = relaycreds.get('secret.hardwaremanagementpassword', {}).get('value', None) + if not relayuser or not relaypass: + raise Exception('No credentials for {0}'.format(nodename)) + wc.set_basic_credentials(relayuser, relaypass) + wc.request('GET', targurl) + rsp = wc.getresponse() + _ = rsp.read() + if rsp.status == 302: + newurl = rsp.headers['Location'] + newhost, newport = newurl.replace('https://', '').split('/')[0].split(':') + def verify_cert(certificate): + hashval = base64.b64decode(rb['fingerprint']) + if len(hashval) == 48: + return hashlib.sha384(certificate).digest() == hashval + raise Exception('Certificate validation failed') + rb['addresses'] = [(newhost, newport)] + rb['forwarder_url'] = targurl + rb['forwarder_server'] = nodename + rb[''] + ssdp.check_fish(('/DeviceDescription.json', rb), newport, verify_cert) + disco.detected(rb) + start_response('200 OK', []) + yield 'Registered' + return if env['PATH_INFO'] == '/self/bmcconfig': hmattr = cfg.get_node_attributes(nodename, 'hardwaremanagement.*') hmattr = hmattr.get(nodename, {}) diff --git a/confluent_server/confluent/util.py b/confluent_server/confluent/util.py index 52726302..35b1e08e 100644 --- a/confluent_server/confluent/util.py +++ b/confluent_server/confluent/util.py @@ -146,12 +146,24 @@ def get_fingerprint(certificate, algo='sha512'): return 'sha256$' + hashlib.sha256(certificate).hexdigest() elif algo == 'sha512': return 'sha512$' + hashlib.sha512(certificate).hexdigest() + elif algo == 'sha384': + return 'sha384$' + hashlib.sha384(certificate).hexdigest() raise Exception('Unsupported fingerprint algorithm ' + algo) +hashlens = { + 48: hashlib.sha384, + 64: hashlib.sha512, + 32: hashlib.sha256 +} + def cert_matches(fingerprint, certificate): if not fingerprint or not certificate: return False + if '$' not in fingerprint: + fingerprint = base64.b64decode(certificate) + algo = hashlens[len(fingerprint)] + return algo(certificate).digest() == fingerprint algo, _, fp = fingerprint.partition('$') newfp = None if algo in ('sha512', 'sha256'):