2
0
mirror of https://github.com/xcat2/confluent.git synced 2025-03-19 09:57:45 +00:00

Merge branch 'remote_discovery'

This commit is contained in:
Jarrod Johnson 2022-07-20 16:22:01 -04:00
commit 7751a71851
10 changed files with 135 additions and 20 deletions

View File

@ -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]

View File

@ -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 '

View File

@ -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)

View File

@ -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

View File

@ -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',

View File

@ -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:

View File

@ -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

View File

@ -1600,6 +1600,7 @@ class Disk(ConfluentMessage):
'hotspare',
'rebuilding',
'online',
'offline',
])
state_aliases = {
'unconfigured bad': 'fault',

View File

@ -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, {})

View File

@ -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'):