mirror of
https://github.com/xcat2/confluent.git
synced 2026-06-10 01:53:09 +00:00
636 lines
29 KiB
Python
636 lines
29 KiB
Python
import asyncio
|
|
import confluent.runansible as runansible
|
|
import confluent.syncfiles as syncfiles
|
|
import confluent.config.configmanager as configmanager
|
|
import confluent.collective.manager as collective
|
|
import confluent.netutil as netutil
|
|
import confluent.noderange as noderange
|
|
import confluent.sshutil as sshutil
|
|
import confluent.util as util
|
|
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
|
|
import crypt
|
|
import json
|
|
import os
|
|
import time
|
|
import yaml
|
|
try:
|
|
from yaml import CSafeDumper as SafeDumper
|
|
from yaml import CSafeLoader as SafeLoader
|
|
except ImportError:
|
|
from yaml import SafeLoader
|
|
from yaml import SafeDumper
|
|
import confluent.discovery.protocols.ssdp as ssdp
|
|
import subprocess
|
|
import aiohmi.util.webclient as webclient
|
|
|
|
|
|
currtz = 'UTC'
|
|
keymap = 'us'
|
|
currlocale = 'en_US.UTF-8'
|
|
currtzvintage = None
|
|
|
|
|
|
def yamldump(input):
|
|
return yaml.dump_all([input], Dumper=SafeDumper, default_flow_style=False)
|
|
|
|
def yamlload(input):
|
|
return yaml.load(input, Loader=SafeLoader)
|
|
|
|
def listdump(input):
|
|
# special case yaml for flat dumb list
|
|
# this is about 25x faster than doing full yaml dump even with CSafeDumper
|
|
# with a 17,000 element list
|
|
retval = ''
|
|
for entry in input:
|
|
retval += '- ' + entry + '\n'
|
|
return retval.encode()
|
|
|
|
|
|
async def get_extra_names(nodename, cfg, myip=None, preferadjacent=False, addlocalhost=True):
|
|
if addlocalhost:
|
|
names = set(['127.0.0.1', '::1', 'localhost', 'localhost.localdomain'])
|
|
else:
|
|
names = set([])
|
|
dnsinfo = cfg.get_node_attributes(nodename, ('dns.*', 'net.*hostname'))
|
|
dnsinfo = dnsinfo.get(nodename, {})
|
|
domain = dnsinfo.get('dns.domain', {}).get('value', None)
|
|
if domain and domain not in nodename:
|
|
names.add('{0}.{1}'.format(nodename, domain))
|
|
for keyname in dnsinfo:
|
|
if keyname.endswith('hostname'):
|
|
currnames = dnsinfo[keyname].get('value', None)
|
|
if currnames:
|
|
currnames = currnames.split(',')
|
|
for currname in currnames:
|
|
names.add(currname)
|
|
if domain and domain not in currname:
|
|
names.add('{0}.{1}'.format(currname, domain))
|
|
if myip:
|
|
ncfgs = [await netutil.get_nic_config(cfg, nodename, serverip=myip)]
|
|
fncfg = await netutil.get_full_net_config(cfg, nodename, serverip=myip)
|
|
ncfgs.append(fncfg.get('default', {}))
|
|
for ent in fncfg.get('extranets', []):
|
|
ncfgs.append(fncfg['extranets'][ent])
|
|
addall = True
|
|
routedaddrs = set([])
|
|
for ncfg in ncfgs:
|
|
for nip in (ncfg.get('ipv4_address', None), ncfg.get('ipv6_address', None)):
|
|
if nip:
|
|
nip = nip.split('/', 1)[0]
|
|
if not preferadjacent or await netutil.address_is_local(nip):
|
|
names.add(nip)
|
|
addall = False
|
|
else:
|
|
routedaddrs.add(nip)
|
|
if addall:
|
|
names.update(routedaddrs)
|
|
return names
|
|
|
|
async def handle_request(req, make_response, mimetype):
|
|
global currtz
|
|
global keymap
|
|
global currlocale
|
|
global currtzvintage
|
|
configmanager.check_quorum()
|
|
cfg = configmanager.ConfigManager(None)
|
|
reqpath = req.rel_url.path
|
|
nodename = req.headers.get('CONFLUENT_NODENAME', None)
|
|
clientip = req.headers.get('X-Forwarded-For', None)
|
|
if reqpath == '/self/whoami':
|
|
clientids = env.get('HTTP_CONFLUENT_IDS', None)
|
|
if not clientids:
|
|
rsp = await make_response(mimetype, 400, 'Bad Request')
|
|
await rsp.write(b'Bad Request')
|
|
return rsp
|
|
for ids in clientids.split('/'):
|
|
_, v = ids.split('=', 1)
|
|
repname = disco.get_node_by_uuid_or_mac(v)
|
|
if repname:
|
|
rsp = await make_response(mimetype, 200, 'OK')
|
|
await rsp.write(repname.encode())
|
|
return rsp
|
|
rsp = await make_response(mimetype, 404, 'Unknown')
|
|
return rsp
|
|
if reqpath == '/self/registerapikey':
|
|
crypthmac = env.get('HTTP_CONFLUENT_CRYPTHMAC', None)
|
|
if int(env.get('CONTENT_LENGTH', 65)) > 64:
|
|
rsp = await make_response(mimetype, 400, 'Bad Request')
|
|
await rsp.write('Bad Request')
|
|
return rsp
|
|
cryptkey = await req.read()
|
|
if not (crypthmac and cryptkey):
|
|
return await make_response(mimetype, 401, 'Unauthorized', body='Unauthorized')
|
|
hmackey = cfg.get_node_attributes(nodename, ['secret.selfapiarmtoken'], decrypt=True)
|
|
hmackey = hmackey.get(nodename, {}).get('secret.selfapiarmtoken', {}).get('value', None)
|
|
if not hmackey:
|
|
return await make_response(mimetype, 401, 'Unauthorized', body='Unauthorized')
|
|
if not isinstance(hmackey, bytes):
|
|
hmackey = hmackey.encode('utf8')
|
|
if not isinstance(cryptkey, bytes):
|
|
cryptkey = cryptkey.encode('utf8')
|
|
try:
|
|
crypthmac = base64.b64decode(crypthmac)
|
|
except Exception:
|
|
return await make_response(mimetype, 400, 'Bad Request', body='Bad Request')
|
|
righthmac = hmac.new(hmackey, cryptkey, hashlib.sha256).digest()
|
|
if righthmac == crypthmac:
|
|
if not isinstance(cryptkey, str):
|
|
cryptkey = cryptkey.decode()
|
|
cfgupdate = {nodename: {'crypted.selfapikey': {'hashvalue': cryptkey}}}
|
|
await cfg.set_node_attributes(cfgupdate)
|
|
await cfg.clear_node_attributes([nodename], ['secret.selfapiarmtoken'])
|
|
return await make_response(mimetype, 200, 'OK', body='Accepted')
|
|
return await make_response(mimetype, 401, 'Unauthorized', body='Unauthorized')
|
|
apikey = req.headers.get('CONFLUENT_APIKEY', None)
|
|
if not (nodename and apikey):
|
|
return await make_response(mimetype, 401, 'Unauthorized', body='Unauthorized')
|
|
if len(apikey) > 48:
|
|
return await make_response(mimetype, 401, 'Unauthorized', body='Unauthorized')
|
|
return rsp
|
|
ea = cfg.get_node_attributes(nodename, ['crypted.selfapikey', 'deployment.apiarmed'])
|
|
eak = ea.get(
|
|
nodename, {}).get('crypted.selfapikey', {}).get('hashvalue', None)
|
|
if not eak:
|
|
return await make_response(mimetype, 401, 'Unauthorized', body='Unauthorized')
|
|
if not isinstance(eak, str):
|
|
eak = eak.decode('utf8')
|
|
salt = '$'.join(eak.split('$', 3)[:-1]) + '$'
|
|
if crypt.crypt(apikey, salt) != eak:
|
|
return await make_response(mimetype, 401, 'Unauthorized', body='Unauthorized')
|
|
if ea.get(nodename, {}).get('deployment.apiarmed', {}).get('value', None) == 'once':
|
|
await cfg.set_node_attributes({nodename: {'deployment.apiarmed': ''}})
|
|
myip = req.headers.get('X-Forwarded-Host', None)
|
|
if myip and ']' in myip:
|
|
myip = myip.split(']', 1)[0]
|
|
elif myip:
|
|
myip = myip.split(':', 1)[0]
|
|
if myip:
|
|
myip = myip.replace('[', '').replace(']', '')
|
|
retype = req.headers.get('Accept', 'application/yaml')
|
|
isgeneric = False
|
|
if retype == '*/*':
|
|
isgeneric = True
|
|
retype = 'application/yaml'
|
|
if retype == 'application/yaml':
|
|
dumper = yamldump
|
|
elif retype == 'application/json':
|
|
dumper = json.dumps
|
|
else:
|
|
return await make_response(mimetype, 406, 'Not supported', body='Unsupported content type in ACCEPT: ' + retype)
|
|
operation = req.method
|
|
if operation not in ('HEAD', 'GET') and req.content_length > 0:
|
|
reqbody = await req.read()
|
|
if reqpath == '/self/register_discovered':
|
|
rb = json.loads(reqbody)
|
|
if not rb.get('path', None):
|
|
return await make_response(mimetype, 400, 'Bad Request', body='Missing Path')
|
|
targurl = '/affluent/systems/by-port/{0}/webaccess'.format(rb['path'])
|
|
tlsverifier = util.TLSCertVerifier(cfg, nodename, 'pubkeys.tls_hardwaremanager')
|
|
wc = webclient.WebConnection(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)
|
|
rsp, status = await wc.grab_json_response_with_status(targurl)
|
|
if 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
|
|
if 'bay' in rb:
|
|
rb['enclosure.bay'] = rb['bay']
|
|
if rb['type'] == 'lenovo-xcc':
|
|
await ssdp.check_fish(('/DeviceDescription.json', rb), newport, verify_cert)
|
|
elif rb['type'] == 'lenovo-smm2':
|
|
rb['services'] = ['service:lenovo-smm2']
|
|
else:
|
|
return await make_response(mimetype, 400, 'Unsupported Device', body='Unsupported device for remote discovery registration')
|
|
await disco.detected(rb)
|
|
return await make_response(mimetype, 200, 'OK', body='Registered')
|
|
if reqpath == '/self/bmcconfig':
|
|
hmattr = cfg.get_node_attributes(nodename, 'hardwaremanagement.*')
|
|
hmattr = hmattr.get(nodename, {})
|
|
res = {}
|
|
port = hmattr.get('hardwaremanagement.port', {}).get('value', None)
|
|
if port is not None:
|
|
res['bmcport'] = port
|
|
vlan = hmattr.get('hardwaremanagement.vlan', {}).get('value', None)
|
|
if vlan is not None:
|
|
res['bmcvlan'] = vlan
|
|
bmcaddr = hmattr.get('hardwaremanagement.manager', {}).get('value',
|
|
None)
|
|
if not bmcaddr:
|
|
return await make_response(mimetype, 500, 'Internal Server Error', body='Missing value in hardwaremanagement.manager')
|
|
bmcaddr = bmcaddr.split('/', 1)[0]
|
|
bmcaddr = await asyncio.get_running_loop().getaddrinfo(bmcaddr, 0)[0]
|
|
bmcaddr = bmcaddr[-1][0]
|
|
if '.' in bmcaddr: # ipv4 is allowed
|
|
netconfig = await netutil.get_nic_config(cfg, nodename, ip=bmcaddr)
|
|
res['bmcipv4'] = bmcaddr
|
|
res['prefixv4'] = netconfig['prefix']
|
|
res['bmcgw'] = netconfig.get('ipv4_gateway', None)
|
|
# credential security results in user/password having to be deferred
|
|
return await make_response(mimetype, 200, 'OK', body=dumper(res))
|
|
elif reqpath == '/self/myattribs':
|
|
cfd = cfg.get_node_attributes(nodename, '*', decrypt=True).get(nodename, {})
|
|
rsp = {}
|
|
for k in cfd:
|
|
if k.startswith('secret') or k.startswith('crypt') or 'value' not in cfd[k] or not cfd[k]['value']:
|
|
continue
|
|
rsp[k] = cfd[k]['value']
|
|
if isinstance(rsp[k], bytes):
|
|
rsp[k] = rsp[k].decode()
|
|
return await make_response(mimetype, 200, 'OK', body=dumper(rsp))
|
|
elif reqpath == '/self/netcfg':
|
|
ncfg = await netutil.get_full_net_config(cfg, nodename, myip)
|
|
return await make_response(mimetype, 200, 'OK', body=dumper(ncfg))
|
|
elif reqpath in ('/self/deploycfg', '/self/deploycfg2'):
|
|
if 'CONFLUENT_MGTIFACE' in req.headers:
|
|
nicname = req.headers['CONFLUENT_MGTIFACE']
|
|
try:
|
|
ifidx = int(nicname)
|
|
except ValueError:
|
|
with open('/sys/class/net/{}/ifindex'.format(nicname), 'r') as nici:
|
|
ifidx = int(nici.read())
|
|
ncfg = await netutil.get_nic_config(cfg, nodename, ifidx=ifidx)
|
|
else:
|
|
ncfg = await netutil.get_nic_config(cfg, nodename, serverip=myip, clientip=clientip)
|
|
if reqpath == '/self/deploycfg':
|
|
for key in list(ncfg):
|
|
if 'v6' in key:
|
|
del ncfg[key]
|
|
if ncfg['prefix']:
|
|
ncfg['ipv4_netmask'] = netutil.cidr_to_mask(ncfg['prefix'])
|
|
if ncfg['ipv4_method'] == 'firmwaredhcp':
|
|
ncfg['ipv4_method'] = 'static'
|
|
deployinfo = cfg.get_node_attributes(
|
|
nodename, ('deployment.*', 'console.method', 'crypted.*',
|
|
'dns.*', 'ntp.*'))
|
|
deployinfo = deployinfo.get(nodename, {})
|
|
profile = deployinfo.get(
|
|
'deployment.pendingprofile', {}).get('value', '')
|
|
ncfg['encryptboot'] = deployinfo.get('deployment.encryptboot', {}).get(
|
|
'value', None)
|
|
if ncfg['encryptboot'] in ('', 'none'):
|
|
ncfg['encryptboot'] = None
|
|
ncfg['profile'] = profile
|
|
protocol = deployinfo.get('deployment.useinsecureprotocols', {}).get(
|
|
'value', 'never')
|
|
ncfg['confluent_uuid'] = configmanager.get_global('confluent_uuid')
|
|
ncfg['textconsole'] = bool(deployinfo.get(
|
|
'console.method', {}).get('value', None))
|
|
if protocol == 'always':
|
|
ncfg['protocol'] = 'http'
|
|
else:
|
|
ncfg['protocol'] = 'https'
|
|
ncfg['rootpassword'] = deployinfo.get('crypted.rootpassword', {}).get(
|
|
'hashvalue', None)
|
|
ncfg['grubpassword'] = deployinfo.get('crypted.grubpassword', {}).get(
|
|
'grubhashvalue', None)
|
|
if currtzvintage and currtzvintage > (time.time() - 30.0):
|
|
ncfg['timezone'] = currtz
|
|
else:
|
|
needlocalectl = True
|
|
try:
|
|
with open('/etc/vconsole.conf') as consconf:
|
|
for line in consconf.read().split('\n'):
|
|
line = line.split('#', 1)[0]
|
|
if '=' not in line:
|
|
continue
|
|
k, v = line.split('=', 1)
|
|
if k == 'KEYMAP':
|
|
keymap = v.replace('"', '')
|
|
needlocalectl = False
|
|
if not needlocalectl:
|
|
needlocalectl = True
|
|
localeconf = None
|
|
if os.path.exists('/etc/locale.conf'):
|
|
localeconf = '/etc/locale.conf'
|
|
elif os.path.exists('/etc/default/locale'):
|
|
localeconf = '/etc/default/locale'
|
|
if localeconf:
|
|
with open(localeconf) as lcin:
|
|
for line in lcin.read().split('\n'):
|
|
line = line.split('#', 1)[0]
|
|
if '=' not in line:
|
|
continue
|
|
k, v = line.split('=', 1)
|
|
if k == 'LANG':
|
|
needlocalectl = False
|
|
currlocale = v.replace('"', '')
|
|
except IOError:
|
|
pass
|
|
if needlocalectl:
|
|
try:
|
|
langinfo = (await util.check_output('localectl', 'status'))[0].split(b'\n')
|
|
except Exception:
|
|
langinfo = []
|
|
for line in langinfo:
|
|
line = line.strip()
|
|
if line.startswith(b'System Locale:'):
|
|
ccurrlocale = line.split(b'=')[-1]
|
|
if not ccurrlocale:
|
|
continue
|
|
if not isinstance(ccurrlocale, str):
|
|
ccurrlocale = ccurrlocale.decode('utf8')
|
|
if ccurrlocale == 'n/a':
|
|
continue
|
|
currlocale = ccurrlocale
|
|
elif line.startswith(b'VC Keymap:'):
|
|
ckeymap = line.split(b':')[-1]
|
|
ckeymap = ckeymap.strip()
|
|
if not ckeymap:
|
|
continue
|
|
if not isinstance(ckeymap, str):
|
|
ckeymap = ckeymap.decode('utf8')
|
|
if ckeymap == 'n/a':
|
|
continue
|
|
keymap = ckeymap
|
|
try:
|
|
tdcout, tdcerr = await util.check_output('timedatectl')
|
|
tdc = tdcout.split(b'\n')
|
|
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
tdc = []
|
|
currtzvintage = time.time()
|
|
ncfg['timezone'] = currtz
|
|
for ent in tdc:
|
|
ent = ent.strip()
|
|
if ent.startswith(b'Time zone:'):
|
|
currtz = ent.split(b': ', 1)[1].split(b'(', 1)[0].strip()
|
|
if not isinstance(currtz, str):
|
|
currtz = currtz.decode('utf8')
|
|
currtzvintage = time.time()
|
|
ncfg['timezone'] = currtz
|
|
break
|
|
ncfg['locale'] = currlocale
|
|
ncfg['keymap'] = keymap
|
|
ncfg['nameservers'] = []
|
|
for dns in deployinfo.get(
|
|
'dns.servers', {}).get('value', '').split(','):
|
|
ncfg['nameservers'].append(dns)
|
|
ntpsrvs = deployinfo.get('ntp.servers', {}).get('value', '')
|
|
if ntpsrvs:
|
|
ntpsrvs = ntpsrvs.split(',')
|
|
if ntpsrvs:
|
|
ncfg['ntpservers'] = []
|
|
for ntpsrv in ntpsrvs:
|
|
ncfg['ntpservers'].append(ntpsrv)
|
|
dnsdomain = deployinfo.get('dns.domain', {}).get('value', None)
|
|
ncfg['dnsdomain'] = dnsdomain
|
|
return await make_response(mimetype, 200, 'OK', body=dumper(ncfg))
|
|
elif reqpath == '/self/sshcert' and reqbody:
|
|
if not sshutil.ca_exists():
|
|
return await make_response(mimetype, 500, 'Unconfigured', body='CA is not configured on this system (run ...)')
|
|
pals = await get_extra_names(nodename, cfg, myip)
|
|
cert = await sshutil.sign_host_key(reqbody, nodename, pals)
|
|
return await make_response('text/plain', 200, 'OK', body=cert.encode())
|
|
elif reqpath == '/self/nodelist':
|
|
nodes, _ = await get_cluster_list(nodename, cfg)
|
|
if isgeneric:
|
|
mrsp = await make_response('text/plain', 200, 'OK')
|
|
for node in util.natural_sort(nodes):
|
|
await mrsp.write(f'{node}\n'.encode('utf-8'))
|
|
else:
|
|
mrsp = await make_response(retype, 200, 'OK')
|
|
if retype == 'application/yaml':
|
|
await mrsp.write(listdump(list(util.natural_sort(nodes))))
|
|
else:
|
|
await mrsp.write(dumper(list(util.natural_sort(nodes))))
|
|
return mrsp
|
|
elif reqpath == '/self/remoteconfigbmc' and reqbody:
|
|
try:
|
|
reqbody = yamlload(reqbody)
|
|
except Exception:
|
|
reqbody = None
|
|
cfgmod = reqbody.get('configmod', 'unspecified')
|
|
if cfgmod == 'xcc':
|
|
xcc.remote_nodecfg(nodename, cfg)
|
|
elif cfgmod == 'tsm':
|
|
tsm.remote_nodecfg(nodename, cfg)
|
|
else:
|
|
return await make_response(mimetype, 500, 'unsupported configmod', body='Unsupported configmod "{}"'.format(cfgmod))
|
|
return await make_response(mimetype, 200, 'Ok', body='complete')
|
|
elif reqpath == '/self/updatestatus' and reqbody:
|
|
update = yamlload(reqbody)
|
|
statusstr = update.get('state', None)
|
|
statusdetail = update.get('state_detail', None)
|
|
didstateupdate = False
|
|
if statusstr or 'status' in update:
|
|
await cfg.set_node_attributes({nodename: {
|
|
'deployment.client_ip': {'value': clientip}}})
|
|
if statusstr:
|
|
await cfg.set_node_attributes({nodename: {'deployment.state': statusstr}})
|
|
didstateupdate = True
|
|
if statusdetail:
|
|
await cfg.set_node_attributes({nodename: {'deployment.state_detail': statusdetail}})
|
|
didstateupdate = True
|
|
if 'status' not in update and didstateupdate:
|
|
mrsp = await make_response(mimetype, 200, 'Ok')
|
|
await mrsp.write(b'Accepted')
|
|
return
|
|
if update['status'] == 'staged':
|
|
targattr = 'deployment.stagedprofile'
|
|
elif update['status'] == 'complete':
|
|
targattr = 'deployment.profile'
|
|
else:
|
|
raise Exception('Unknown update status request')
|
|
currattr = cfg.get_node_attributes(nodename, 'deployment.*').get(
|
|
nodename, {})
|
|
pending = None
|
|
if targattr == 'deployment.profile':
|
|
pending = currattr.get('deployment.stagedprofile', {}).get('value', '')
|
|
if not pending:
|
|
pending = currattr.get('deployment.pendingprofile', {}).get('value', '')
|
|
updates = {}
|
|
if pending:
|
|
updates['deployment.pendingprofile'] = {'value': ''}
|
|
if targattr == 'deployment.profile':
|
|
updates['deployment.stagedprofile'] = {'value': ''}
|
|
dls = cfg.get_node_attributes(nodename, 'deployment.lock')
|
|
dls = dls.get(nodename, {}).get('deployment.lock', {}).get('value', None)
|
|
if dls == 'autolock':
|
|
updates['deployment.lock'] = 'locked'
|
|
currprof = currattr.get(targattr, {}).get('value', '')
|
|
if currprof != pending:
|
|
updates[targattr] = {'value': pending}
|
|
await cfg.set_node_attributes({nodename: updates})
|
|
return await make_response('text/plain', 200, 'OK', body='OK')
|
|
else:
|
|
return await make_response('text/plain', 500, 'Error', body='No pending profile detected, unable to accept status update')
|
|
elif reqpath == '/self/saveapikey' and reqbody:
|
|
if not isinstance(reqbody, str):
|
|
reqbody = reqbody.decode('utf8')
|
|
await cfg.set_node_attributes({
|
|
nodename: {'deployment.sealedapikey': {'value': reqbody}}})
|
|
return await make_response(mimetype, 200, 'OK', body='OK')
|
|
elif reqpath.startswith('/self/remoteconfig/') and 'POST' == operation:
|
|
scriptcat = reqpath.replace('/self/remoteconfig/', '')
|
|
playlist = []
|
|
for privacy in ('public', 'private'):
|
|
slist, profile = get_scriptlist(
|
|
scriptcat, cfg, nodename,
|
|
'/var/lib/confluent/{0}/os/{{0}}/ansible/{{1}}'.format(privacy))
|
|
dirname = '/var/lib/confluent/{2}/os/{0}/ansible/{1}/'.format(
|
|
profile, scriptcat, privacy)
|
|
if not os.path.isdir(dirname):
|
|
dirname = '/var/lib/confluent/{2}/os/{0}/ansible/{1}.d/'.format(
|
|
profile, scriptcat, privacy)
|
|
for filename in slist:
|
|
if filename.endswith('.yaml') or filename.endswith('.yml'):
|
|
playlist.append(os.path.join(dirname, filename))
|
|
if playlist:
|
|
await runansible.run_playbooks(playlist, [nodename])
|
|
return await make_response(mimetype, 202, 'Queued', body='Queued')
|
|
else:
|
|
return await make_response(mimetype, 200, 'OK', body='OK')
|
|
elif reqpath.startswith('/self/remotesyncfiles'):
|
|
if 'POST' == operation:
|
|
pals = await get_extra_names(nodename, cfg, myip, preferadjacent=True, addlocalhost=False)
|
|
if clientip in pals:
|
|
pals = [clientip]
|
|
result = syncfiles.start_syncfiles(
|
|
nodename, cfg, json.loads(reqbody), pals)
|
|
return await make_response(mimetype, result[0], result[1], body=result[2])
|
|
if 'GET' == operation:
|
|
statcode, status, output = syncfiles.get_syncresult(nodename)
|
|
output = json.dumps(output)
|
|
return await make_response('application/json', statcode, status, body=output)
|
|
elif reqpath.startswith('/self/remoteconfig/status'):
|
|
rst = runansible.running_status.get(nodename, None)
|
|
if not rst:
|
|
return await make_response(mimetype, 204, 'Not Running')
|
|
mrsp = await make_response(mimetype, 200, 'OK')
|
|
if rst.complete:
|
|
del runansible.running_status[nodename]
|
|
await mrsp.write(rst.dump_text().encode('utf8'))
|
|
return mrsp
|
|
elif reqpath.startswith('/self/scriptlist/'):
|
|
scriptcat = reqpath.replace('/self/scriptlist/', '')
|
|
slist, _ = get_scriptlist(
|
|
scriptcat, cfg, nodename,
|
|
'/var/lib/confluent/public/os/{0}/scripts/{1}')
|
|
if slist:
|
|
mrsp = await make_response('application/yaml', 200, 'OK')
|
|
await mrsp.write(yamldump(util.natural_sort(slist)))
|
|
else:
|
|
mrsp = await make_response(mimetype, 200, 'OK')
|
|
return mrsp
|
|
elif reqpath.startswith('/self/profileprivate/pending/'):
|
|
fname = reqpath.replace('/self/profileprivate/', '')
|
|
deployinfo = cfg.get_node_attributes(
|
|
nodename, ('deployment.*',))
|
|
deployinfo = deployinfo.get(nodename, {})
|
|
profile = deployinfo.get(
|
|
'deployment.pendingprofile', {}).get('value', '')
|
|
if not profile:
|
|
return await make_response(mimetype, 400, 'No pending profile', body='No profile')
|
|
fname = '/var/lib/confluent/private/os/{}/{}'.format(profile, fname)
|
|
fullpath = os.path.abspath(fname)
|
|
if not fullpath.startswith('/var/lib/confluent/private/os/{}/'.format(profile)):
|
|
return await make_response(mimetype, 400, 'Bad Request', body='Bad Request')
|
|
try:
|
|
with open(fname, 'rb') as privdata:
|
|
return await make_response(mimetype, 200, 'OK', body=privdata.read())
|
|
except IOError:
|
|
return await make_response(mimetype, 404, 'Not Found', body='Not found')
|
|
else:
|
|
return await make_response(mimetype, 404, 'Not Found', body='Not found')
|
|
|
|
|
|
def list_ansible_scripts(cfg, nodename, scriptcat):
|
|
playlist = []
|
|
for privacy in ('public', 'private'):
|
|
slist, profile = get_scriptlist(
|
|
scriptcat, cfg, nodename,
|
|
'/var/lib/confluent/{0}/os/{{0}}/ansible/{{1}}'.format(privacy))
|
|
dirname = '/var/lib/confluent/{2}/os/{0}/ansible/{1}/'.format(
|
|
profile, scriptcat, privacy)
|
|
if not os.path.isdir(dirname):
|
|
dirname = '/var/lib/confluent/{2}/os/{0}/ansible/{1}.d/'.format(
|
|
profile, scriptcat, privacy)
|
|
for filename in slist:
|
|
if filename.endswith('.yaml') or filename.endswith('.yml'):
|
|
playlist.append(os.path.join(dirname, filename))
|
|
return playlist
|
|
|
|
def get_scriptlist(scriptcat, cfg, nodename, pathtemplate):
|
|
if '..' in scriptcat:
|
|
return None, None
|
|
deployinfo = cfg.get_node_attributes(
|
|
nodename, ('deployment.*',))
|
|
deployinfo = deployinfo.get(nodename, {})
|
|
profile = deployinfo.get(
|
|
'deployment.pendingprofile', {}).get('value', '')
|
|
if not profile:
|
|
profile = deployinfo.get(
|
|
'deployment.stagedprofile', {}).get('value', '')
|
|
if not profile:
|
|
profile = deployinfo.get(
|
|
'deployment.profile', {}).get('value', '')
|
|
slist = []
|
|
target = pathtemplate.format(profile, scriptcat)
|
|
target = os.path.abspath(target)
|
|
allowedbase = os.path.abspath(pathtemplate.format(profile, '').rstrip('/'))
|
|
allowedbaseprefix = os.path.join(allowedbase, '')
|
|
if not target.startswith(allowedbaseprefix):
|
|
return None, None
|
|
if not os.path.isdir(target) and os.path.isdir(target + '.d'):
|
|
target = target + '.d'
|
|
try:
|
|
slist = list(util.natural_sort(os.listdir(target)))
|
|
except OSError:
|
|
pass
|
|
return slist, profile
|
|
|
|
|
|
async def get_cluster_list(nodename=None, cfg=None):
|
|
if cfg is None:
|
|
cfg = configmanager.ConfigManager(None)
|
|
nodes = None
|
|
if nodename is not None:
|
|
sshpeers = cfg.get_node_attributes(nodename, 'ssh.trustnodes')
|
|
sshpeers = sshpeers.get(nodename, {}).get('ssh.trustnodes', {}).get(
|
|
'value', None)
|
|
if sshpeers:
|
|
nodes = noderange.NodeRange(sshpeers, cfg).nodes
|
|
autonodes = False
|
|
if nodes is None:
|
|
autonodes = True
|
|
nodes = set(cfg.list_nodes())
|
|
domain = None
|
|
for node in list(util.natural_sort(nodes)):
|
|
if domain is None:
|
|
domaininfo = cfg.get_node_attributes(node, 'dns.domain')
|
|
domain = domaininfo.get(node, {}).get('dns.domain', {}).get(
|
|
'value', None)
|
|
for extraname in await get_extra_names(node, cfg):
|
|
nodes.add(extraname)
|
|
if autonodes:
|
|
for mgr in configmanager.list_collective():
|
|
nodes.add(mgr)
|
|
if domain and domain not in mgr:
|
|
nodes.add('{0}.{1}'.format(mgr, domain))
|
|
myname = collective.get_myname()
|
|
nodes.add(myname)
|
|
if domain and domain not in myname:
|
|
nodes.add('{0}.{1}'.format(myname, domain))
|
|
nodes.add('::1')
|
|
nodes.add('127.0.0.1')
|
|
nodes.add('localhost')
|
|
nodes.add('localhost.domain')
|
|
return nodes, domain
|