mirror of
https://github.com/xcat2/confluent.git
synced 2026-09-29 08:41:00 +00:00
Compare commits
190 Commits
ansibleplay
...
3.2
| Author | SHA1 | Date | |
|---|---|---|---|
| ce1d89c741 | |||
| cbee00d3bb | |||
| 14bb841b2d | |||
| 03a75ec67d | |||
| 269300119a | |||
| a2edf46b8a | |||
| 74e011e964 | |||
| 66372bea0c | |||
| d250a3e850 | |||
| 49b69a2e19 | |||
| 93d4847763 | |||
| b283b50cc6 | |||
| 1f320af8e5 | |||
| 5fea265dc8 | |||
| b74d648e7f | |||
| 380f21853c | |||
| 9900d76b0c | |||
| aa7701ea3c | |||
| 3044c662ad | |||
| 7e02d1d008 | |||
| f321e06e6e | |||
| 5c0af9ebc7 | |||
| c146b50b5b | |||
| cae8b2d0f8 | |||
| 2664adefa4 | |||
| c5f03ee62d | |||
| a932e91b17 | |||
| 1c00aa3c4e | |||
| bd889f4c78 | |||
| 409a0796a1 | |||
| 70ffef6b06 | |||
| 77a043faa6 | |||
| d69eb6ef12 | |||
| 984c3044fe | |||
| 8543129fb9 | |||
| 257edff3a7 | |||
| 763feda63b | |||
| 508e5b4a46 | |||
| 7676c4fd26 | |||
| c3b954a743 | |||
| a94a341582 | |||
| 08dfd55ea0 | |||
| cf6975ded3 | |||
| d17b1d060c | |||
| fcb4b33e8e | |||
| 121e0727cf | |||
| b5b9631b3f | |||
| c09f96b8c9 | |||
| 196ed31bee | |||
| bb05f72583 | |||
| 68b4f83627 | |||
| 672bc4f754 | |||
| 4df3fda8ea | |||
| f32619aa3e | |||
| dd7b9f0244 | |||
| def534e73f | |||
| 2cb641e734 | |||
| ca78b664c9 | |||
| 0e55d390e3 | |||
| 002146b2f7 | |||
| 917a51a406 | |||
| d3fa08d78b | |||
| 38eb88249e | |||
| c333c3eb9c | |||
| f32a9a2f08 | |||
| f584b9bc11 | |||
| affbbcc89b | |||
| 45cc4b7788 | |||
| 945f108548 | |||
| 1f9c440525 | |||
| 8397c5defc | |||
| b7af6b5c27 | |||
| c38aac88df | |||
| 6d1348f5fa | |||
| c05f18d865 | |||
| bf00c75f43 | |||
| 2e04647c95 | |||
| 479ddd582e | |||
| d82690f0d9 | |||
| 34cfd99fd8 | |||
| 9009f63b2d | |||
| ab6bf82da6 | |||
| 0fae615756 | |||
| 37dca72579 | |||
| 1aea406852 | |||
| 374b87e2d7 | |||
| 3469c8ab2b | |||
| e08ca5fb7e | |||
| 23dffe882e | |||
| b6fb91b228 | |||
| ac4d1fa8e1 | |||
| 1d902c4d90 | |||
| f11687998f | |||
| 153956b2cd | |||
| 64d5081be3 | |||
| 8d16b412ae | |||
| c5ec34d5a5 | |||
| 6a4086679c | |||
| 3d2422ded3 | |||
| 7d0f47bbcb | |||
| 9a779f2dd2 | |||
| 4724d9e45b | |||
| 8404ddf3a2 | |||
| 872a13589c | |||
| 433a22e6e7 | |||
| 1f238a3f8b | |||
| 670fc87e1d | |||
| 3be3d4a588 | |||
| 14d13749ad | |||
| 92a10ede11 | |||
| d9051e80d3 | |||
| 2d43fac1b5 | |||
| 4a0d419f85 | |||
| 7fb5f9756d | |||
| b482410072 | |||
| 4d47f58e53 | |||
| c5bb55e8f2 | |||
| 2320718618 | |||
| d64b3e6a4e | |||
| 637e0f0a81 | |||
| e26218eb92 | |||
| 03eb026b61 | |||
| beab6a3c02 | |||
| 5e0ebce300 | |||
| 85c4ec5654 | |||
| 7957a6abd4 | |||
| 39a7d8d131 | |||
| 54c5bf128e | |||
| ca0c592044 | |||
| 829d1316b2 | |||
| c2c1c85651 | |||
| c5833f1417 | |||
| 07ae3593c3 | |||
| 8ab35b11cd | |||
| 35ef6170ba | |||
| d650f11255 | |||
| b4926b39fe | |||
| 53ccee734f | |||
| fe9d0e84d3 | |||
| 4e7e12f90f | |||
| 8ae68cd34a | |||
| 607d69b1e1 | |||
| 75e13239c2 | |||
| c88d8d047a | |||
| 304fbba2bb | |||
| d2f35ccbde | |||
| 273c32d4a7 | |||
| 1616999cb9 | |||
| 195db5c6e8 | |||
| fa0e216c7e | |||
| 1605169b92 | |||
| 7fa0016536 | |||
| b4d26c3c4e | |||
| 7470d9b55c | |||
| aaf1fb8c94 | |||
| 50e62c7b73 | |||
| 363eff831f | |||
| 31820ddfd7 | |||
| 153749aebe | |||
| 7c5dd85e74 | |||
| 7a5bf8a06f | |||
| 9c05057deb | |||
| 8b1902601f | |||
| d94d9d0a78 | |||
| 457b0851c9 | |||
| d2cae9faca | |||
| e27739eafd | |||
| 874302537d | |||
| cd3b67d4cc | |||
| 7aacac2fb7 | |||
| a69fb3fec3 | |||
| 2d9cbe4f33 | |||
| e33ed0afc8 | |||
| ace6544444 | |||
| 1468c56e1f | |||
| b36756ebbc | |||
| a8f378deb7 | |||
| cb4e53c459 | |||
| 3e0b283593 | |||
| 3b4e9fd2e4 | |||
| 6af6195798 | |||
| a8d1f484b5 | |||
| 619c60533d | |||
| c6fd760006 | |||
| 03f88c6e3e | |||
| 697b33ae80 | |||
| 79ef1c4811 | |||
| f442e949aa | |||
| 8050d3f69a | |||
| 66efa1da5c |
@@ -663,6 +663,28 @@ def get_session_node(shellargs):
|
||||
return None
|
||||
|
||||
|
||||
def run_inline_command(path, arg, completion, **kwargs):
|
||||
tlvdata.send(session.connection, {'operation': 'pause',
|
||||
'path': currconsole})
|
||||
buffdata = ''
|
||||
while select.select((session.connection,), (), (), 0)[0]:
|
||||
buffdata += consume_termdata(session.connection, bufferonly=True)
|
||||
rc = session.simple_noderange_command(consolename, path, arg, **kwargs)
|
||||
tlvdata.send(session.connection, {'operation': 'resume',
|
||||
'path': currconsole})
|
||||
sys.stdout.write(completion)
|
||||
sys.stdout.flush()
|
||||
if buffdata:
|
||||
try:
|
||||
sys.stdout.write(buffdata)
|
||||
except UnicodeEncodeError:
|
||||
sys.stdout.buffer.write(buffdata.encode('utf8'))
|
||||
except IOError: # Some times circumstances are bad
|
||||
# resort to byte at a time...
|
||||
for d in buffdata:
|
||||
sys.stdout.write(d)
|
||||
return rc
|
||||
|
||||
def conserver_command(filehandle, localcommand):
|
||||
# x - conserver has that as 'show baud', I am inclined to replace that with
|
||||
# 'request exclusive'
|
||||
@@ -698,13 +720,13 @@ def conserver_command(filehandle, localcommand):
|
||||
localcommand = get_command_bytes(filehandle, localcommand, cmdlen)
|
||||
|
||||
if localcommand[1] == 'o': # off
|
||||
print("powering off...")
|
||||
session.simple_noderange_command(consolename, '/power/state', 'off')
|
||||
print("complete]\r")
|
||||
sys.stdout.write("powering off...")
|
||||
sys.stdout.flush()
|
||||
run_inline_command('/power/state', 'off', 'complete]')
|
||||
elif localcommand[1] == 's': # shutdown
|
||||
print("shutting down...")
|
||||
session.simple_noderange_command(consolename, '/power/state', 'shutdown')
|
||||
print("complete]\r")
|
||||
sys.stdout.write("shutting down...")
|
||||
sys.stdout.flush()
|
||||
run_inline_command('/power/state', 'shutdown', 'complete]')
|
||||
elif localcommand[1] == 'b': # boot
|
||||
cmdlen += 1
|
||||
localcommand = get_command_bytes(filehandle, localcommand, cmdlen)
|
||||
@@ -715,59 +737,46 @@ def conserver_command(filehandle, localcommand):
|
||||
bootmode = 'uefi'
|
||||
bootdev = 'setup'
|
||||
|
||||
rc = session.simple_noderange_command(consolename, '/boot/nextdevice', bootdev, bootmode=bootmode)
|
||||
rc = run_inline_command('/boot/nextdevice', bootdev, '', bootmode=bootmode)
|
||||
|
||||
if rc:
|
||||
print("Error]\r")
|
||||
else:
|
||||
rc = session.simple_noderange_command(consolename, '/power/state', 'boot')
|
||||
rc = run_inline_command('/power/state', 'boot', 'complete]')
|
||||
if rc:
|
||||
print("Error]\r")
|
||||
else:
|
||||
print("complete]\r")
|
||||
print("Error]")
|
||||
|
||||
elif localcommand[2] == 'n': # boot to network
|
||||
print("booting to network...")
|
||||
|
||||
sys.stdout.write("booting to network...")
|
||||
sys.stdout.flush()
|
||||
bootmode = 'uefi'
|
||||
bootdev = 'network'
|
||||
|
||||
rc = session.simple_noderange_command(consolename, '/boot/nextdevice', bootdev, bootmode=bootmode)
|
||||
|
||||
rc = run_inline_command('/boot/nextdevice', bootdev, '', bootmode=bootmode)
|
||||
if rc:
|
||||
print("Error]\r")
|
||||
else:
|
||||
rc = session.simple_noderange_command(consolename, '/power/state', 'boot')
|
||||
|
||||
rc = run_inline_command('/power/state', 'boot', 'complete]')
|
||||
if rc:
|
||||
print("Error]\r")
|
||||
else:
|
||||
print("complete]\r")
|
||||
|
||||
elif localcommand[2] == '\x0d': # boot to default
|
||||
print("booting to default...")
|
||||
|
||||
bootmode = 'uefi'
|
||||
bootdev = 'default'
|
||||
|
||||
rc = session.simple_noderange_command(consolename, '/boot/nextdevice', bootdev, bootmode=bootmode)
|
||||
rc = run_inline_command('/boot/nextdevice', bootdev, '', bootmode=bootmode)
|
||||
|
||||
if rc:
|
||||
print("Error]\r")
|
||||
else:
|
||||
rc = session.simple_noderange_command(consolename, '/power/state', 'boot')
|
||||
|
||||
rc = run_inline_command('/power/state', 'boot', 'complete]')
|
||||
if rc:
|
||||
print("Error]\r")
|
||||
else:
|
||||
print("complete]\r")
|
||||
|
||||
else:
|
||||
print("Unknown boot state.]\r")
|
||||
|
||||
else:
|
||||
print("Unknown power state.]\r")
|
||||
|
||||
check_power_state()
|
||||
elif localcommand[0] == 'r':
|
||||
sys.stdout.write('\x1b7\x1b[999;999H\x1b[6n')
|
||||
@@ -929,65 +938,7 @@ def main():
|
||||
rdylist = ()
|
||||
for fh in rdylist:
|
||||
if fh == session.connection:
|
||||
# this only should get called in the
|
||||
# case of a console session
|
||||
# each command should slurp up all relevant
|
||||
# recv potential
|
||||
#fh.read()
|
||||
try:
|
||||
data = tlvdata.recv(fh)
|
||||
except Exception:
|
||||
data = None
|
||||
if type(data) == dict:
|
||||
updatestatus(data)
|
||||
continue
|
||||
if data is not None:
|
||||
data = client.stringify(data)
|
||||
if clearpowermessage:
|
||||
sys.stdout.write("\x1b[2J\x1b[;H")
|
||||
clearpowermessage = False
|
||||
try:
|
||||
sys.stdout.write(data)
|
||||
except UnicodeEncodeError:
|
||||
sys.stdout.buffer.write(data.encode('utf8'))
|
||||
except IOError: # Some times circumstances are bad
|
||||
# resort to byte at a time...
|
||||
for d in data:
|
||||
sys.stdout.write(d)
|
||||
now = time.time()
|
||||
if ('showtime' not in laststate or
|
||||
(now // 60) != laststate['showtime'] // 60):
|
||||
# don't bother churning if minute does not change
|
||||
laststate['showtime'] = now
|
||||
updatestatus()
|
||||
try:
|
||||
sys.stdout.flush()
|
||||
except Exception:
|
||||
# EWOULDBLOCK causes this to raise, ignore
|
||||
# this scenario comfortable that it
|
||||
# will come out soon enough
|
||||
pass
|
||||
else:
|
||||
deadline = 5
|
||||
connected = False
|
||||
while not connected and deadline > 0:
|
||||
try:
|
||||
server_connect()
|
||||
connected = True
|
||||
except Exception:
|
||||
pass
|
||||
if not connected:
|
||||
time.sleep(1)
|
||||
deadline -=1
|
||||
if connected:
|
||||
do_command(
|
||||
"start /nodes/%s/console/session skipreplay=True" % consolename,
|
||||
netserver)
|
||||
else:
|
||||
doexit = True
|
||||
inconsole = False
|
||||
sys.stdout.write("\r\n[remote disconnected]\r\n")
|
||||
break
|
||||
consume_termdata(fh)
|
||||
else:
|
||||
try:
|
||||
myinput = fh.read()
|
||||
@@ -1013,6 +964,64 @@ def main():
|
||||
sys.stdout.write('Lost connection to server')
|
||||
quitconfetty(fullexit=True)
|
||||
|
||||
def consume_termdata(fh, bufferonly=False):
|
||||
global clearpowermessage
|
||||
try:
|
||||
data = tlvdata.recv(fh)
|
||||
except Exception:
|
||||
data = None
|
||||
if type(data) == dict:
|
||||
updatestatus(data)
|
||||
return
|
||||
if data is not None:
|
||||
data = client.stringify(data)
|
||||
if clearpowermessage:
|
||||
sys.stdout.write("\x1b[2J\x1b[;H")
|
||||
clearpowermessage = False
|
||||
if bufferonly:
|
||||
return data
|
||||
try:
|
||||
sys.stdout.write(data)
|
||||
except UnicodeEncodeError:
|
||||
sys.stdout.buffer.write(data.encode('utf8'))
|
||||
except IOError: # Some times circumstances are bad
|
||||
# resort to byte at a time...
|
||||
for d in data:
|
||||
sys.stdout.write(d)
|
||||
now = time.time()
|
||||
if ('showtime' not in laststate or
|
||||
(now // 60) != laststate['showtime'] // 60):
|
||||
# don't bother churning if minute does not change
|
||||
laststate['showtime'] = now
|
||||
updatestatus()
|
||||
try:
|
||||
sys.stdout.flush()
|
||||
except Exception:
|
||||
# EWOULDBLOCK causes this to raise, ignore
|
||||
# this scenario comfortable that it
|
||||
# will come out soon enough
|
||||
pass
|
||||
else:
|
||||
deadline = 5
|
||||
connected = False
|
||||
while not connected and deadline > 0:
|
||||
try:
|
||||
server_connect()
|
||||
connected = True
|
||||
except Exception:
|
||||
pass
|
||||
if not connected:
|
||||
time.sleep(1)
|
||||
deadline -=1
|
||||
if connected:
|
||||
do_command(
|
||||
"start /nodes/%s/console/session skipreplay=True" % consolename,
|
||||
netserver)
|
||||
else:
|
||||
doexit = True
|
||||
inconsole = False
|
||||
sys.stdout.write("\r\n[remote disconnected]\r\n")
|
||||
|
||||
if __name__ == '__main__':
|
||||
errcode = 0
|
||||
deadline = 0
|
||||
@@ -1024,11 +1033,12 @@ if __name__ == '__main__':
|
||||
errcode = e.errorcode
|
||||
except Exception as e:
|
||||
import traceback
|
||||
excinfo = traceback.print_exc()
|
||||
try:
|
||||
quitconfetty()
|
||||
except Exception:
|
||||
pass
|
||||
traceback.print_exc()
|
||||
print(excinfo)
|
||||
finally:
|
||||
if deadline and os.times()[4] < deadline:
|
||||
sys.stderr.write('[Exited early, hit enter to continue]')
|
||||
|
||||
@@ -47,14 +47,14 @@ exitcode = 0
|
||||
errorNodes = set([])
|
||||
session.stop_if_noderange_over(noderange, options.maxnodes)
|
||||
success = session.simple_noderange_command(noderange, 'configuration/management_controller/reset', 'reset', key='state', errnodes=errorNodes) # = 0 if successful
|
||||
if success != 0:
|
||||
sys.exit(success)
|
||||
|
||||
# Determine which nodes were successful and print them
|
||||
|
||||
allNodes = set([])
|
||||
|
||||
for node in session.read('/noderange/{0}/nodes/'.format(noderange)):
|
||||
if 'error' in node and success != 0:
|
||||
sys.exit(success)
|
||||
allNodes.add(node['item']['href'].replace("/", ""))
|
||||
|
||||
goodNodes = allNodes - errorNodes
|
||||
|
||||
@@ -159,7 +159,7 @@ def parse_config_line(arguments, single=False):
|
||||
if setmode is None:
|
||||
setmode = True
|
||||
if setmode != True:
|
||||
bailout('Cannot do set and query in same command')
|
||||
bailout('Cannot do set and query in same command: Query detected but "{0}" appears to be set'.format(param))
|
||||
if '=' in param:
|
||||
key, _, value = param.partition('=')
|
||||
_assign_value()
|
||||
@@ -171,7 +171,7 @@ def parse_config_line(arguments, single=False):
|
||||
if setmode is None:
|
||||
setmode = False
|
||||
if setmode != False:
|
||||
bailout('Cannot do set and query in same command')
|
||||
bailout('Cannot do set and query in same command: Set mode detected but "{0}" appears to be a query'.format(param))
|
||||
if '.' not in param:
|
||||
if param == 'bmc':
|
||||
printallbmc = True
|
||||
|
||||
@@ -34,6 +34,7 @@ defcolumns = ['Node', 'Model', 'Serial', 'UUID', 'Mac Address', 'Type',
|
||||
columnmapping = {
|
||||
'Node': 'nodename',
|
||||
'Model': 'modelnumber',
|
||||
'Model Name': 'modelname',
|
||||
'Serial': 'serialnumber',
|
||||
'UUID': 'uuid',
|
||||
'Type': 'types',
|
||||
@@ -148,6 +149,7 @@ unique_fields = frozenset(['serial', 'mac', 'uuid'])
|
||||
def import_csv(options, session):
|
||||
nodedata = []
|
||||
unique_data = {}
|
||||
exitcode = 0
|
||||
with open(options.importfile, 'r') as datasrc:
|
||||
records = csv.reader(datasrc)
|
||||
fields = process_header(next(records))
|
||||
@@ -188,6 +190,7 @@ def import_csv(options, session):
|
||||
for res in session.create('/nodes/', datum):
|
||||
if 'error' in res:
|
||||
sys.stderr.write(res['error'] + '\n')
|
||||
exitcode |= res.get('errorcode', 1)
|
||||
continue
|
||||
elif 'created' in res:
|
||||
print('Defined ' + res['created'])
|
||||
@@ -198,11 +201,14 @@ def import_csv(options, session):
|
||||
{'node': nodename}):
|
||||
if 'error' in res:
|
||||
sys.stderr.write(res['error'] + '\n')
|
||||
exitcode |= res.get('errorcode', 1)
|
||||
continue
|
||||
elif 'assigned' in res:
|
||||
print('Discovered ' + res['assigned'])
|
||||
else:
|
||||
print(repr(res))
|
||||
if exitcode:
|
||||
sys.exit(exitcode)
|
||||
|
||||
|
||||
def list_discovery(options, session):
|
||||
@@ -291,12 +297,18 @@ def assign_discovery(options, session, needid=True):
|
||||
if not matches:
|
||||
sys.stderr.write("No matching discovery candidates found\n")
|
||||
sys.exit(1)
|
||||
exitcode = 0
|
||||
for res in session.update('/discovery/by-mac/{0}'.format(matches[0]),
|
||||
{'node': options.node}):
|
||||
if 'assigned' in res:
|
||||
print('Assigned: {0}'.format(res['assigned']))
|
||||
elif 'error' in res:
|
||||
sys.stderr.write('Error: {0}\n'.format(res['error']))
|
||||
exitcode |= res.get('errorcode', 1)
|
||||
else:
|
||||
print(repr(res))
|
||||
if exitcode:
|
||||
sys.exit(exitcode)
|
||||
|
||||
def blocking_scan(session):
|
||||
list(session.update('/discovery/rescan', {'rescan': 'start'}))
|
||||
|
||||
@@ -32,6 +32,13 @@ path = os.path.realpath(os.path.join(path, '..', 'lib', 'python'))
|
||||
if path.startswith('/opt'):
|
||||
sys.path.append(path)
|
||||
|
||||
|
||||
class hybridcsv(csv.excel):
|
||||
lineterminator = '\n'
|
||||
|
||||
|
||||
csv.register_dialect('hybrid', hybridcsv)
|
||||
|
||||
import confluent.client as client
|
||||
|
||||
sensorcollections = {
|
||||
@@ -209,7 +216,7 @@ def main():
|
||||
headernames.append(headername)
|
||||
if options.csv:
|
||||
linebyline = False
|
||||
csvwriter = csv.writer(sys.stdout)
|
||||
csvwriter = csv.writer(sys.stdout, dialect='hybrid')
|
||||
if options.interval:
|
||||
csvwriter.writerow(['time', 'node'] + headernames)
|
||||
else:
|
||||
|
||||
@@ -48,6 +48,8 @@ def run():
|
||||
help='Do not prefix output with node names')
|
||||
argparser.add_option('-p', '--port', type='int', default=0,
|
||||
help='Specify a custom port for ssh')
|
||||
argparser.add_option('-s', '--substitutename',
|
||||
help='Use a different name other than the nodename for ssh')
|
||||
argparser.add_option('-m', '--maxnodes', type='int',
|
||||
help='Specify a maximum number of '
|
||||
'nodes to run remote ssh command to, '
|
||||
@@ -71,6 +73,19 @@ def run():
|
||||
exitcode = 0
|
||||
|
||||
c.stop_if_noderange_over(args[0], options.maxnodes)
|
||||
nodemap = {}
|
||||
if options.substitutename:
|
||||
subname = options.substitutename
|
||||
if '{' not in subname:
|
||||
subname = '{node}' + subname
|
||||
for exp in c.create('/noderange/{0}/attributes/expression'.format(args[0]),
|
||||
{'expression': subname}):
|
||||
if 'error' in exp:
|
||||
sys.stderr.write(exp['error'] + '\n')
|
||||
exitcode |= exp.get('errorcode', 1)
|
||||
ex = exp.get('databynode', ())
|
||||
for node in ex:
|
||||
nodemap[node] = ex[node]['value']
|
||||
for exp in c.create('/noderange/{0}/attributes/expression'.format(args[0]),
|
||||
{'expression': cmdstr}):
|
||||
if 'error' in exp:
|
||||
@@ -79,12 +94,13 @@ def run():
|
||||
ex = exp.get('databynode', ())
|
||||
for node in ex:
|
||||
cmd = ex[node]['value']
|
||||
sshnode = nodemap.get(node, node)
|
||||
if not isinstance(cmd, str) and not isinstance(cmd, bytes):
|
||||
cmd = cmd.encode('utf-8')
|
||||
if options.port:
|
||||
cmdv = ['ssh', '-p', '{0}'.format(options.port), node, cmd]
|
||||
cmdv = ['ssh', '-p', '{0}'.format(options.port), sshnode, cmd]
|
||||
else:
|
||||
cmdv = ['ssh', node, cmd]
|
||||
cmdv = ['ssh', sshnode, cmd]
|
||||
if currprocs < concurrentprocs:
|
||||
currprocs += 1
|
||||
run_cmdv(node, cmdv, all, pipedesc)
|
||||
|
||||
@@ -116,7 +116,7 @@ class OptParser(optparse.OptionParser):
|
||||
def main():
|
||||
argparser = OptParser(
|
||||
usage="Usage: %prog <noderange> servicedata "
|
||||
"<filename>",
|
||||
"<directory or filename>",
|
||||
epilog='\nservicedata will save service data to the given '
|
||||
'directory. It is saved to the location on the relevant '
|
||||
'management server (the confluent server if running remote, '
|
||||
|
||||
@@ -22,7 +22,7 @@ node and group names sorted alphanumerically.
|
||||
output suggests a large number of nodes are unreachable, abbreviate
|
||||
showing 'rack1' as being unreachable may make more obvious a possible cause.
|
||||
|
||||
* `-b`, `--base`:
|
||||
* `-b BASE`, `--base=BASE`:
|
||||
Use given node as reference for comparison when using
|
||||
-d, instead of using the most common result
|
||||
|
||||
@@ -44,16 +44,19 @@ node and group names sorted alphanumerically.
|
||||
|
||||
* `-c`, `--count`:
|
||||
Print a count of the number of nodes in an output group under the
|
||||
noderange.
|
||||
noderange.
|
||||
|
||||
* `-r`, `--reverse`:
|
||||
Rather than starting with most common to least common, start with
|
||||
the least common and print the most common last.
|
||||
* `-r`, `--reverse`:
|
||||
Rather than starting with most common to least common, start with
|
||||
the least common and print the most common last.
|
||||
|
||||
* `-l`, `--log`:
|
||||
* `-l LOG`, `--log=LOG`:
|
||||
Save output per node to individual log files, replacing {node} in the name
|
||||
with the nodename of each
|
||||
|
||||
* `-h`, `--help`:
|
||||
Show help message and exit
|
||||
|
||||
## EXAMPLES
|
||||
* Organizing power state of multiple nodes:
|
||||
`# nodepower n1-n12 | collate`
|
||||
|
||||
@@ -37,6 +37,9 @@ on circumstances.
|
||||
|
||||
* `-i`:
|
||||
Provide the token as an argument rather than interactively.
|
||||
|
||||
* `-h`, `--help`:
|
||||
Show help message and exit
|
||||
|
||||
## EXAMPLES
|
||||
* Inviting a server called mgt2:
|
||||
|
||||
@@ -17,15 +17,18 @@ the various function specific commands.
|
||||
|
||||
## OPTIONS
|
||||
|
||||
* `-s`, `--server`:
|
||||
* `-s SERVER:PORT`, `--server=SERVER:PORT`:
|
||||
Confluent instance to connect to
|
||||
|
||||
* `-c`, `--control`:
|
||||
* `-c PATH`, `--control=PATH`:
|
||||
Path to offer terminal control
|
||||
|
||||
* `-m`, `--mintime`:
|
||||
* `-m MINTIME`, `--mintime=MINTIME`:
|
||||
Minimum time to run or else pause for input (used to
|
||||
keep a terminal from closing quickly on error)
|
||||
|
||||
* `-h`, `--help`:
|
||||
Show help message and exit
|
||||
|
||||
## COMMANDS
|
||||
|
||||
|
||||
@@ -37,3 +37,6 @@ the json files (password protected, removed from the files, or unprotected).
|
||||
keys.json file, and only the protected data is needed.
|
||||
keys do not change and as such they do not require
|
||||
incremental backup.
|
||||
|
||||
* `-h`, `--help`:
|
||||
Show help message and exit
|
||||
|
||||
@@ -53,10 +53,13 @@ See nodegroupattrib(8) command on how to manage attributes on a group level.
|
||||
Request interactive prompting to provide values rather than the command line
|
||||
or environment variables.
|
||||
|
||||
* `-m`, `--maxnodes`:
|
||||
* `-m MAXNODES`, `--maxnodes=MAXNODES`:
|
||||
Prompt if trying to set attributes on more than
|
||||
specified number of nodes.
|
||||
|
||||
* `-h`, `--help`:
|
||||
Show help message and exit
|
||||
|
||||
## EXAMPLES
|
||||
* Listing matching nodes of a simple noderange:
|
||||
`# nodeattrib n1-n2`
|
||||
|
||||
@@ -11,9 +11,12 @@ nodebmcreset(8) -- Reset management controller
|
||||
|
||||
## OPTIONS
|
||||
|
||||
* `-m`, `--maxnodes`:
|
||||
* `-m MAXNODES`, `--maxnodes=MAXNODES`:
|
||||
Number of nodes to affect before prompting for
|
||||
confirmation
|
||||
|
||||
* `-h`, `--help`:
|
||||
Show help message and exit
|
||||
|
||||
## EXAMPLES:
|
||||
|
||||
|
||||
@@ -3,8 +3,7 @@ nodeboot(8) -- Reboot a confluent node to a specific device
|
||||
|
||||
## SYNOPSIS
|
||||
|
||||
`nodeboot <noderange>`
|
||||
`nodeboot [options] <noderange>` [default|cd|network|setup|hd]
|
||||
`nodeboot [options] <noderange> [default|cd|network|setup|hd]`
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
@@ -26,9 +25,12 @@ device without inducing a reboot, see the `nodesetboot` command.
|
||||
For a system that supports it, mark the boot override to persist rather than
|
||||
be a one time change. Many systems do not support this functionality.
|
||||
|
||||
* `-m`, `--maxnodes`:
|
||||
* `-m MAXNODES`, `--maxnodes=MAXNODES`:
|
||||
Specify a maximum number of nodes to boot, prompting
|
||||
if over the threshold
|
||||
|
||||
* `-h`, `--help`:
|
||||
Show help message and exit
|
||||
|
||||
* `default`:
|
||||
Request a normal default boot with no particular device override
|
||||
|
||||
@@ -3,8 +3,7 @@ nodeconfig(8) -- Show or change node configuration
|
||||
|
||||
## SYNOPSIS
|
||||
|
||||
`nodeconfig <noderange> [options] [<configuration>..]`
|
||||
`nodeconfig <noderange> [options] [<configuration=value>..]`
|
||||
`nodeconfig [options] <noderange> [setting|setting=value]`
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
@@ -53,9 +52,12 @@ actually be in effect until a reboot.
|
||||
configuration reset to default. Currently the only component implemented
|
||||
is uefi.
|
||||
|
||||
* `-m`, `--maxnodes`:
|
||||
* `-m MAXNODES`, `--maxnodes=MAXNODES`:
|
||||
Specify a maximum number of nodes to configure, prompting if over
|
||||
the threshold
|
||||
|
||||
* `-h`, `--help`:
|
||||
Show help message and exit
|
||||
|
||||
## EXAMPLES
|
||||
* Showing the current IP configuration of noderange BMC/IMM/XCC:
|
||||
|
||||
@@ -3,9 +3,7 @@ nodedeploy(8) -- Request preparation and/or initiating a node deployment
|
||||
|
||||
## SYNOPSIS
|
||||
|
||||
`nodedeploy <noderange>`
|
||||
`nodedeploy <noderange> -c`
|
||||
`nodedeploy <noderange> [-n] [-p] <name of profile>`
|
||||
`nodedeploy [-h] [-c] [-n] [-p] [-m MAXNODES] <noderange> [profile]`
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
@@ -29,8 +27,11 @@ deployment status.
|
||||
Prepare the network services for deployment, but do not interact with BMCs. This is intended for scenarios where
|
||||
the boot device control and server restart will be handled outside of confluent.
|
||||
|
||||
* `-m`, `--maxnodes`:
|
||||
* `-m MAXNODES`, `--maxnodes=MAXNODES`:
|
||||
Specifiy a maximum nodes to be deployed.
|
||||
|
||||
* `-h`, `--help`:
|
||||
Show help message and exit
|
||||
|
||||
## EXAMPLES
|
||||
* Begin the instalalation of a profile of CentOS 8.2:
|
||||
|
||||
@@ -3,8 +3,7 @@ nodeeventlog(8) -- Pull eventlog from confluent nodes
|
||||
|
||||
## SYNOPSIS
|
||||
|
||||
`nodeeventlog <noderange>`
|
||||
`nodeeventlog <noderange> [clear]`
|
||||
`nodeeventlog [options] <noderange> [clear]`
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
@@ -13,9 +12,12 @@ noderange.
|
||||
|
||||
## OPTIONS
|
||||
|
||||
* `-m`, `--maxnodes`:
|
||||
* `-m MAXNODES`, `--maxnodes=MAXNODES`:
|
||||
Specify a maximum number of nodes to clear if clearing log, prompting if
|
||||
over the threshold
|
||||
over the threshold
|
||||
|
||||
* `-h`, `--help`:
|
||||
Show help message and exit
|
||||
|
||||
## EXAMPLES
|
||||
* Pull the event log from n2 and n3:
|
||||
|
||||
@@ -3,9 +3,7 @@ nodefirmware(8) -- Report firmware information on confluent nodes
|
||||
|
||||
## SYNOPSIS
|
||||
|
||||
`nodefirmware <noderange>`
|
||||
`nodefirmware <noderange> list|<components>|core`
|
||||
`nodefirmware <noderange> update [--backup] <filename>`
|
||||
`nodefirmware <noderange> [list][update [--backup <file>]]|[<components>]`
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
@@ -31,9 +29,12 @@ the out of band facilities. Firmware updates can end in one of three states:
|
||||
* `-b`, `--backup`:
|
||||
Target a backup bank rather than primary
|
||||
|
||||
* `-m`, `--maxnodes`:
|
||||
* `-m MAXNODES`, `--maxnodes=MAXNODES`:
|
||||
When updating, prompt if more than the specified number of servers will
|
||||
be affected
|
||||
|
||||
* `-h`, `--help`:
|
||||
Show help message and exit
|
||||
|
||||
## EXAMPLES
|
||||
|
||||
|
||||
@@ -36,6 +36,9 @@ the attributes are set on the node versus a group to which a node belongs.
|
||||
|
||||
* `-p`, `--prompt`:
|
||||
Prompt for attribute values interactively
|
||||
|
||||
* `-h`, `--help`:
|
||||
Show help message and exit
|
||||
|
||||
## EXAMPLES
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ nodelicense(8) -- Manage license keys on BMC
|
||||
|
||||
`nodelicense` manages license keys on supported BMCs. Without an argument, the command
|
||||
lists currently installed license. Using `delete` will remove the specified license name
|
||||
from th eBMC. The `save` subcommand will take the passed directory (which may be in the form
|
||||
from the BMC. The `save` subcommand will take the passed directory (which may be in the form
|
||||
of /path/to/{node}/ to have the node name substituted for each node) and back up installed licenses
|
||||
to that directory. The `install` command will take the specified filename and install. The filename
|
||||
argument may be of the form xcc_fod_0034_7X21{id.serial}.key to have the serial number substituted
|
||||
@@ -17,5 +17,8 @@ to allow unique licenses to be specified in a single command.
|
||||
|
||||
## OPTIONS
|
||||
|
||||
* `-m`, `--maxnodes`:
|
||||
* `-m MAXNODES`, `--maxnodes=MAXNODES`:
|
||||
Specify a maximum number of nodes to delete licenses from, prompting if over the threshold
|
||||
|
||||
* `-h`, `--help`:
|
||||
Show help message and exit
|
||||
|
||||
@@ -34,8 +34,8 @@ the host platform attempts.
|
||||
|
||||
## OPTIONS
|
||||
|
||||
* `-c`:
|
||||
Specify the maximum number of instances to run concurrently.
|
||||
* `-h`, `--help`:
|
||||
Show help message and exit
|
||||
|
||||
## EXAMPLES
|
||||
* Listing currently mounted media:
|
||||
|
||||
@@ -30,6 +30,13 @@ off will not react to this request.
|
||||
|
||||
* `-p`, '--showprevious':
|
||||
Show previous power state for all directives that may change power state.
|
||||
|
||||
* `-m MAXNODES`, `--maxnodes=MAXNODES`:
|
||||
Specify a maximum number of nodes to change power state, prompting if
|
||||
over the threshold
|
||||
|
||||
* `-h`, `--help`:
|
||||
Show help message and exit
|
||||
|
||||
## EXAMPLES
|
||||
* Get power state of nodes n1 through n4:
|
||||
|
||||
@@ -11,9 +11,12 @@ noderemove(8) -- Remove nodes from the confluent management service
|
||||
|
||||
## OPTIONS
|
||||
|
||||
* `-m`, `--maxnodes`:
|
||||
* `-m MAXNODES`, `--maxnodes=MAXNODES`:
|
||||
Specify a maximum number of nodes to delete, prompting if over the
|
||||
threshold
|
||||
|
||||
* `-h`, `--help`:
|
||||
Show help message and exit
|
||||
|
||||
## EXAMPLES
|
||||
|
||||
@@ -27,4 +30,3 @@ noderemove(8) -- Remove nodes from the confluent management service
|
||||
`r2u3: deleted`
|
||||
`r2u2: deleted`
|
||||
`r2u1: deleted`
|
||||
|
||||
|
||||
@@ -13,8 +13,11 @@ the chassis and putting it back in, but without actually having to do so.
|
||||
|
||||
## OPTIONS
|
||||
|
||||
* `-m`, `--maxnodes`:
|
||||
* `-m MAXNODES`, `--maxnodes=MAXNODES`:
|
||||
Specify a maximum number of nodes to reseat, prompting if over the threshold
|
||||
|
||||
* `-h`, `--help`:
|
||||
Show help message and exit
|
||||
|
||||
## EXAMPLES
|
||||
|
||||
|
||||
@@ -17,7 +17,9 @@ noderange. This will present progress as percentage for all nodes.
|
||||
exceeds the count, then excess nodes will wait until one of the
|
||||
active count completes.
|
||||
|
||||
* `-m`, `--maxnodes`:
|
||||
* `-m MAXNODES`, `--maxnodes=MAXNODES`:
|
||||
Specify a maximum number of nodes to run rsync to, prompting if over the
|
||||
threshold
|
||||
|
||||
* `-h`, `--help`:
|
||||
Show help message and exit
|
||||
|
||||
@@ -21,9 +21,12 @@ themselves, see nodeshell(8).
|
||||
* `-n`, `--nonodeprefix`:
|
||||
Do not prefix output with node names
|
||||
|
||||
* `-m`, `--maxnodes`:
|
||||
* `-m MAXNODES`, `--maxnodes=MAXNODES`:
|
||||
Specify a maximum number of nodes to run the command with, prompting if over
|
||||
the threshold
|
||||
|
||||
* `-h`, `--help`:
|
||||
Show help message and exit
|
||||
|
||||
## EXAMPLES
|
||||
|
||||
|
||||
@@ -2,8 +2,7 @@ nodesetboot(8) -- Check or set next boot device for noderange
|
||||
====================================================
|
||||
|
||||
## SYNOPSIS
|
||||
|
||||
`nodesetboot <noderange>`
|
||||
|
||||
`nodesetboot [options] <noderange> [default|cd|network|setup|hd|usb|floppy]`
|
||||
|
||||
## DESCRIPTION
|
||||
@@ -21,6 +20,8 @@ to use `nodesetboot <noderange> setup` and then initiate a reboot from within
|
||||
the operating system with ssh or similar rather than using the remote hardware
|
||||
control.
|
||||
|
||||
Running the command with no target queries the current setting.
|
||||
|
||||
## OPTIONS
|
||||
|
||||
* `-b`, `--bios`:
|
||||
@@ -34,9 +35,12 @@ control.
|
||||
* `-u`, `--uefi`:
|
||||
This flag does nothing, it is for command compatibility with xCAT's rsetboot
|
||||
|
||||
* `-m`, `--maxnodes`:
|
||||
* `-m MAXNODES`, `--maxnodes=MAXNODES`:
|
||||
Specify a maximum number of nodes to modify next boot device, prompting if
|
||||
over the threshold
|
||||
|
||||
* `-h`, `--help`:
|
||||
Show help message and exit
|
||||
|
||||
* `default`:
|
||||
Request a normal default boot with no particular device override
|
||||
|
||||
@@ -26,7 +26,14 @@ as stderr, unlike psh which combines all stdout and stderr into stdout.
|
||||
|
||||
* `-p PORT`, `--port=PORT`
|
||||
Specify a custom port for ssh
|
||||
|
||||
|
||||
* `-s SUBSTITUTION`, `--substitutename=SUBSTITITUTION`
|
||||
Specify a substitution name instead of the nodename. If no {} are in the substitution,
|
||||
it is considered to be an append. For example, '-s -ib' would produce 'node1-ib' from 'node1'.
|
||||
Full expression syntax is supported, in which case the substitution is considered to be the entire
|
||||
new name. {node}-ib would be equivalent to -ib. For example, nodeshell -s {bmc} node1
|
||||
would ssh to the BMC instead of the node.
|
||||
|
||||
## EXAMPLES
|
||||
|
||||
* Running `echo hi` on for nodes:
|
||||
|
||||
@@ -21,9 +21,12 @@ specific suffixes added to any file produced.
|
||||
|
||||
## OPTIONS
|
||||
|
||||
* `-m`, `--maxnodes`:
|
||||
* `-m MAXNODES`, `--maxnodes=MAXNODES`:
|
||||
Specify a maximum number of nodes to download diagnostic data from, prompting
|
||||
if over the threshold
|
||||
if over the threshold
|
||||
|
||||
* `-h`, `--help`:
|
||||
Show help message and exit
|
||||
|
||||
## EXAMPLES
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ osdeploy(8) --- Configure general OS deployment facilities of confluent
|
||||
|
||||
`osdeploy import <iso>`
|
||||
`osdeploy updateboot <profile>`
|
||||
`osdeploy initialize [-h] [-g] [-u] [-s] [-k] [-t] [-p] [-i] [-l]`
|
||||
`osdeploy initialize [-h] [-g] [-u] [-s] [-k] [-t] [-p] [-i] [-l] [-a]`
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
|
||||
@@ -100,6 +100,9 @@ class HTTPSClient(client.HTTPConnection, object):
|
||||
self.sock = ctx.wrap_socket(psock, server_hostname=host)
|
||||
|
||||
def grab_url(self, url, data=None, returnrsp=False):
|
||||
return self.grab_url_with_status(url, data, returnrsp)[1]
|
||||
|
||||
def grab_url_with_status(self, url, data=None, returnrsp=False):
|
||||
if data:
|
||||
method = 'POST'
|
||||
else:
|
||||
@@ -108,9 +111,9 @@ class HTTPSClient(client.HTTPConnection, object):
|
||||
rsp = self.getresponse()
|
||||
if rsp.status >= 200 and rsp.status < 300:
|
||||
if returnrsp:
|
||||
return rsp
|
||||
return rsp.status, rsp
|
||||
else:
|
||||
return rsp.read()
|
||||
return rsp.status, rsp.read()
|
||||
raise Exception(rsp.read())
|
||||
|
||||
if __name__ == '__main__':
|
||||
@@ -121,8 +124,26 @@ if __name__ == '__main__':
|
||||
if len(sys.argv) == 1:
|
||||
HTTPSClient()
|
||||
sys.exit(0)
|
||||
if sys.argv[-2] == '-o':
|
||||
with open(sys.argv[3], 'wb') as outf:
|
||||
try:
|
||||
outbin = sys.argv.index('-o')
|
||||
sys.argv.pop(outbin)
|
||||
outbin = sys.argv.pop(outbin)
|
||||
except ValueError:
|
||||
outbin = None
|
||||
try:
|
||||
waitfor = sys.argv.index('-w')
|
||||
sys.argv.pop(waitfor)
|
||||
waitfor = int(sys.argv.pop(waitfor))
|
||||
except ValueError:
|
||||
waitfor = None
|
||||
try:
|
||||
data = sys.argv.index('-d')
|
||||
sys.argv.pop(data)
|
||||
data = sys.argv.pop(data)
|
||||
except ValueError:
|
||||
data = None
|
||||
if outbin:
|
||||
with open(outbin, 'ab+') as outf:
|
||||
reader = HTTPSClient(json=json).grab_url(
|
||||
sys.argv[1], data, returnrsp=True)
|
||||
chunk = reader.read(16384)
|
||||
@@ -130,8 +151,13 @@ if __name__ == '__main__':
|
||||
outf.write(chunk)
|
||||
chunk = reader.read(16384)
|
||||
sys.exit(0)
|
||||
elif len(sys.argv) > 2 and not os.path.exists(sys.argv[-1]):
|
||||
data = sys.argv[-1]
|
||||
if os.path.exists(sys.argv[-1]):
|
||||
if len(sys.argv) > 2 and os.path.exists(sys.argv[-1]):
|
||||
data = open(sys.argv[-1]).read()
|
||||
sys.stdout.write(HTTPSClient(json=json).grab_url(sys.argv[1], data).decode())
|
||||
if waitfor:
|
||||
client = HTTPSClient(json=json)
|
||||
status = 201
|
||||
while status != waitfor:
|
||||
status, rsp = client.grab_url_with_status(sys.argv[1], data)
|
||||
sys.stdout.write(rsp.decode())
|
||||
else:
|
||||
sys.stdout.write(HTTPSClient(json=json).grab_url(sys.argv[1], data).decode())
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
Ansible playbooks ending in .yml or .yaml that are placed into this directory will be executed at the
|
||||
appropriate phase of the install process.
|
||||
|
||||
The 'hosts' may be omitted, and if included will be ignored, replaced with the host that is specifically
|
||||
requesting the playbooks be executed.
|
||||
|
||||
Also, the playbooks will be executed on the deployment server. Hence it may be slower in aggregate than
|
||||
running content under scripts/ which ask much less of the deployment server
|
||||
|
||||
Here is an example of what a playbook would look like broadly:
|
||||
|
||||
- name: Example
|
||||
gather_facts: no
|
||||
tasks:
|
||||
- name: Example1
|
||||
lineinfile:
|
||||
path: /etc/hosts
|
||||
line: 1.2.3.4 test1
|
||||
create: yes
|
||||
- name: Example2
|
||||
lineinfile:
|
||||
path: /etc/hosts
|
||||
line: 1.2.3.5 test2
|
||||
create: yes
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
Ansible playbooks ending in .yml or .yaml that are placed into this directory will be executed at the
|
||||
appropriate phase of the install process.
|
||||
|
||||
The 'hosts' may be omitted, and if included will be ignored, replaced with the host that is specifically
|
||||
requesting the playbooks be executed.
|
||||
|
||||
Also, the playbooks will be executed on the deployment server. Hence it may be slower in aggregate than
|
||||
running content under scripts/ which ask much less of the deployment server
|
||||
|
||||
Here is an example of what a playbook would look like broadly:
|
||||
|
||||
- name: Example
|
||||
gather_facts: no
|
||||
tasks:
|
||||
- name: Example1
|
||||
lineinfile:
|
||||
path: /etc/hosts
|
||||
line: 1.2.3.4 test1
|
||||
create: yes
|
||||
- name: Example2
|
||||
lineinfile:
|
||||
path: /etc/hosts
|
||||
line: 1.2.3.5 test2
|
||||
create: yes
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/bin/sh
|
||||
sed -i 's/centos/CentOS/; s/rhel/Red Hat Enterprise Linux/' $2/profile.yaml
|
||||
sed -i 's/centos/CentOS/; s/rhel/Red Hat Enterprise Linux/; s/oraclelinux/Oracle Linux/; s/alma/AlmaLinux/' $2/profile.yaml
|
||||
ln -s $1/images/pxeboot/vmlinuz $2/boot/kernel && \
|
||||
ln -s $1/images/pxeboot/initrd.img $2/boot/initramfs/distribution
|
||||
mkdir -p $2/boot/efi/boot && \
|
||||
|
||||
@@ -33,14 +33,15 @@
|
||||
reboot
|
||||
|
||||
%packages
|
||||
#-kernel-uek # This can opt out of the UEK for the relevant distribution
|
||||
chrony
|
||||
rsync
|
||||
python
|
||||
tar
|
||||
pciutils
|
||||
%include /tmp/addonpackages
|
||||
%end
|
||||
|
||||
|
||||
# A kickstart.custom file is provided to enable easily adding
|
||||
# kickstart content without modifying the stock file.
|
||||
# While the stock file may be safely modified, using the .custom
|
||||
@@ -49,32 +50,33 @@ pciutils
|
||||
%include /tmp/kickstart.custom
|
||||
|
||||
%pre
|
||||
profile=$(grep ^profile: /etc/confluent/confluent.deploycfg |awk '{print $2}')
|
||||
mgr=$(grep deploy_server /etc/confluent/confluent.deploycfg |awk '{print $2}')
|
||||
curl -f https://$mgr/confluent-public/os/$profile/scripts/pre.sh > /tmp/preinst.sh
|
||||
confluent_profile=$(grep ^profile: /etc/confluent/confluent.deploycfg |awk '{print $2}')
|
||||
confluent_mgr=$(grep deploy_server /etc/confluent/confluent.deploycfg |awk '{print $2}')
|
||||
curl -f https://$confluent_mgr/confluent-public/os/$confluent_profile/scripts/pre.sh > /tmp/preinst.sh
|
||||
. /tmp/preinst.sh
|
||||
%end
|
||||
|
||||
%post --nochroot
|
||||
mkdir -p /mnt/sysimage/etc/confluent
|
||||
profile=$(grep ^profile: /etc/confluent/confluent.deploycfg |awk '{print $2}')
|
||||
mgr=$(grep deploy_server /etc/confluent/confluent.deploycfg |awk '{print $2}')
|
||||
curl -f https://$mgr/confluent-public/os/$profile/scripts/prechroot.sh > /tmp/postinst.sh
|
||||
confluent_profile=$(grep ^profile: /etc/confluent/confluent.deploycfg |awk '{print $2}')
|
||||
confluent_mgr=$(grep deploy_server /etc/confluent/confluent.deploycfg |awk '{print $2}')
|
||||
curl -f https://$confluent_mgr/confluent-public/os/$confluent_profile/scripts/prechroot.sh > /tmp/postinst.sh
|
||||
. /tmp/postinst.sh
|
||||
|
||||
# Hook firstboot.sh
|
||||
curl -f https://$mgr/confluent-public/os/$profile/scripts/firstboot.service > /mnt/sysimage/etc/systemd/system/firstboot.service
|
||||
curl -f https://$mgr/confluent-public/os/$profile/scripts/firstboot.sh > /mnt/sysimage/etc/confluent/firstboot.sh
|
||||
chmod +x /mnt/sysimage/etc/confluent/firstboot.sh
|
||||
curl -f https://$confluent_mgr/confluent-public/os/$confluent_profile/scripts/firstboot.service > /mnt/sysimage/etc/systemd/system/firstboot.service
|
||||
mkdir -p /mnt/sysimage/opt/confluent/bin
|
||||
curl -f https://$confluent_mgr/confluent-public/os/$confluent_profile/scripts/firstboot.sh > /mnt/sysimage/opt/confluent/bin/firstboot.sh
|
||||
chmod +x /mnt/sysimage/opt/confluent/bin/firstboot.sh
|
||||
%end
|
||||
|
||||
%post
|
||||
cat /etc/confluent/tls/*.pem >> /etc/pki/tls/certs/ca-bundle.crt
|
||||
systemctl enable firstboot
|
||||
chgrp ssh_keys /etc/ssh/ssh*key
|
||||
restorecon /etc/ssh/ssh*key /root/.shosts /etc/ssh/shosts.equiv /etc/ssh/ssh_config.d/* /etc/confluent/firstboot.sh
|
||||
profile=$(grep ^profile: /etc/confluent/confluent.deploycfg |awk '{print $2}')
|
||||
mgr=$(grep deploy_server /etc/confluent/confluent.deploycfg |awk '{print $2}')
|
||||
curl -f https://$mgr/confluent-public/os/$profile/scripts/post.sh > /tmp/postinst.sh
|
||||
restorecon /etc/ssh/ssh*key /root/.shosts /etc/ssh/shosts.equiv /etc/ssh/ssh_config.d/* /opt/confluent/bin/firstboot.sh
|
||||
confluent_profile=$(grep ^profile: /etc/confluent/confluent.deploycfg |awk '{print $2}')
|
||||
confluent_mgr=$(grep deploy_server /etc/confluent/confluent.deploycfg |awk '{print $2}')
|
||||
curl -f https://$confluent_mgr/confluent-public/os/$confluent_profile/scripts/post.sh > /tmp/postinst.sh
|
||||
. /tmp/postinst.sh
|
||||
%end
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
label: %%DISTRO%% %%VERSION%% %%ARCH%% (Default Profile)
|
||||
kernelargs: quiet
|
||||
kernelargs: quiet # These arguments are passed to the installer
|
||||
#installedargs: example # These arguments would be added to the installed system
|
||||
|
||||
@@ -0,0 +1,414 @@
|
||||
# Copyright 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.
|
||||
|
||||
import argparse
|
||||
import ctypes
|
||||
import fcntl
|
||||
import json
|
||||
from select import select
|
||||
import socket
|
||||
import struct
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
if os.path.exists('/opt/confluent/bin/apiclient'):
|
||||
apiclient = '/opt/confluent/bin/apiclient'
|
||||
elif os.path.exists('/etc/confluent/apiclient'):
|
||||
apiclient = '/etc/confluent/apiclient'
|
||||
|
||||
class IpmiMsg(ctypes.Structure):
|
||||
_fields_ = [('netfn', ctypes.c_ubyte),
|
||||
('cmd', ctypes.c_ubyte),
|
||||
('data_len', ctypes.c_short),
|
||||
('data', ctypes.POINTER(ctypes.c_ubyte))]
|
||||
|
||||
|
||||
class IpmiSystemInterfaceAddr(ctypes.Structure):
|
||||
_fields_ = [('addr_type', ctypes.c_int),
|
||||
('channel', ctypes.c_short),
|
||||
('lun', ctypes.c_ubyte)]
|
||||
|
||||
|
||||
class IpmiRecv(ctypes.Structure):
|
||||
_fields_ = [('recv_type', ctypes.c_int),
|
||||
('addr', ctypes.POINTER(IpmiSystemInterfaceAddr)),
|
||||
('addr_len', ctypes.c_uint),
|
||||
('msgid', ctypes.c_long),
|
||||
('msg', IpmiMsg)]
|
||||
|
||||
|
||||
class IpmiReq(ctypes.Structure):
|
||||
_fields_ = [('addr', ctypes.POINTER(IpmiSystemInterfaceAddr)),
|
||||
('addr_len', ctypes.c_uint),
|
||||
('msgid', ctypes.c_long),
|
||||
('msg', IpmiMsg)]
|
||||
|
||||
|
||||
_IONONE = 0
|
||||
_IOWRITE = 1
|
||||
_IOREAD = 2
|
||||
IPMICTL_SET_MY_ADDRESS_CMD = (
|
||||
_IOREAD << 30 | ctypes.sizeof(ctypes.c_uint) << 16
|
||||
| ord('i') << 8 | 17) # from ipmi.h
|
||||
IPMICTL_SEND_COMMAND = (
|
||||
_IOREAD << 30 | ctypes.sizeof(IpmiReq) << 16
|
||||
| ord('i') << 8 | 13) # from ipmi.h
|
||||
# next is really IPMICTL_RECEIVE_MSG_TRUNC, but will only use that
|
||||
IPMICTL_RECV = (
|
||||
(_IOWRITE | _IOREAD) << 30 | ctypes.sizeof(IpmiRecv) << 16
|
||||
| ord('i') << 8 | 11) # from ipmi.h
|
||||
BMC_SLAVE_ADDR = ctypes.c_uint(0x20)
|
||||
CURRCHAN = 0xf
|
||||
ADDRTYPE = 0xc
|
||||
|
||||
|
||||
class Session(object):
|
||||
def __init__(self, devnode='/dev/ipmi0'):
|
||||
"""Create a local session inband
|
||||
|
||||
:param: devnode: The path to the ipmi device
|
||||
"""
|
||||
self.ipmidev = open(devnode, 'r+')
|
||||
fcntl.ioctl(self.ipmidev, IPMICTL_SET_MY_ADDRESS_CMD, BMC_SLAVE_ADDR)
|
||||
# the interface is initted, create some reusable memory for our session
|
||||
self.databuffer = ctypes.create_string_buffer(4096)
|
||||
self.req = IpmiReq()
|
||||
self.rsp = IpmiRecv()
|
||||
self.addr = IpmiSystemInterfaceAddr()
|
||||
self.req.msg.data = ctypes.cast(
|
||||
ctypes.addressof(self.databuffer),
|
||||
ctypes.POINTER(ctypes.c_ubyte))
|
||||
self.rsp.msg.data = self.req.msg.data
|
||||
self.userid = None
|
||||
self.password = None
|
||||
|
||||
def await_reply(self):
|
||||
rd, _, _ = select((self.ipmidev,), (), (), 1)
|
||||
while not rd:
|
||||
rd, _, _ = select((self.ipmidev,), (), (), 1)
|
||||
|
||||
def pause(self, seconds):
|
||||
time.sleep(seconds)
|
||||
|
||||
@property
|
||||
def parsed_rsp(self):
|
||||
response = {'netfn': self.rsp.msg.netfn, 'command': self.rsp.msg.cmd,
|
||||
'code': bytearray(self.databuffer.raw)[0],
|
||||
'data': bytearray(
|
||||
self.databuffer.raw[1:self.rsp.msg.data_len])}
|
||||
return response
|
||||
|
||||
def await_config(s, bmccfg, channel):
|
||||
vlan = bmccfg.get('bmcvlan', None)
|
||||
ipv4 = bmccfg.get('bmcipv4', None)
|
||||
prefix = bmccfg.get('prefixv4', None)
|
||||
gw = bmccfg.get('bmcgw', None)
|
||||
|
||||
|
||||
|
||||
def raw_command(self,
|
||||
netfn,
|
||||
command,
|
||||
data=(),
|
||||
bridge_request=None,
|
||||
retry=True,
|
||||
delay_xmit=None,
|
||||
timeout=None,
|
||||
waitall=False, rslun=0):
|
||||
self.addr.channel = CURRCHAN
|
||||
self.addr.addr_type = ADDRTYPE
|
||||
self.addr.lun = rslun
|
||||
self.req.addr_len = ctypes.sizeof(IpmiSystemInterfaceAddr)
|
||||
self.req.addr = ctypes.pointer(self.addr)
|
||||
self.req.msg.netfn = netfn
|
||||
self.req.msg.cmd = command
|
||||
if data:
|
||||
data = memoryview(bytearray(data))
|
||||
try:
|
||||
self.databuffer[:len(data)] = data[:len(data)]
|
||||
except ValueError:
|
||||
self.databuffer[:len(data)] = data[:len(data)].tobytes()
|
||||
self.req.msg.data_len = len(data)
|
||||
fcntl.ioctl(self.ipmidev, IPMICTL_SEND_COMMAND, self.req)
|
||||
self.await_reply()
|
||||
self.rsp.msg.data_len = 4096
|
||||
self.rsp.addr = ctypes.pointer(self.addr)
|
||||
self.rsp.addr_len = ctypes.sizeof(IpmiSystemInterfaceAddr)
|
||||
fcntl.ioctl(self.ipmidev, IPMICTL_RECV, self.rsp)
|
||||
return self.parsed_rsp
|
||||
|
||||
|
||||
def _is_tsm(model):
|
||||
return model in ('7y00', '7z01', '7y98', '7y99')
|
||||
|
||||
def set_port(s, port, vendor, model):
|
||||
oport = port
|
||||
if vendor not in ('IBM', 'Lenovo'):
|
||||
raise Exception('{0} not implemented'.format(vendor))
|
||||
if _is_tsm(model):
|
||||
return set_port_tsm(s, port, model)
|
||||
else:
|
||||
set_port_xcc(s, port, model)
|
||||
return 1
|
||||
|
||||
|
||||
def set_port_tsm(s, port, model):
|
||||
oport = port
|
||||
sys.stdout.write('Setting TSM port to "{}"...'.format(oport))
|
||||
sys.stdout.flush()
|
||||
if port == 'ocp':
|
||||
s.raw_command(0x32, 0x71, b'\x00\x01\x00')
|
||||
elif port == 'dedicated':
|
||||
s.raw_command(0x32, 0x71, b'\x00\x00\x00')
|
||||
else:
|
||||
raise Exception("Unsupported port for TSM")
|
||||
timer = 15
|
||||
while timer:
|
||||
timer = timer - 1
|
||||
time.sleep(1.0)
|
||||
sys.stdout.write('.')
|
||||
sys.stdout.flush()
|
||||
if port == 'ocp':
|
||||
iface = 0
|
||||
s.raw_command(0x32, 0x71, b'\x00\x00\x03')
|
||||
elif port == 'dedicated':
|
||||
iface = 1
|
||||
s.raw_command(0x32, 0x71, b'\x00\x01\x03')
|
||||
rsp = s.raw_command(0x32, 0x72, bytearray([4, iface, 0]))
|
||||
print('Complete')
|
||||
return int(rsp['data'][0])
|
||||
|
||||
|
||||
def set_port_xcc(s, port, model):
|
||||
oport = port
|
||||
if port.lower() == 'dedicated':
|
||||
port = b'\x01'
|
||||
elif port.lower() in ('ml2', 'ocp'):
|
||||
port = b'\x02\x00'
|
||||
elif port.lower() == 'lom':
|
||||
if model == '7x58':
|
||||
port = b'\x00\x02'
|
||||
else:
|
||||
port = b'\x00\x00'
|
||||
else:
|
||||
port = port.split(' ')
|
||||
port = bytes(bytearray([int(x) for x in port]))
|
||||
currport = bytes(s.raw_command(0xc, 2, b'\x01\xc0\x00\x00')['data'][1:])
|
||||
if port == currport:
|
||||
sys.stdout.write('XCC port already set to "{}"\n'.format(oport))
|
||||
return
|
||||
sys.stdout.write('Setting XCC port to "{}"...'.format(oport))
|
||||
sys.stdout.flush()
|
||||
s.raw_command(0xc, 1, b'\x01\xc0' + port)
|
||||
tries = 60
|
||||
while currport != port and tries:
|
||||
tries -= 1
|
||||
time.sleep(0.5)
|
||||
sys.stdout.write('.')
|
||||
sys.stdout.flush()
|
||||
currport = bytes(s.raw_command(0xc, 2, b'\x01\xc0\x00\x00')['data'][1:])
|
||||
if not tries:
|
||||
raise Exception('Timeout attempting to set port')
|
||||
sys.stdout.write('Complete\n')
|
||||
|
||||
|
||||
def check_vlan(s, vlan, channel):
|
||||
if vlan == 'off':
|
||||
vlan = b'\x00\x00'
|
||||
else:
|
||||
vlan = int(vlan)
|
||||
if vlan:
|
||||
vlan = vlan | 32768
|
||||
vlan = struct.pack('<H', vlan)
|
||||
currvlan = bytes(s.raw_command(0xc, 2, bytearray([channel, 0x14 ,0, 0]))['data'][1:])
|
||||
if bytearray(currvlan)[1] & 0b10000000 == 0:
|
||||
currvlan = b'\x00\x00'
|
||||
return currvlan == vlan
|
||||
|
||||
|
||||
def set_vlan(s, vlan, channel):
|
||||
ovlan = vlan
|
||||
if vlan == 'off':
|
||||
vlan = b'\x00\x00'
|
||||
else:
|
||||
vlan = int(vlan)
|
||||
if vlan:
|
||||
vlan = vlan | 32768
|
||||
vlan = struct.pack('<H', vlan)
|
||||
if check_vlan(s, ovlan, channel):
|
||||
sys.stdout.write('VLAN already configured to "{0}"\n'.format(ovlan))
|
||||
return False
|
||||
rsp = s.raw_command(0xc, 1, bytearray([channel, 0x14]) + vlan)
|
||||
if rsp.get('code', 1) == 0:
|
||||
print('VLAN configured to "{}"'.format(ovlan))
|
||||
else:
|
||||
print('Error setting vlan: ' + repr(rsp))
|
||||
return True
|
||||
|
||||
|
||||
def get_lan_channel(s):
|
||||
for chan in range(1, 16):
|
||||
rsp = s.raw_command(6, 0x42, bytearray([chan]))['data']
|
||||
if not rsp:
|
||||
continue
|
||||
medtype = int(rsp[1]) & 0b1111111
|
||||
if medtype not in (4, 6):
|
||||
continue
|
||||
rsp = s.raw_command(0xc, 2, bytearray([2, chan, 5, 0, 0]))
|
||||
if rsp.get('code', 1) == 0:
|
||||
return chan
|
||||
return 1
|
||||
|
||||
|
||||
def check_ipv4(s, ipaddr, channel):
|
||||
ipaddr = bytearray(socket.inet_aton(ipaddr))
|
||||
rsp = s.raw_command(0xc, 2, bytearray([channel, 3, 0, 0]))['data'][-4:]
|
||||
return rsp == ipaddr
|
||||
|
||||
def set_ipv4(s, ipaddr, channel):
|
||||
oipaddr = ipaddr
|
||||
ipaddr = bytearray(socket.inet_aton(ipaddr))
|
||||
if check_ipv4(s, oipaddr, channel):
|
||||
print('IP Address already set to {}'.format(oipaddr))
|
||||
return False
|
||||
rsp = int(s.raw_command(0xc, 2, bytearray([channel, 4, 0, 0]))['data'][1]) & 0b1111
|
||||
if rsp != 1:
|
||||
sys.stdout.write("Changing configuration to static...")
|
||||
sys.stdout.flush()
|
||||
resp = s.raw_command(0xc, 1, bytearray([channel, 4, 1]))
|
||||
tries = 0
|
||||
while rsp != 1 and tries < 30:
|
||||
sys.stdout.write('.')
|
||||
sys.stdout.flush()
|
||||
tries += 1
|
||||
time.sleep(0.5)
|
||||
rsp = int(s.raw_command(0xc, 2, bytearray([channel, 4, 0, 0]))['data'][1]) & 0b1111
|
||||
sys.stdout.write('Complete\n')
|
||||
sys.stdout.flush()
|
||||
print('Setting IP to {}'.format(oipaddr))
|
||||
s.raw_command(0xc, 1, bytearray([channel, 3]) + ipaddr)
|
||||
return True
|
||||
|
||||
|
||||
def check_subnet(s, prefix, channel):
|
||||
prefix = int(prefix)
|
||||
mask = bytearray(struct.pack('!I', (2**32 - 1) ^ (2**(32 - prefix) - 1)))
|
||||
rsp = s.raw_command(0xc, 2, bytearray([channel, 6, 0, 0]))['data'][-4:]
|
||||
return rsp == mask
|
||||
|
||||
def set_subnet(s, prefix, channel):
|
||||
oprefix = prefix
|
||||
prefix = int(prefix)
|
||||
mask = bytearray(struct.pack('!I', (2**32 - 1) ^ (2**(32 - prefix) - 1)))
|
||||
if check_subnet(s, prefix, channel):
|
||||
print('Subnet Mask already set to /{}'.format(oprefix))
|
||||
return False
|
||||
print('Setting subnet mask to /{}'.format(oprefix))
|
||||
s.raw_command(0xc, 1, bytearray([channel, 6]) + mask)
|
||||
return True
|
||||
|
||||
|
||||
def check_gateway(s, gw, channel):
|
||||
gw = bytearray(socket.inet_aton(gw))
|
||||
rsp = s.raw_command(0xc, 2, bytearray([channel, 12, 0, 0]))['data'][-4:]
|
||||
return rsp == gw
|
||||
|
||||
def set_gateway(s, gw, channel):
|
||||
ogw = gw
|
||||
gw = bytearray(socket.inet_aton(gw))
|
||||
rsp = s.raw_command(0xc, 2, bytearray([channel, 12, 0, 0]))['data'][-4:]
|
||||
if check_gateway(s, ogw, channel):
|
||||
print('Gateway already set to {}'.format(ogw))
|
||||
return False
|
||||
print('Setting gateway to {}'.format(ogw))
|
||||
s.raw_command(0xc, 1, bytearray([channel, 12]) + gw)
|
||||
return True
|
||||
|
||||
def dotwait():
|
||||
sys.stdout.write('.')
|
||||
sys.stdout.flush()
|
||||
time.sleep(0.5)
|
||||
|
||||
def main():
|
||||
a = argparse.ArgumentParser(description='Locally configure a BMC device')
|
||||
a.add_argument('-v', help='vlan id or off to disable vlan tagging')
|
||||
a.add_argument('-p', help='Which port to use (dedicated, lom, ocp, ml2)')
|
||||
a.add_argument('-i', help='JSON configuration file to read for '
|
||||
'configuration')
|
||||
a.add_argument('-c', help='Use Confluent API to direct BMC configuration',
|
||||
action='store_true')
|
||||
args = a.parse_args()
|
||||
if args.i:
|
||||
bmccfg = json.load(open(args.i))
|
||||
elif args.c:
|
||||
bmccfgsrc = subprocess.check_output(
|
||||
[sys.executable, apiclient, '/confluent-api/self/bmcconfig', '-j'])
|
||||
bmccfg = json.loads(bmccfgsrc)
|
||||
else:
|
||||
bmccfg = {}
|
||||
if args.p is not None:
|
||||
bmccfg['bmcport'] = args.p
|
||||
if args.v is not None:
|
||||
bmccfg['bmcvlan'] = args.v
|
||||
vendor = open('/sys/devices/virtual/dmi/id/sys_vendor').read()
|
||||
vendor = vendor.strip()
|
||||
try:
|
||||
model = open('/sys/devices/virtual/dmi/id/product_sku').read().strip()
|
||||
if model == 'none':
|
||||
raise Exception('No SKU')
|
||||
except Exception:
|
||||
model = open('/sys/devices/virtual/dmi/id/product_name').read()
|
||||
if vendor in ('Lenovo', 'IBM'):
|
||||
if '[' in model and ']' in model:
|
||||
model = model.split('[')[1].split(']')[0]
|
||||
model = model[:4].lower()
|
||||
s = Session('/dev/ipmi0')
|
||||
if not bmccfg:
|
||||
print("No BMC configuration specified, exiting.")
|
||||
return
|
||||
if bmccfg.get('bmcport', None):
|
||||
channel = set_port(s, bmccfg['bmcport'], vendor, model)
|
||||
else:
|
||||
channel = get_lan_channel(s)
|
||||
awaitvlan = False
|
||||
awaitip = False
|
||||
awaitprefix = False
|
||||
awaitgw = False
|
||||
if bmccfg.get('bmcvlan', None):
|
||||
awaitvlan = set_vlan(s, bmccfg['bmcvlan'], channel)
|
||||
if bmccfg.get('bmcipv4', None):
|
||||
awaitip = set_ipv4(s, bmccfg['bmcipv4'], channel)
|
||||
if bmccfg.get('prefixv4', None):
|
||||
awaitprefix = set_subnet(s, bmccfg['prefixv4'], channel)
|
||||
if bmccfg.get('bmcgw', None):
|
||||
awaitgw = set_gateway(s, bmccfg['bmcgw'], channel)
|
||||
sys.stdout.write('Waiting for changes to take effect...')
|
||||
sys.stdout.flush()
|
||||
while awaitvlan and not check_vlan(s, bmccfg['bmcvlan'], channel):
|
||||
dotwait()
|
||||
while awaitip and not check_ipv4(s, bmccfg['bmcipv4'], channel):
|
||||
dotwait()
|
||||
while awaitprefix and not check_subnet(s, bmccfg['prefixv4'], channel):
|
||||
dotwait()
|
||||
while awaitprefix and not check_gateway(s, bmccfg['bmcgw'], channel):
|
||||
dotwait()
|
||||
sys.stdout.write('done\n')
|
||||
sys.stdout.flush()
|
||||
#await_config(s, bmccfg, channel)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -4,7 +4,7 @@ Requires=network-online.target
|
||||
After=network-online.target
|
||||
|
||||
[Service]
|
||||
ExecStart=/etc/confluent/firstboot.sh
|
||||
ExecStart=/opt/confluent/bin/firstboot.sh
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
@@ -6,17 +6,31 @@
|
||||
# the script notifies confluent that install is fully complete.
|
||||
|
||||
nodename=$(grep ^NODENAME /etc/confluent/confluent.info|awk '{print $2}')
|
||||
apikey=$(cat /etc/confluent/confluent.apikey)
|
||||
mgr=$(grep deploy_server /etc/confluent/confluent.deploycfg|awk '{print $2}')
|
||||
profile=$(grep ^profile: /etc/confluent/confluent.deploycfg|awk '{print $2}')
|
||||
cat /etc/confluent/tls/*.pem >> /etc/pki/tls/certs/ca-bundle.crt
|
||||
export nodename mgr profile
|
||||
confluent_apikey=$(cat /etc/confluent/confluent.apikey)
|
||||
confluent_mgr=$(grep deploy_server /etc/confluent/confluent.deploycfg|awk '{print $2}')
|
||||
confluent_profile=$(grep ^profile: /etc/confluent/confluent.deploycfg|awk '{print $2}')
|
||||
export nodename confluent_mgr confluent_profile
|
||||
. /etc/confluent/functions
|
||||
exec >> /var/log/confluent/confluent-firstboot.log
|
||||
exec 2>> /var/log/confluent/confluent-firstboot.log
|
||||
tail -f /var/log/confluent/confluent-firstboot.log > /dev/console &
|
||||
logshowpid=$!
|
||||
|
||||
if [ ! -f /etc/confluent/firstboot.ran ]; then
|
||||
touch /etc/confluent/firstboot.ran
|
||||
|
||||
run_remote firstboot.custom
|
||||
cat /etc/confluent/tls/*.pem >> /etc/pki/tls/certs/ca-bundle.crt
|
||||
|
||||
run_remote firstboot.custom
|
||||
# Firstboot scripts may be placed into firstboot.d, e.g. firstboot.d/01-firstaction.sh, firstboot.d/02-secondaction.sh
|
||||
run_remote_parts firstboot.d
|
||||
|
||||
curl -X POST -d 'status: complete' -H "CONFLUENT_NODENAME: $nodename" -H "CONFLUENT_APIKEY: $apikey" https://$mgr/confluent-api/self/updatestatus
|
||||
# Induce execution of remote configuration, e.g. ansible plays in ansible/firstboot.d/
|
||||
run_remote_config firstboot.d
|
||||
fi
|
||||
|
||||
curl -X POST -d 'status: complete' -H "CONFLUENT_NODENAME: $nodename" -H "CONFLUENT_APIKEY: $confluent_apikey" https://$confluent_mgr/confluent-api/self/updatestatus
|
||||
systemctl disable firstboot
|
||||
rm /etc/systemd/system/firstboot.service
|
||||
rm /etc/confluent/firstboot.ran
|
||||
kill $logshowpid
|
||||
|
||||
@@ -1,37 +1,73 @@
|
||||
function set_confluent_vars() {
|
||||
if [ -z "$mgr" ]; then
|
||||
mgr=$(grep ^deploy_server: /etc/confluent/confluent.deploycfg | sed -e 's/[^ ]*: //')
|
||||
if [ -z "$confluent_mgr" ]; then
|
||||
confluent_mgr=$(grep ^deploy_server: /etc/confluent/confluent.deploycfg | sed -e 's/[^ ]*: //')
|
||||
fi
|
||||
if [ -z "$profile" ]; then
|
||||
profile=$(grep ^profile: /etc/confluent/confluent.deploycfg | sed -e 's/[^ ]*: //')
|
||||
if [ -z "$confluent_profile" ]; then
|
||||
confluent_profile=$(grep ^profile: /etc/confluent/confluent.deploycfg | sed -e 's/[^ ]*: //')
|
||||
fi
|
||||
}
|
||||
|
||||
fetch_remote() {
|
||||
if [ -f /etc/confluent/ca.pem ]; then
|
||||
curlargs=" --cacert /etc/confluent/ca.pem"
|
||||
fi
|
||||
set_confluent_vars
|
||||
mkdir -p $(dirname $1)
|
||||
curl -f -sS $curlargs https://$mgr/confluent-public/os/$profile/scripts/$1 > $1
|
||||
if [ $? != 0 ]; then echo $1 failed to download; return 1; fi
|
||||
}
|
||||
|
||||
run_remote() {
|
||||
requestedcmd="'$*'"
|
||||
curlargs=""
|
||||
if [ -f /etc/confluent/ca.pem ]; then
|
||||
curlargs=" --cacert /etc/confluent/ca.pem"
|
||||
fi
|
||||
set_confluent_vars
|
||||
mkdir -p $(dirname $1)
|
||||
curl -f -sS $curlargs https://$confluent_mgr/confluent-public/os/$confluent_profile/scripts/$1 > $1
|
||||
if [ $? != 0 ]; then echo $1 failed to download; return 1; fi
|
||||
}
|
||||
|
||||
source_remote_parts() {
|
||||
confluentscripttmpdir=$(mktemp -d)
|
||||
scriptlist=$(/usr/libexec/platform-python /etc/confluent/apiclient /confluent-api/self/scriptlist/$1|sed -e 's/^- //')
|
||||
for script in $scriptlist; do
|
||||
source_remote $1/$script
|
||||
done
|
||||
unset confluentscripttmpdir
|
||||
}
|
||||
|
||||
run_remote_parts() {
|
||||
confluentscripttmpdir=$(mktemp -d)
|
||||
scriptlist=$(/usr/libexec/platform-python /etc/confluent/apiclient /confluent-api/self/scriptlist/$1|sed -e 's/^- //')
|
||||
for script in $scriptlist; do
|
||||
run_remote $1/$script
|
||||
done
|
||||
unset confluentscripttmpdir
|
||||
}
|
||||
|
||||
source_remote() {
|
||||
set_confluent_vars
|
||||
echo
|
||||
echo '---------------------------------------------------------------------------'
|
||||
echo Running $requestedcmd from https://$mgr/confluent-public/os/$profile/scripts/
|
||||
tmpdir=$(mktemp -d)
|
||||
echo Executing in $tmpdir
|
||||
cd $tmpdir
|
||||
mkdir -p $(dirname $1)
|
||||
curl -f -sS $curlargs https://$mgr/confluent-public/os/$profile/scripts/$1 > $1
|
||||
echo Sourcing $1 from https://$confluent_mgr/confluent-public/os/$confluent_profile/scripts/
|
||||
if [ -z "$confluentscripttmpdir" ]; then
|
||||
confluentscripttmpdir=$(mktemp -d)
|
||||
fi
|
||||
echo Sourcing from $confluentscripttmpdir
|
||||
cd $confluentscripttmpdir
|
||||
fetch_remote $1
|
||||
if [ $? != 0 ]; then echo $1 failed to download; return 1; fi
|
||||
chmod +x $1
|
||||
cmd=$1
|
||||
shift
|
||||
source $cmd
|
||||
cd - > /dev/null
|
||||
return $retcode
|
||||
}
|
||||
|
||||
run_remote() {
|
||||
requestedcmd="'$*'"
|
||||
set_confluent_vars
|
||||
echo
|
||||
echo '---------------------------------------------------------------------------'
|
||||
echo Running $requestedcmd from https://$confluent_mgr/confluent-public/os/$confluent_profile/scripts/
|
||||
if [ -z "$confluentscripttmpdir" ]; then
|
||||
confluentscripttmpdir=$(mktemp -d)
|
||||
fi
|
||||
echo Executing in $confluentscripttmpdir
|
||||
cd $confluentscripttmpdir
|
||||
fetch_remote $1
|
||||
if [ $? != 0 ]; then echo $requestedcmd failed to download; return 1; fi
|
||||
chmod +x $1
|
||||
cmd=$1
|
||||
@@ -41,7 +77,9 @@ run_remote() {
|
||||
shift
|
||||
./$cmd $*
|
||||
retcode=$?
|
||||
echo "$requestedcmd exited with code $retcode"
|
||||
if [ $retcode -ne 0 ]; then
|
||||
echo "$requestedcmd exited with code $retcode"
|
||||
fi
|
||||
cd - > /dev/null
|
||||
return $retcode
|
||||
}
|
||||
@@ -53,12 +91,12 @@ run_remote_python() {
|
||||
curlargs=" --cacert /etc/confluent/ca.pem"
|
||||
fi
|
||||
echo '---------------------------------------------------------------------------'
|
||||
echo Running python script "'$*'" from https://$mgr/confluent-public/os/$profile/scripts/
|
||||
echo Running python script "'$*'" from https://$confluent_mgr/confluent-public/os/$confluent_profile/scripts/
|
||||
tmpdir=$(mktemp -d)
|
||||
echo Executing in $tmpdir
|
||||
cd $tmpdir
|
||||
mkdir -p $(dirname $1)
|
||||
curl -f -sS $curlargs https://$mgr/confluent-public/os/$profile/scripts/$1 > $1
|
||||
curl -f -sS $curlargs https://$confluent_mgr/confluent-public/os/$confluent_profile/scripts/$1 > $1
|
||||
if [ $? != 0 ]; then echo "'$*'" failed to download; return 1; fi
|
||||
if [ -x /usr/libexec/platform-python ]; then
|
||||
/usr/libexec/platform-python $*
|
||||
@@ -70,3 +108,26 @@ run_remote_python() {
|
||||
cd - > /dev/null
|
||||
return $retcode
|
||||
}
|
||||
|
||||
run_remote_config() {
|
||||
echo
|
||||
set_confluent_vars
|
||||
apiclient=/opt/confluent/bin/apiclient
|
||||
if [ -f /etc/confluent/apiclient ]; then
|
||||
apiclient=/etc/confluent/apiclient
|
||||
fi
|
||||
echo '---------------------------------------------------------------------------'
|
||||
echo Requesting to run remote configuration for "'$*'" from $confluent_mgr under profile $confluent_profile
|
||||
if [ -x /usr/libexec/platform-python ]; then
|
||||
/usr/libexec/platform-python $apiclient /confluent-api/self/remoteconfig/"$*" -d {}
|
||||
/usr/libexec/platform-python $apiclient /confluent-api/self/remoteconfig/status -w 204
|
||||
else
|
||||
/usr/bin/python $apiclient /confluent-api/self/remoteconfig/"$*" -d {}
|
||||
/usr/bin/python $apiclient /confluent-api/self/remoteconfig/status -w 204
|
||||
fi
|
||||
echo
|
||||
echo 'Completed remote configuration'
|
||||
echo '---------------------------------------------------------------------------'
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,6 @@
|
||||
# This is a convenient place to keep customizations separate from modifying the stock scripts
|
||||
# While modification of the stock scripts is fine, it may be easier to rebase to a newer
|
||||
# stock profile if the '.custom' files are used.
|
||||
|
||||
# An example for installing OFED for infiniband follows (see the file for more detail):
|
||||
#run_remote infiniband/mofed.post
|
||||
|
||||
|
||||
@@ -1,20 +1,22 @@
|
||||
#!/bin/sh
|
||||
# need to copy over ssh key info
|
||||
mkdir -p /var/log/confluent
|
||||
exec >> /var/log/confluent/confluent-post.log
|
||||
exec 2>> /var/log/confluent/confluent-post.log
|
||||
tail -f /var/log/confluent/confluent-post.log > /dev/tty &
|
||||
logshowpid=$!
|
||||
nodename=$(grep ^NODENAME /etc/confluent/confluent.info|awk '{print $2}')
|
||||
apikey=$(cat /etc/confluent/confluent.apikey)
|
||||
|
||||
chmod 700 /etc/confluent
|
||||
chmod og-rwx /etc/confluent/*
|
||||
|
||||
export mgr profile nodename
|
||||
export confluent_mgr confluent_profile nodename
|
||||
. /etc/confluent/functions
|
||||
|
||||
curl -X POST -d 'status: staged' -H "CONFLUENT_NODENAME: $nodename" -H "CONFLUENT_APIKEY: $apikey" https://$mgr/confluent-api/self/updatestatus
|
||||
|
||||
|
||||
if [ -f /tmp/cryptboot ]; then
|
||||
run_remote tpm_luks.sh
|
||||
fi
|
||||
|
||||
# By default, the install repository is ignored, change
|
||||
# this by manually adding local repositories
|
||||
|
||||
@@ -30,6 +32,18 @@ run_remote_python add_local_repositories
|
||||
# run_remote_python will use the appropriate python interpreter path to run the specified script
|
||||
# A post.custom is provided to more conveniently hold customizations, see the post.custom file.
|
||||
|
||||
# This will induce server side processing of the syncfile contents if
|
||||
# present
|
||||
run_remote_python syncfileclient
|
||||
|
||||
# run_remote example.sh
|
||||
# run_remote_python example.py
|
||||
run_remote post.custom
|
||||
|
||||
# Also, scripts may be placed into 'post.d', e.g. post.d/01-runfirst.sh, post.d/02-runsecond.sh
|
||||
run_remote_parts post.d
|
||||
|
||||
# Induce execution of remote configuration, e.g. ansible plays in ansible/post.d/
|
||||
run_remote_config post.d
|
||||
curl -sf -X POST -d 'status: staged' -H "CONFLUENT_NODENAME: $nodename" -H "CONFLUENT_APIKEY: $apikey" https://$confluent_mgr/confluent-api/self/updatestatus
|
||||
kill $logshowpid
|
||||
|
||||
@@ -11,4 +11,3 @@
|
||||
#Some addons improve efficiency by adding dependencies during install
|
||||
#here is an example for adding OFED install prereqs to the install
|
||||
#run_remote infiniband/mofed.pre
|
||||
|
||||
|
||||
@@ -7,7 +7,14 @@
|
||||
# If you want to use a more custom partition plan, the easiest
|
||||
# method is to edit the kicktstart file and comment out or
|
||||
# delete %include /tmp/partitioning
|
||||
|
||||
if [ -f "/run/install/cmdline.d/01-autocons.conf" ]; then
|
||||
consoledev=$(cat /run/install/cmdline.d/01-autocons.conf | sed -e 's!console=!/dev/!' -e 's/,.*//')
|
||||
TMUX= tmux a <> $consoledev >&0 2>&1 &
|
||||
fi
|
||||
exec >> /tmp/confluent-pre.log
|
||||
exec 2>> /tmp/confluent-pre.log
|
||||
tail -f /tmp/confluent-pre.log > /dev/tty &
|
||||
logshowpid=$!
|
||||
nodename=$(grep ^NODENAME /etc/confluent/confluent.info|awk '{print $2}')
|
||||
locale=$(grep ^locale: /etc/confluent/confluent.deploycfg)
|
||||
locale=${locale#locale: }
|
||||
@@ -28,7 +35,7 @@ if [ "$rootpw" = null ]; then
|
||||
else
|
||||
echo "rootpw --iscrypted $rootpw" > /tmp/rootpw
|
||||
fi
|
||||
curl -f https://$mgr/confluent-public/os/$profile/profile.yaml > /tmp/instprofile.yaml
|
||||
curl -sf https://$confluent_mgr/confluent-public/os/$confluent_profile/profile.yaml > /tmp/instprofile.yaml
|
||||
blargs=$(grep ^installedargs: /tmp/instprofile.yaml | sed -e 's/#.*//' -e 's/^installedargs: //')
|
||||
if [ ! -z "$blargs" ]; then
|
||||
blargs=' --append="'$blargs'"'
|
||||
@@ -43,33 +50,33 @@ if [ ! -z "$blargs" ]; then
|
||||
echo "bootloader $blargs" > /tmp/grubpw
|
||||
fi
|
||||
ssh-keygen -A
|
||||
for pubkey in /etc/ssh/ssh_host_*_key.pub; do
|
||||
for pubkey in /etc/ssh/ssh_host*key.pub; do
|
||||
certfile=${pubkey/.pub/-cert.pub}
|
||||
curl -f -X POST -H "CONFLUENT_NODENAME: $nodename" -H "CONFLUENT_APIKEY: $(cat /etc/confluent/confluent.apikey)" -d @$pubkey https://$mgr/confluent-api/self/sshcert > $certfile
|
||||
curl -sf -X POST -H "CONFLUENT_NODENAME: $nodename" -H "CONFLUENT_APIKEY: $(cat /etc/confluent/confluent.apikey)" -d @$pubkey https://$confluent_mgr/confluent-api/self/sshcert > $certfile
|
||||
echo HostCertificate $certfile >> /etc/ssh/sshd_config.anaconda
|
||||
done
|
||||
/usr/sbin/sshd -f /etc/ssh/sshd_config.anaconda
|
||||
if [ -f "/run/install/cmdline.d/01-autocons.conf" ]; then
|
||||
consoledev=$(cat /run/install/cmdline.d/01-autocons.conf | sed -e 's!console=!/dev/!' -e 's/,.*//')
|
||||
TMUX= tmux a <> $consoledev >&0 2>&1 &
|
||||
fi
|
||||
touch /tmp/addonpackages
|
||||
cryptboot=$(grep ^encryptboot: /etc/confluent/confluent.deploycfg | awk '{print $2}')
|
||||
LUKSPARTY=''
|
||||
touch /tmp/addonpackages
|
||||
if [ "$cryptboot" == "tpm2" ]; then
|
||||
LUKSPARTY="--encrypted --passphrase=$(cat /etc/confluent/confluent.apikey)"
|
||||
echo $cryptboot >> /tmp/cryptboot
|
||||
fi
|
||||
|
||||
|
||||
export mgr profile nodename
|
||||
curl -f https://$mgr/confluent-public/os/$profile/scripts/functions > /tmp/functions
|
||||
export confluent_mgr confluent_profile nodename
|
||||
curl -sf https://$confluent_mgr/confluent-public/os/$confluent_profile/scripts/functions > /tmp/functions
|
||||
. /tmp/functions
|
||||
run_remote_python getinstalldisk
|
||||
if [ -e /tmp/installdisk ]; then
|
||||
run_remote pre.custom
|
||||
run_remote_parts pre.d
|
||||
if [ ! -e /tmp/installdisk ]; then
|
||||
run_remote_python getinstalldisk
|
||||
fi
|
||||
if [ -e /tmp/installdisk -a ! -e /tmp/partitioning ]; then
|
||||
echo clearpart --all --initlabel >> /tmp/partitioning
|
||||
echo ignoredisk --only-use $(cat /tmp/installdisk) >> /tmp/partitioning
|
||||
echo autopart --nohome $LUKSPARTY >> /tmp/partitioning
|
||||
fi
|
||||
python /etc/confluent/apiclient /confluent-public/os/$profile/kickstart.custom -o /tmp/kickstart.custom
|
||||
run_remote pre.custom
|
||||
python /etc/confluent/apiclient /confluent-public/os/$confluent_profile/kickstart.custom -o /tmp/kickstart.custom
|
||||
kill $logshowpid
|
||||
|
||||
@@ -8,11 +8,22 @@
|
||||
# for customization, which will run in a more normal mechanism
|
||||
|
||||
nodename=$(grep ^NODENAME /etc/confluent/confluent.info|awk '{print $2}')
|
||||
export mgr profile nodename
|
||||
export confluent_mgr confluent_profile nodename
|
||||
cp -a /etc/confluent /mnt/sysimage/etc
|
||||
chmod -R og-rwx /mnt/sysimage/etc/confluent
|
||||
cp /tmp/functions /mnt/sysimage/etc/confluent/
|
||||
hostnamectl set-hostname $nodename
|
||||
cp /etc/hostname /mnt/sysimage/etc/hostname
|
||||
. /tmp/functions
|
||||
cp /tmp/cryptboot /mnt/sysimage/tmp/
|
||||
if [ -f /tmp/cryptboot ]; then
|
||||
cp /tmp/cryptboot /mnt/sysimage/tmp/
|
||||
fi
|
||||
mkdir -p /mnt/sysimage/var/log/confluent
|
||||
mv /tmp/confluent-pre.log /mnt/sysimage/var/log/confluent
|
||||
echo Port 2222 >> /etc/ssh/sshd_config.anaconda
|
||||
echo Match LocalPort 22 >> /etc/ssh/sshd_config.anaconda
|
||||
echo " ChrootDirectory /mnt/sysimage" >> /etc/ssh/sshd_config.anaconda
|
||||
kill -HUP $(cat /run/sshd.pid)
|
||||
|
||||
# Preserve the ssh setup work done for the installer
|
||||
# by copying into the target system and setting up
|
||||
|
||||
@@ -18,6 +18,6 @@ chmod 700 /mnt/sysimage/root/.ssh/
|
||||
cp /root/.ssh/authorized_keys /mnt/sysimage/root/.ssh/
|
||||
chmod 600 /mnt/sysimage/root/.ssh/authorized_keys
|
||||
cp /etc/ssh/ssh_known_hosts /mnt/sysimage/etc/ssh/
|
||||
curl -f -H "CONFLUENT_NODENAME: $nodename" -H "CONFLUENT_APIKEY: $(cat /etc/confluent/confluent.apikey)" https://$mgr/confluent-api/self/nodelist > /tmp/allnodes
|
||||
curl -f -H "CONFLUENT_NODENAME: $nodename" -H "CONFLUENT_APIKEY: $(cat /etc/confluent/confluent.apikey)" https://$confluent_mgr/confluent-api/self/nodelist > /tmp/allnodes
|
||||
cp /tmp/allnodes /mnt/sysimage/etc/ssh/shosts.equiv
|
||||
cp /tmp/allnodes /mnt/sysimage/root/.shosts
|
||||
|
||||
@@ -0,0 +1,225 @@
|
||||
#!/usr/bin/python
|
||||
import importlib
|
||||
import tempfile
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
try:
|
||||
from importlib.machinery import SourceFileLoader
|
||||
def load_source(mod, path):
|
||||
return SourceFileLoader(mod, path).load_module()
|
||||
except ImportError:
|
||||
from imp import load_source
|
||||
|
||||
try:
|
||||
apiclient = load_source('apiclient', '/opt/confluent/bin/apiclient')
|
||||
except IOError:
|
||||
apiclient = load_source('apiclient', '/etc/confluent/apiclient')
|
||||
|
||||
|
||||
def partitionhostsline(line):
|
||||
comment = ''
|
||||
try:
|
||||
cmdidx = line.index('#')
|
||||
comment = line[cmdidx:]
|
||||
line = line[:cmdidx].strip()
|
||||
except ValueError:
|
||||
pass
|
||||
if not line:
|
||||
return '', [], comment
|
||||
ipaddr, names = line.split(maxsplit=1)
|
||||
names = names.split()
|
||||
return ipaddr, names, comment
|
||||
|
||||
class HostMerger:
|
||||
def __init__(self):
|
||||
self.byip = {}
|
||||
self.byname = {}
|
||||
self.sourcelines = []
|
||||
self.targlines = []
|
||||
|
||||
def read_source(self, sourcefile):
|
||||
with open(sourcefile, 'r') as hfile:
|
||||
self.sourcelines = hfile.read().split('\n')
|
||||
while not self.sourcelines[-1]:
|
||||
self.sourcelines = self.sourcelines[:-1]
|
||||
for x in range(len(self.sourcelines)):
|
||||
line = self.sourcelines[x]
|
||||
currip, names, comment = partitionhostsline(line)
|
||||
if currip:
|
||||
self.byip[currip] = x
|
||||
for name in names:
|
||||
self.byname[name] = x
|
||||
|
||||
def read_target(self, targetfile):
|
||||
with open(targetfile, 'r') as hfile:
|
||||
lines = hfile.read().split('\n')
|
||||
if not lines[-1]:
|
||||
lines = lines[:-1]
|
||||
for y in range(len(lines)):
|
||||
line = lines[y]
|
||||
currip, names, comment = partitionhostsline(line)
|
||||
if currip in self.byip:
|
||||
x = self.byip[currip]
|
||||
if self.sourcelines[x] is None:
|
||||
# have already consumed this enntry
|
||||
continue
|
||||
self.targlines.append(self.sourcelines[x])
|
||||
self.sourcelines[x] = None
|
||||
continue
|
||||
for name in names:
|
||||
if name in self.byname:
|
||||
x = self.byname[name]
|
||||
if self.sourcelines[x] is None:
|
||||
break
|
||||
self.targlines.append(self.sourcelines[x])
|
||||
self.sourcelines[x] = None
|
||||
break
|
||||
else:
|
||||
self.targlines.append(line)
|
||||
|
||||
def write_out(self, targetfile):
|
||||
while not self.targlines[-1]:
|
||||
self.targlines = self.targlines[:-1]
|
||||
while not self.sourcelines[-1]:
|
||||
self.sourcelines = self.sourcelines[:-1]
|
||||
with open(targetfile, 'w') as hosts:
|
||||
for line in self.targlines:
|
||||
hosts.write(line + '\n')
|
||||
for line in self.sourcelines:
|
||||
if line is not None:
|
||||
hosts.write(line + '\n')
|
||||
|
||||
|
||||
class CredMerger:
|
||||
def __init__(self):
|
||||
try:
|
||||
with open('/etc/login.defs', 'r') as ldefs:
|
||||
defs = ldefs.read().split('\n')
|
||||
except FileNotFoundError:
|
||||
defs = []
|
||||
lkup = {}
|
||||
self.discardnames = {}
|
||||
self.shadowednames = {}
|
||||
for line in defs:
|
||||
try:
|
||||
line = line[:line.index('#')]
|
||||
except ValueError:
|
||||
pass
|
||||
keyval = line.split()
|
||||
if len(keyval) < 2:
|
||||
continue
|
||||
lkup[keyval[0]] = keyval[1]
|
||||
self.uidmin = int(lkup.get('UID_MIN', 1000))
|
||||
self.uidmax = int(lkup.get('UID_MAX', 60000))
|
||||
self.gidmin = int(lkup.get('GID_MIN', 1000))
|
||||
self.gidmax = int(lkup.get('GID_MAX', 60000))
|
||||
self.shadowlines = None
|
||||
|
||||
def read_passwd(self, source, targfile=False):
|
||||
self.read_generic(source, self.uidmin, self.uidmax, targfile)
|
||||
|
||||
def read_group(self, source, targfile=False):
|
||||
self.read_generic(source, self.gidmin, self.gidmax, targfile)
|
||||
|
||||
def read_generic(self, source, minid, maxid, targfile):
|
||||
if targfile:
|
||||
self.targdata = []
|
||||
else:
|
||||
self.sourcedata = []
|
||||
with open(source, 'r') as inputfile:
|
||||
for line in inputfile.read().split('\n'):
|
||||
try:
|
||||
name, _, uid, _ = line.split(':', 3)
|
||||
uid = int(uid)
|
||||
except ValueError:
|
||||
continue
|
||||
if targfile:
|
||||
if uid <= minid or uid >= maxid:
|
||||
self.targdata.append(line)
|
||||
else:
|
||||
self.discardnames[name] = 1
|
||||
else:
|
||||
if uid >= minid and uid <= maxid:
|
||||
self.sourcedata.append(line)
|
||||
|
||||
def read_shadow(self, source):
|
||||
self.shadowlines = []
|
||||
try:
|
||||
with open(source, 'r') as inshadow:
|
||||
for line in inshadow.read().split('\n'):
|
||||
try:
|
||||
name, _ = line.split(':' , 1)
|
||||
except ValueError:
|
||||
continue
|
||||
if name in self.discardnames:
|
||||
continue
|
||||
self.shadowednames[name] = 1
|
||||
self.shadowlines.append(line)
|
||||
except FileNotFoundError:
|
||||
return
|
||||
|
||||
def write_out(self, outfile):
|
||||
with open(outfile, 'w') as targ:
|
||||
for line in self.targdata:
|
||||
targ.write(line + '\n')
|
||||
for line in self.sourcedata:
|
||||
targ.write(line + '\n')
|
||||
if outfile == '/etc/passwd':
|
||||
if self.shadowlines is None:
|
||||
self.read_shadow('/etc/shadow')
|
||||
with open('/etc/shadow', 'w') as shadout:
|
||||
for line in self.shadowlines:
|
||||
shadout.write(line + '\n')
|
||||
for line in self.sourcedata:
|
||||
name, _ = line.split(':', 1)
|
||||
if name in self.shadowednames:
|
||||
continue
|
||||
shadout.write(name + ':!:::::::\n')
|
||||
if outfile == '/etc/group':
|
||||
if self.shadowlines is None:
|
||||
self.read_shadow('/etc/gshadow')
|
||||
with open('/etc/gshadow', 'w') as shadout:
|
||||
for line in self.shadowlines:
|
||||
shadout.write(line + '\n')
|
||||
for line in self.sourcedata:
|
||||
name, _ = line.split(':' , 1)
|
||||
if name in self.shadowednames:
|
||||
continue
|
||||
shadout.write(name + ':!::\n')
|
||||
|
||||
def synchronize():
|
||||
tmpdir = tempfile.mkdtemp()
|
||||
try:
|
||||
ac = apiclient.HTTPSClient()
|
||||
data = json.dumps({'merge': tmpdir})
|
||||
status, rsp = ac.grab_url_with_status('/confluent-api/self/remotesyncfiles', data)
|
||||
if status == 202:
|
||||
while status != 204:
|
||||
status, rsp = ac.grab_url_with_status('/confluent-api/self/remotesyncfiles')
|
||||
if not isinstance(rsp, str):
|
||||
rsp = rsp.decode('utf8')
|
||||
pendpasswd = os.path.join(tmpdir, 'etc/passwd')
|
||||
if os.path.exists(pendpasswd):
|
||||
cm = CredMerger()
|
||||
cm.read_passwd(pendpasswd, targfile=False)
|
||||
cm.read_passwd('/etc/passwd', targfile=True)
|
||||
cm.write_out('/etc/passwd')
|
||||
pendgroup = os.path.join(tmpdir, 'etc/group')
|
||||
if os.path.exists(pendgroup):
|
||||
cm = CredMerger()
|
||||
cm.read_group(pendgroup, targfile=False)
|
||||
cm.read_group('/etc/group', targfile=True)
|
||||
cm.write_out('/etc/group')
|
||||
pendhosts = os.path.join(tmpdir, 'etc/hosts')
|
||||
if os.path.exists(pendhosts):
|
||||
cm = HostMerger()
|
||||
cm.read_source(pendhosts)
|
||||
cm.read_target('/etc/hosts')
|
||||
cm.write_out('/etc/hosts')
|
||||
finally:
|
||||
shutil.rmtree(tmpdir)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
synchronize()
|
||||
@@ -0,0 +1,13 @@
|
||||
# This file lists files to synchronize or merge to the deployed systems from the deployment server
|
||||
# To specify taking /some/path/hosts on the deploymennt server and duplicating it to /etc/hosts:
|
||||
# /some/path/hosts -> /etc/hosts
|
||||
|
||||
# If wanting to simply use the same path for source and destinaiton, the -> may be skipped:
|
||||
# /etc/hosts
|
||||
|
||||
# Entries under MERGE will attempt to be intelligently merged. This supports /etc/group and /etc/passwd
|
||||
# Any supporting entries in /etc/shadow or /etc/gshadow are added automatically, with password disabled
|
||||
# It also will not inject 'system' ids (under 1,000 usually) as those tend to be local and rpm managed.
|
||||
MERGE:
|
||||
# /etc/passwd
|
||||
# /etc/group
|
||||
@@ -0,0 +1,25 @@
|
||||
Ansible playbooks ending in .yml or .yaml that are placed into this directory will be executed at the
|
||||
appropriate phase of the install process.
|
||||
|
||||
The 'hosts' may be omitted, and if included will be ignored, replaced with the host that is specifically
|
||||
requesting the playbooks be executed.
|
||||
|
||||
Also, the playbooks will be executed on the deployment server. Hence it may be slower in aggregate than
|
||||
running content under scripts/ which ask much less of the deployment server
|
||||
|
||||
Here is an example of what a playbook would look like broadly:
|
||||
|
||||
- name: Example
|
||||
gather_facts: no
|
||||
tasks:
|
||||
- name: Example1
|
||||
lineinfile:
|
||||
path: /etc/hosts
|
||||
line: 1.2.3.4 test1
|
||||
create: yes
|
||||
- name: Example2
|
||||
lineinfile:
|
||||
path: /etc/hosts
|
||||
line: 1.2.3.5 test2
|
||||
create: yes
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
Ansible playbooks ending in .yml or .yaml that are placed into this directory will be executed at the
|
||||
appropriate phase of the install process.
|
||||
|
||||
The 'hosts' may be omitted, and if included will be ignored, replaced with the host that is specifically
|
||||
requesting the playbooks be executed.
|
||||
|
||||
Also, the playbooks will be executed on the deployment server. Hence it may be slower in aggregate than
|
||||
running content under scripts/ which ask much less of the deployment server
|
||||
|
||||
Here is an example of what a playbook would look like broadly:
|
||||
|
||||
- name: Example
|
||||
gather_facts: no
|
||||
tasks:
|
||||
- name: Example1
|
||||
lineinfile:
|
||||
path: /etc/hosts
|
||||
line: 1.2.3.4 test1
|
||||
create: yes
|
||||
- name: Example2
|
||||
lineinfile:
|
||||
path: /etc/hosts
|
||||
line: 1.2.3.5 test2
|
||||
create: yes
|
||||
|
||||
@@ -52,23 +52,23 @@ pciutils
|
||||
%include /tmp/kickstart.custom
|
||||
|
||||
%pre
|
||||
profile=$(grep ^profile: /etc/confluent/confluent.deploycfg |awk '{print $2}')
|
||||
mgr=$(grep deploy_server /etc/confluent/confluent.deploycfg |awk '{print $2}')
|
||||
curl -f https://$mgr/confluent-public/os/$profile/scripts/pre.sh > /tmp/preinst.sh
|
||||
confluent_profile=$(grep ^profile: /etc/confluent/confluent.deploycfg |awk '{print $2}')
|
||||
confluent_mgr=$(grep deploy_server /etc/confluent/confluent.deploycfg |awk '{print $2}')
|
||||
curl -f https://$confluent_mgr/confluent-public/os/$confluent_profile/scripts/pre.sh > /tmp/preinst.sh
|
||||
. /tmp/preinst.sh
|
||||
%end
|
||||
|
||||
%post --nochroot
|
||||
mkdir -p /mnt/sysimage/etc/confluent
|
||||
profile=$(grep ^profile: /etc/confluent/confluent.deploycfg |awk '{print $2}')
|
||||
mgr=$(grep deploy_server /etc/confluent/confluent.deploycfg |awk '{print $2}')
|
||||
curl -f https://$mgr/confluent-public/os/$profile/scripts/prechroot.sh > /tmp/postinst.sh
|
||||
confluent_profile=$(grep ^profile: /etc/confluent/confluent.deploycfg |awk '{print $2}')
|
||||
confluent_mgr=$(grep deploy_server /etc/confluent/confluent.deploycfg |awk '{print $2}')
|
||||
curl -f https://$confluent_mgr/confluent-public/os/$confluent_profile/scripts/prechroot.sh > /tmp/postinst.sh
|
||||
. /tmp/postinst.sh
|
||||
|
||||
# Hook firstboot.sh
|
||||
curl -f https://$mgr/confluent-public/os/$profile/scripts/firstboot.service > /mnt/sysimage/etc/systemd/system/firstboot.service
|
||||
curl -f https://$confluent_mgr/confluent-public/os/$confluent_profile/scripts/firstboot.service > /mnt/sysimage/etc/systemd/system/firstboot.service
|
||||
mkdir -p /mnt/sysimage/opt/confluent/bin
|
||||
curl -f https://$mgr/confluent-public/os/$profile/scripts/firstboot.sh > /mnt/sysimage/opt/confluent/bin/firstboot.sh
|
||||
curl -f https://$confluent_mgr/confluent-public/os/$confluent_profile/scripts/firstboot.sh > /mnt/sysimage/opt/confluent/bin/firstboot.sh
|
||||
chmod +x /mnt/sysimage/opt/confluent/bin/firstboot.sh
|
||||
%end
|
||||
|
||||
@@ -77,8 +77,8 @@ cat /etc/confluent/tls/*.pem >> /etc/pki/tls/certs/ca-bundle.crt
|
||||
systemctl enable firstboot
|
||||
chgrp ssh_keys /etc/ssh/ssh*key
|
||||
restorecon /etc/ssh/ssh*key /root/.shosts /etc/ssh/shosts.equiv /etc/ssh/ssh_config.d/* /opt/confluent/bin/firstboot.sh
|
||||
profile=$(grep ^profile: /etc/confluent/confluent.deploycfg |awk '{print $2}')
|
||||
mgr=$(grep deploy_server /etc/confluent/confluent.deploycfg |awk '{print $2}')
|
||||
curl -f https://$mgr/confluent-public/os/$profile/scripts/post.sh > /tmp/postinst.sh
|
||||
confluent_profile=$(grep ^profile: /etc/confluent/confluent.deploycfg |awk '{print $2}')
|
||||
confluent_mgr=$(grep deploy_server /etc/confluent/confluent.deploycfg |awk '{print $2}')
|
||||
curl -f https://$confluent_mgr/confluent-public/os/$confluent_profile/scripts/post.sh > /tmp/postinst.sh
|
||||
. /tmp/postinst.sh
|
||||
%end
|
||||
|
||||
@@ -6,19 +6,31 @@
|
||||
# the script notifies confluent that install is fully complete.
|
||||
|
||||
nodename=$(grep ^NODENAME /etc/confluent/confluent.info|awk '{print $2}')
|
||||
apikey=$(cat /etc/confluent/confluent.apikey)
|
||||
mgr=$(grep deploy_server /etc/confluent/confluent.deploycfg|awk '{print $2}')
|
||||
profile=$(grep ^profile: /etc/confluent/confluent.deploycfg|awk '{print $2}')
|
||||
cat /etc/confluent/tls/*.pem >> /etc/pki/tls/certs/ca-bundle.crt
|
||||
export nodename mgr profile
|
||||
confluent_apikey=$(cat /etc/confluent/confluent.apikey)
|
||||
confluent_mgr=$(grep deploy_server /etc/confluent/confluent.deploycfg|awk '{print $2}')
|
||||
confluent_profile=$(grep ^profile: /etc/confluent/confluent.deploycfg|awk '{print $2}')
|
||||
export nodename confluent_mgr confluent_profile
|
||||
. /etc/confluent/functions
|
||||
exec >> /var/log/confluent/confluent-firstboot.log
|
||||
exec 2>> /var/log/confluent/confluent-firstboot.log
|
||||
tail -f /var/log/confluent/confluent-firstboot.log > /dev/console &
|
||||
logshowpid=$!
|
||||
|
||||
if [ ! -f /etc/confluent/firstboot.ran ]; then
|
||||
touch /etc/confluent/firstboot.ran
|
||||
|
||||
run_remote firstboot.custom
|
||||
# Firstboot scripts may be placed into firstboot.d, e.g. firstboot.d/01-firstaction.sh, firstboot.d/02-secondaction.sh
|
||||
run_remote_parts firstboot
|
||||
cat /etc/confluent/tls/*.pem >> /etc/pki/tls/certs/ca-bundle.crt
|
||||
|
||||
run_remote firstboot.custom
|
||||
# Firstboot scripts may be placed into firstboot.d, e.g. firstboot.d/01-firstaction.sh, firstboot.d/02-secondaction.sh
|
||||
run_remote_parts firstboot.d
|
||||
|
||||
curl -X POST -d 'status: complete' -H "CONFLUENT_NODENAME: $nodename" -H "CONFLUENT_APIKEY: $apikey" https://$mgr/confluent-api/self/updatestatus
|
||||
# Induce execution of remote configuration, e.g. ansible plays in ansible/firstboot.d/
|
||||
run_remote_config firstboot.d
|
||||
fi
|
||||
|
||||
curl -X POST -d 'status: complete' -H "CONFLUENT_NODENAME: $nodename" -H "CONFLUENT_APIKEY: $confluent_apikey" https://$confluent_mgr/confluent-api/self/updatestatus
|
||||
systemctl disable firstboot
|
||||
rm /etc/systemd/system/firstboot.service
|
||||
rm /etc/confluent/firstboot.ran
|
||||
kill $logshowpid
|
||||
|
||||
@@ -1,44 +1,73 @@
|
||||
function set_confluent_vars() {
|
||||
if [ -z "$mgr" ]; then
|
||||
mgr=$(grep ^deploy_server: /etc/confluent/confluent.deploycfg | sed -e 's/[^ ]*: //')
|
||||
if [ -z "$confluent_mgr" ]; then
|
||||
confluent_mgr=$(grep ^deploy_server: /etc/confluent/confluent.deploycfg | sed -e 's/[^ ]*: //')
|
||||
fi
|
||||
if [ -z "$profile" ]; then
|
||||
profile=$(grep ^profile: /etc/confluent/confluent.deploycfg | sed -e 's/[^ ]*: //')
|
||||
if [ -z "$confluent_profile" ]; then
|
||||
confluent_profile=$(grep ^profile: /etc/confluent/confluent.deploycfg | sed -e 's/[^ ]*: //')
|
||||
fi
|
||||
}
|
||||
|
||||
fetch_remote() {
|
||||
if [ -f /etc/confluent/ca.pem ]; then
|
||||
curlargs=" --cacert /etc/confluent/ca.pem"
|
||||
fi
|
||||
set_confluent_vars
|
||||
mkdir -p $(dirname $1)
|
||||
curl -f -sS $curlargs https://$mgr/confluent-public/os/$profile/scripts/$1 > $1
|
||||
if [ $? != 0 ]; then echo $1 failed to download; return 1; fi
|
||||
}
|
||||
|
||||
run_remote_parts() {
|
||||
scriptlist=$(/usr/libexec/platform-python /etc/confluent/apiclient /confluent-api/self/scriptlist/$1|sed -e 's/^- //')
|
||||
for script in $scriptlist; do
|
||||
run_remote $1.d/$script
|
||||
done
|
||||
}
|
||||
|
||||
run_remote() {
|
||||
requestedcmd="'$*'"
|
||||
curlargs=""
|
||||
if [ -f /etc/confluent/ca.pem ]; then
|
||||
curlargs=" --cacert /etc/confluent/ca.pem"
|
||||
fi
|
||||
set_confluent_vars
|
||||
mkdir -p $(dirname $1)
|
||||
curl -f -sS $curlargs https://$confluent_mgr/confluent-public/os/$confluent_profile/scripts/$1 > $1
|
||||
if [ $? != 0 ]; then echo $1 failed to download; return 1; fi
|
||||
}
|
||||
|
||||
source_remote_parts() {
|
||||
confluentscripttmpdir=$(mktemp -d)
|
||||
scriptlist=$(/usr/libexec/platform-python /etc/confluent/apiclient /confluent-api/self/scriptlist/$1|sed -e 's/^- //')
|
||||
for script in $scriptlist; do
|
||||
source_remote $1/$script
|
||||
done
|
||||
unset confluentscripttmpdir
|
||||
}
|
||||
|
||||
run_remote_parts() {
|
||||
confluentscripttmpdir=$(mktemp -d)
|
||||
scriptlist=$(/usr/libexec/platform-python /etc/confluent/apiclient /confluent-api/self/scriptlist/$1|sed -e 's/^- //')
|
||||
for script in $scriptlist; do
|
||||
run_remote $1/$script
|
||||
done
|
||||
unset confluentscripttmpdir
|
||||
}
|
||||
|
||||
source_remote() {
|
||||
set_confluent_vars
|
||||
echo
|
||||
echo '---------------------------------------------------------------------------'
|
||||
echo Running $requestedcmd from https://$mgr/confluent-public/os/$profile/scripts/
|
||||
tmpdir=$(mktemp -d)
|
||||
echo Executing in $tmpdir
|
||||
cd $tmpdir
|
||||
mkdir -p $(dirname $1)
|
||||
curl -f -sS $curlargs https://$mgr/confluent-public/os/$profile/scripts/$1 > $1
|
||||
echo Sourcing $1 from https://$confluent_mgr/confluent-public/os/$confluent_profile/scripts/
|
||||
if [ -z "$confluentscripttmpdir" ]; then
|
||||
confluentscripttmpdir=$(mktemp -d)
|
||||
fi
|
||||
echo Sourcing from $confluentscripttmpdir
|
||||
cd $confluentscripttmpdir
|
||||
fetch_remote $1
|
||||
if [ $? != 0 ]; then echo $1 failed to download; return 1; fi
|
||||
chmod +x $1
|
||||
cmd=$1
|
||||
shift
|
||||
source $cmd
|
||||
cd - > /dev/null
|
||||
return $retcode
|
||||
}
|
||||
|
||||
run_remote() {
|
||||
requestedcmd="'$*'"
|
||||
set_confluent_vars
|
||||
echo
|
||||
echo '---------------------------------------------------------------------------'
|
||||
echo Running $requestedcmd from https://$confluent_mgr/confluent-public/os/$confluent_profile/scripts/
|
||||
if [ -z "$confluentscripttmpdir" ]; then
|
||||
confluentscripttmpdir=$(mktemp -d)
|
||||
fi
|
||||
echo Executing in $confluentscripttmpdir
|
||||
cd $confluentscripttmpdir
|
||||
fetch_remote $1
|
||||
if [ $? != 0 ]; then echo $requestedcmd failed to download; return 1; fi
|
||||
chmod +x $1
|
||||
cmd=$1
|
||||
@@ -48,7 +77,9 @@ run_remote() {
|
||||
shift
|
||||
./$cmd $*
|
||||
retcode=$?
|
||||
echo "$requestedcmd exited with code $retcode"
|
||||
if [ $retcode -ne 0 ]; then
|
||||
echo "$requestedcmd exited with code $retcode"
|
||||
fi
|
||||
cd - > /dev/null
|
||||
return $retcode
|
||||
}
|
||||
@@ -60,16 +91,43 @@ run_remote_python() {
|
||||
curlargs=" --cacert /etc/confluent/ca.pem"
|
||||
fi
|
||||
echo '---------------------------------------------------------------------------'
|
||||
echo Running python script "'$*'" from https://$mgr/confluent-public/os/$profile/scripts/
|
||||
echo Running python script "'$*'" from https://$confluent_mgr/confluent-public/os/$confluent_profile/scripts/
|
||||
tmpdir=$(mktemp -d)
|
||||
echo Executing in $tmpdir
|
||||
cd $tmpdir
|
||||
mkdir -p $(dirname $1)
|
||||
curl -f -sS $curlargs https://$mgr/confluent-public/os/$profile/scripts/$1 > $1
|
||||
curl -f -sS $curlargs https://$confluent_mgr/confluent-public/os/$confluent_profile/scripts/$1 > $1
|
||||
if [ $? != 0 ]; then echo "'$*'" failed to download; return 1; fi
|
||||
/usr/libexec/platform-python $*
|
||||
if [ -x /usr/libexec/platform-python ]; then
|
||||
/usr/libexec/platform-python $*
|
||||
else
|
||||
/usr/bin/python $*
|
||||
fi
|
||||
retcode=$?
|
||||
echo "'$*' exited with code $retcode"
|
||||
cd - > /dev/null
|
||||
return $retcode
|
||||
}
|
||||
|
||||
run_remote_config() {
|
||||
echo
|
||||
set_confluent_vars
|
||||
apiclient=/opt/confluent/bin/apiclient
|
||||
if [ -f /etc/confluent/apiclient ]; then
|
||||
apiclient=/etc/confluent/apiclient
|
||||
fi
|
||||
echo '---------------------------------------------------------------------------'
|
||||
echo Requesting to run remote configuration for "'$*'" from $confluent_mgr under profile $confluent_profile
|
||||
if [ -x /usr/libexec/platform-python ]; then
|
||||
/usr/libexec/platform-python $apiclient /confluent-api/self/remoteconfig/"$*" -d {}
|
||||
/usr/libexec/platform-python $apiclient /confluent-api/self/remoteconfig/status -w 204
|
||||
else
|
||||
/usr/bin/python $apiclient /confluent-api/self/remoteconfig/"$*" -d {}
|
||||
/usr/bin/python $apiclient /confluent-api/self/remoteconfig/status -w 204
|
||||
fi
|
||||
echo
|
||||
echo 'Completed remote configuration'
|
||||
echo '---------------------------------------------------------------------------'
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -9,12 +9,11 @@ if lspci -d 15b3:: -n |grep 15b3 > /dev/null; then
|
||||
# two lines to use the .iso instead of the tgz packaging
|
||||
#fetch_remote infiniband/mofed.iso
|
||||
#mkdir MLNX_OFED
|
||||
#mount -o loop ofed.iso MLNX_OFED
|
||||
#mount -o loop infiniband/mofed.iso MLNX_OFED
|
||||
fetch_remote infiniband/mofed.tgz
|
||||
tar xf infiniband/mofed.tgz
|
||||
# The rest is common between tar and iso
|
||||
cd MLNX_OFED*
|
||||
mount -o loop ofed
|
||||
./mlnxofedinstall --force
|
||||
fi
|
||||
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
#!/bin/sh
|
||||
# need to copy over ssh key info
|
||||
mkdir -p /var/log/confluent
|
||||
exec >> /var/log/confluent/confluent-post.log
|
||||
exec 2>> /var/log/confluent/confluent-post.log
|
||||
tail -f /var/log/confluent/confluent-post.log > /dev/tty &
|
||||
logshowpid=$!
|
||||
nodename=$(grep ^NODENAME /etc/confluent/confluent.info|awk '{print $2}')
|
||||
apikey=$(cat /etc/confluent/confluent.apikey)
|
||||
|
||||
chmod 700 /etc/confluent
|
||||
chmod og-rwx /etc/confluent/*
|
||||
export mgr profile nodename
|
||||
export confluent_mgr confluent_profile nodename
|
||||
. /etc/confluent/functions
|
||||
|
||||
curl -X POST -d 'status: staged' -H "CONFLUENT_NODENAME: $nodename" -H "CONFLUENT_APIKEY: $apikey" https://$mgr/confluent-api/self/updatestatus
|
||||
|
||||
|
||||
if [ -f /tmp/cryptboot ]; then
|
||||
run_remote tpm_luks.sh
|
||||
@@ -30,9 +32,18 @@ run_remote_python add_local_repositories
|
||||
# run_remote_python will use the appropriate python interpreter path to run the specified script
|
||||
# A post.custom is provided to more conveniently hold customizations, see the post.custom file.
|
||||
|
||||
# This will induce server side processing of the syncfile contents if
|
||||
# present
|
||||
run_remote_python syncfileclient
|
||||
|
||||
# run_remote example.sh
|
||||
# run_remote_python example.py
|
||||
run_remote post.custom
|
||||
|
||||
# Also, scripts may be placed into 'post.d', e.g. post.d/01-runfirst.sh, post.d/02-runsecond.sh
|
||||
run_remote_parts post
|
||||
run_remote_parts post.d
|
||||
|
||||
# Induce execution of remote configuration, e.g. ansible plays in ansible/post.d/
|
||||
run_remote_config post.d
|
||||
curl -sf -X POST -d 'status: staged' -H "CONFLUENT_NODENAME: $nodename" -H "CONFLUENT_APIKEY: $apikey" https://$confluent_mgr/confluent-api/self/updatestatus
|
||||
kill $logshowpid
|
||||
|
||||
@@ -7,7 +7,14 @@
|
||||
# If you want to use a more custom partition plan, the easiest
|
||||
# method is to edit the kicktstart file and comment out or
|
||||
# delete %include /tmp/partitioning
|
||||
|
||||
if [ -f "/run/install/cmdline.d/01-autocons.conf" ]; then
|
||||
consoledev=$(cat /run/install/cmdline.d/01-autocons.conf | sed -e 's!console=!/dev/!' -e 's/,.*//')
|
||||
TMUX= tmux a <> $consoledev >&0 2>&1 &
|
||||
fi
|
||||
exec >> /tmp/confluent-pre.log
|
||||
exec 2>> /tmp/confluent-pre.log
|
||||
tail -f /tmp/confluent-pre.log > /dev/tty &
|
||||
logshowpid=$!
|
||||
/usr/libexec/platform-python /etc/confluent/apiclient >& /dev/null
|
||||
nicname=$(ip link|grep ^$(cat /tmp/confluent.ifidx): | awk '{print $2}' | awk -F: '{print $1}')
|
||||
nmcli c u $nicname
|
||||
@@ -34,7 +41,7 @@ if [ "$rootpw" = null ]; then
|
||||
else
|
||||
echo "rootpw --iscrypted $rootpw" > /tmp/rootpw
|
||||
fi
|
||||
curl -f https://$mgr/confluent-public/os/$profile/profile.yaml > /tmp/instprofile.yaml
|
||||
curl -sf https://$confluent_mgr/confluent-public/os/$confluent_profile/profile.yaml > /tmp/instprofile.yaml
|
||||
blargs=$(grep ^installedargs: /tmp/instprofile.yaml | sed -e 's/#.*//' -e 's/^installedargs: //')
|
||||
if [ ! -z "$blargs" ]; then
|
||||
blargs=' --append="'$blargs'"'
|
||||
@@ -48,16 +55,13 @@ fi
|
||||
if [ ! -z "$blargs" ]; then
|
||||
echo "bootloader $blargs" > /tmp/grubpw
|
||||
fi
|
||||
ssh-keygen -A
|
||||
for pubkey in /etc/ssh/ssh_host*key.pub; do
|
||||
certfile=${pubkey/.pub/-cert.pub}
|
||||
curl -f -X POST -H "CONFLUENT_NODENAME: $nodename" -H "CONFLUENT_APIKEY: $(cat /etc/confluent/confluent.apikey)" -d @$pubkey https://$mgr/confluent-api/self/sshcert > $certfile
|
||||
curl -sf -X POST -H "CONFLUENT_NODENAME: $nodename" -H "CONFLUENT_APIKEY: $(cat /etc/confluent/confluent.apikey)" -d @$pubkey https://$confluent_mgr/confluent-api/self/sshcert > $certfile
|
||||
echo HostCertificate $certfile >> /etc/ssh/sshd_config.anaconda
|
||||
done
|
||||
/usr/sbin/sshd -f /etc/ssh/sshd_config.anaconda
|
||||
if [ -f "/run/install/cmdline.d/01-autocons.conf" ]; then
|
||||
consoledev=$(cat /run/install/cmdline.d/01-autocons.conf | sed -e 's!console=!/dev/!' -e 's/,.*//')
|
||||
tmux a <> $consoledev >&0 2>&1 &
|
||||
fi
|
||||
cryptboot=$(grep ^encryptboot: /etc/confluent/confluent.deploycfg | awk '{print $2}')
|
||||
LUKSPARTY=''
|
||||
touch /tmp/cryptpkglist
|
||||
@@ -65,18 +69,29 @@ touch /tmp/addonpackages
|
||||
if [ "$cryptboot" == "tpm2" ]; then
|
||||
LUKSPARTY="--encrypted --passphrase=$(cat /etc/confluent/confluent.apikey)"
|
||||
echo $cryptboot >> /tmp/cryptboot
|
||||
echo clevis-dracut >> /tmp/cryptpkglist
|
||||
echo clevis-dracut >> /tmp/cryptpkglist
|
||||
fi
|
||||
|
||||
|
||||
export mgr profile nodename
|
||||
curl -f https://$mgr/confluent-public/os/$profile/scripts/functions > /tmp/functions
|
||||
export confluent_mgr confluent_profile nodename
|
||||
curl -sf https://$confluent_mgr/confluent-public/os/$confluent_profile/scripts/functions > /tmp/functions
|
||||
. /tmp/functions
|
||||
run_remote_python getinstalldisk
|
||||
if [ -e /tmp/installdisk ]; then
|
||||
run_remote pre.custom
|
||||
run_remote_parts pre.d
|
||||
if [ ! -e /tmp/installdisk ]; then
|
||||
run_remote_python getinstalldisk
|
||||
fi
|
||||
if [ -e /tmp/installdisk -a ! -e /tmp/partitioning ]; then
|
||||
echo clearpart --all --initlabel >> /tmp/partitioning
|
||||
echo ignoredisk --only-use $(cat /tmp/installdisk) >> /tmp/partitioning
|
||||
echo autopart --nohome $LUKSPARTY >> /tmp/partitioning
|
||||
fi
|
||||
curl -f https://$mgr/confluent-public/os/$profile/kickstart.custom > /tmp/kickstart.custom
|
||||
run_remote pre.custom
|
||||
if [ -e /usr/libexec/platform-python ]; then
|
||||
python=/usr/libexec/platform-python
|
||||
elif [ -e /usr/bin/python3 ]; then
|
||||
python=/usr/byn/python3
|
||||
else
|
||||
python=/usr/bin/python
|
||||
fi
|
||||
$python /etc/confluent/apiclient /confluent-public/os/$confluent_profile/kickstart.custom -o /tmp/kickstart.custom
|
||||
kill $logshowpid
|
||||
|
||||
@@ -8,12 +8,22 @@
|
||||
# for customization, which will run in a more normal mechanism
|
||||
|
||||
nodename=$(grep ^NODENAME /etc/confluent/confluent.info|awk '{print $2}')
|
||||
export mgr profile nodename
|
||||
export confluent_mgr confluent_profile nodename
|
||||
cp -a /etc/confluent /mnt/sysimage/etc
|
||||
chmod -R og-rwx /mnt/sysimage/etc/confluent
|
||||
cp /tmp/functions /mnt/sysimage/etc/confluent/
|
||||
hostnamectl set-hostname $nodename
|
||||
cp /etc/hostname /mnt/sysimage/etc/hostname
|
||||
. /tmp/functions
|
||||
cp /tmp/cryptboot /mnt/sysimage/tmp/
|
||||
if [ -f /tmp/cryptboot ]; then
|
||||
cp /tmp/cryptboot /mnt/sysimage/tmp/
|
||||
fi
|
||||
mkdir -p /mnt/sysimage/var/log/confluent
|
||||
mv /tmp/confluent-pre.log /mnt/sysimage/var/log/confluent
|
||||
echo Port 2222 >> /etc/ssh/sshd_config.anaconda
|
||||
echo Match LocalPort 22 >> /etc/ssh/sshd_config.anaconda
|
||||
echo " ChrootDirectory /mnt/sysimage" >> /etc/ssh/sshd_config.anaconda
|
||||
kill -HUP $(cat /run/sshd.pid)
|
||||
|
||||
# Preserve the ssh setup work done for the installer
|
||||
# by copying into the target system and setting up
|
||||
|
||||
@@ -18,6 +18,6 @@ chmod 700 /mnt/sysimage/root/.ssh/
|
||||
cp /root/.ssh/authorized_keys /mnt/sysimage/root/.ssh/
|
||||
chmod 600 /mnt/sysimage/root/.ssh/authorized_keys
|
||||
cp /etc/ssh/ssh_known_hosts /mnt/sysimage/etc/ssh/
|
||||
curl -f -H "CONFLUENT_NODENAME: $nodename" -H "CONFLUENT_APIKEY: $(cat /etc/confluent/confluent.apikey)" https://$mgr/confluent-api/self/nodelist > /tmp/allnodes
|
||||
curl -f -H "CONFLUENT_NODENAME: $nodename" -H "CONFLUENT_APIKEY: $(cat /etc/confluent/confluent.apikey)" https://$confluent_mgr/confluent-api/self/nodelist > /tmp/allnodes
|
||||
cp /tmp/allnodes /mnt/sysimage/etc/ssh/shosts.equiv
|
||||
cp /tmp/allnodes /mnt/sysimage/root/.shosts
|
||||
|
||||
@@ -0,0 +1,219 @@
|
||||
#!/usr/bin/python
|
||||
import importlib
|
||||
import tempfile
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
from importlib.machinery import SourceFileLoader
|
||||
try:
|
||||
apiclient = SourceFileLoader('apiclient', '/opt/confluent/bin/apiclient').load_module()
|
||||
except FileNotFoundError:
|
||||
apiclient = SourceFileLoader('apiclient', '/etc/confluent/apiclient').load_module()
|
||||
|
||||
|
||||
def partitionhostsline(line):
|
||||
comment = ''
|
||||
try:
|
||||
cmdidx = line.index('#')
|
||||
comment = line[cmdidx:]
|
||||
line = line[:cmdidx].strip()
|
||||
except ValueError:
|
||||
pass
|
||||
if not line:
|
||||
return '', [], comment
|
||||
ipaddr, names = line.split(maxsplit=1)
|
||||
names = names.split()
|
||||
return ipaddr, names, comment
|
||||
|
||||
class HostMerger:
|
||||
def __init__(self):
|
||||
self.byip = {}
|
||||
self.byname = {}
|
||||
self.sourcelines = []
|
||||
self.targlines = []
|
||||
|
||||
def read_source(self, sourcefile):
|
||||
with open(sourcefile, 'r') as hfile:
|
||||
self.sourcelines = hfile.read().split('\n')
|
||||
while not self.sourcelines[-1]:
|
||||
self.sourcelines = self.sourcelines[:-1]
|
||||
for x in range(len(self.sourcelines)):
|
||||
line = self.sourcelines[x]
|
||||
currip, names, comment = partitionhostsline(line)
|
||||
if currip:
|
||||
self.byip[currip] = x
|
||||
for name in names:
|
||||
self.byname[name] = x
|
||||
|
||||
def read_target(self, targetfile):
|
||||
with open(targetfile, 'r') as hfile:
|
||||
lines = hfile.read().split('\n')
|
||||
if not lines[-1]:
|
||||
lines = lines[:-1]
|
||||
for y in range(len(lines)):
|
||||
line = lines[y]
|
||||
currip, names, comment = partitionhostsline(line)
|
||||
if currip in self.byip:
|
||||
x = self.byip[currip]
|
||||
if self.sourcelines[x] is None:
|
||||
# have already consumed this enntry
|
||||
continue
|
||||
self.targlines.append(self.sourcelines[x])
|
||||
self.sourcelines[x] = None
|
||||
continue
|
||||
for name in names:
|
||||
if name in self.byname:
|
||||
x = self.byname[name]
|
||||
if self.sourcelines[x] is None:
|
||||
break
|
||||
self.targlines.append(self.sourcelines[x])
|
||||
self.sourcelines[x] = None
|
||||
break
|
||||
else:
|
||||
self.targlines.append(line)
|
||||
|
||||
def write_out(self, targetfile):
|
||||
while not self.targlines[-1]:
|
||||
self.targlines = self.targlines[:-1]
|
||||
while not self.sourcelines[-1]:
|
||||
self.sourcelines = self.sourcelines[:-1]
|
||||
with open(targetfile, 'w') as hosts:
|
||||
for line in self.targlines:
|
||||
hosts.write(line + '\n')
|
||||
for line in self.sourcelines:
|
||||
if line is not None:
|
||||
hosts.write(line + '\n')
|
||||
|
||||
|
||||
class CredMerger:
|
||||
def __init__(self):
|
||||
try:
|
||||
with open('/etc/login.defs', 'r') as ldefs:
|
||||
defs = ldefs.read().split('\n')
|
||||
except FileNotFoundError:
|
||||
defs = []
|
||||
lkup = {}
|
||||
self.discardnames = {}
|
||||
self.shadowednames = {}
|
||||
for line in defs:
|
||||
try:
|
||||
line = line[:line.index('#')]
|
||||
except ValueError:
|
||||
pass
|
||||
keyval = line.split()
|
||||
if len(keyval) < 2:
|
||||
continue
|
||||
lkup[keyval[0]] = keyval[1]
|
||||
self.uidmin = int(lkup.get('UID_MIN', 1000))
|
||||
self.uidmax = int(lkup.get('UID_MAX', 60000))
|
||||
self.gidmin = int(lkup.get('GID_MIN', 1000))
|
||||
self.gidmax = int(lkup.get('GID_MAX', 60000))
|
||||
self.shadowlines = None
|
||||
|
||||
def read_passwd(self, source, targfile=False):
|
||||
self.read_generic(source, self.uidmin, self.uidmax, targfile)
|
||||
|
||||
def read_group(self, source, targfile=False):
|
||||
self.read_generic(source, self.gidmin, self.gidmax, targfile)
|
||||
|
||||
def read_generic(self, source, minid, maxid, targfile):
|
||||
if targfile:
|
||||
self.targdata = []
|
||||
else:
|
||||
self.sourcedata = []
|
||||
with open(source, 'r') as inputfile:
|
||||
for line in inputfile.read().split('\n'):
|
||||
try:
|
||||
name, _, uid, _ = line.split(':', 3)
|
||||
uid = int(uid)
|
||||
except ValueError:
|
||||
continue
|
||||
if targfile:
|
||||
if uid <= minid or uid >= maxid:
|
||||
self.targdata.append(line)
|
||||
else:
|
||||
self.discardnames[name] = 1
|
||||
else:
|
||||
if uid >= minid and uid <= maxid:
|
||||
self.sourcedata.append(line)
|
||||
|
||||
def read_shadow(self, source):
|
||||
self.shadowlines = []
|
||||
try:
|
||||
with open(source, 'r') as inshadow:
|
||||
for line in inshadow.read().split('\n'):
|
||||
try:
|
||||
name, _ = line.split(':' , 1)
|
||||
except ValueError:
|
||||
continue
|
||||
if name in self.discardnames:
|
||||
continue
|
||||
self.shadowednames[name] = 1
|
||||
self.shadowlines.append(line)
|
||||
except FileNotFoundError:
|
||||
return
|
||||
|
||||
def write_out(self, outfile):
|
||||
with open(outfile, 'w') as targ:
|
||||
for line in self.targdata:
|
||||
targ.write(line + '\n')
|
||||
for line in self.sourcedata:
|
||||
targ.write(line + '\n')
|
||||
if outfile == '/etc/passwd':
|
||||
if self.shadowlines is None:
|
||||
self.read_shadow('/etc/shadow')
|
||||
with open('/etc/shadow', 'w') as shadout:
|
||||
for line in self.shadowlines:
|
||||
shadout.write(line + '\n')
|
||||
for line in self.sourcedata:
|
||||
name, _ = line.split(':', 1)
|
||||
if name in self.shadowednames:
|
||||
continue
|
||||
shadout.write(name + ':!:::::::\n')
|
||||
if outfile == '/etc/group':
|
||||
if self.shadowlines is None:
|
||||
self.read_shadow('/etc/gshadow')
|
||||
with open('/etc/gshadow', 'w') as shadout:
|
||||
for line in self.shadowlines:
|
||||
shadout.write(line + '\n')
|
||||
for line in self.sourcedata:
|
||||
name, _ = line.split(':' , 1)
|
||||
if name in self.shadowednames:
|
||||
continue
|
||||
shadout.write(name + ':!::\n')
|
||||
|
||||
def synchronize():
|
||||
tmpdir = tempfile.mkdtemp()
|
||||
try:
|
||||
ac = apiclient.HTTPSClient()
|
||||
data = json.dumps({'merge': tmpdir})
|
||||
status, rsp = ac.grab_url_with_status('/confluent-api/self/remotesyncfiles', data)
|
||||
if status == 202:
|
||||
while status != 204:
|
||||
status, rsp = ac.grab_url_with_status('/confluent-api/self/remotesyncfiles')
|
||||
if not isinstance(rsp, str):
|
||||
rsp = rsp.decode('utf8')
|
||||
pendpasswd = os.path.join(tmpdir, 'etc/passwd')
|
||||
if os.path.exists(pendpasswd):
|
||||
cm = CredMerger()
|
||||
cm.read_passwd(pendpasswd, targfile=False)
|
||||
cm.read_passwd('/etc/passwd', targfile=True)
|
||||
cm.write_out('/etc/passwd')
|
||||
pendgroup = os.path.join(tmpdir, 'etc/group')
|
||||
if os.path.exists(pendgroup):
|
||||
cm = CredMerger()
|
||||
cm.read_group(pendgroup, targfile=False)
|
||||
cm.read_group('/etc/group', targfile=True)
|
||||
cm.write_out('/etc/group')
|
||||
pendhosts = os.path.join(tmpdir, 'etc/hosts')
|
||||
if os.path.exists(pendhosts):
|
||||
cm = HostMerger()
|
||||
cm.read_source(pendhosts)
|
||||
cm.read_target('/etc/hosts')
|
||||
cm.write_out('/etc/hosts')
|
||||
finally:
|
||||
shutil.rmtree(tmpdir)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
synchronize()
|
||||
@@ -0,0 +1,13 @@
|
||||
# This file lists files to synchronize or merge to the deployed systems from the deployment server
|
||||
# To specify taking /some/path/hosts on the deploymennt server and duplicating it to /etc/hosts:
|
||||
# /some/path/hosts -> /etc/hosts
|
||||
|
||||
# If wanting to simply use the same path for source and destinaiton, the -> may be skipped:
|
||||
# /etc/hosts
|
||||
|
||||
# Entries under MERGE will attempt to be intelligently merged. This supports /etc/group and /etc/passwd
|
||||
# Any supporting entries in /etc/shadow or /etc/gshadow are added automatically, with password disabled
|
||||
# It also will not inject 'system' ids (under 1,000 usually) as those tend to be local and rpm managed.
|
||||
MERGE:
|
||||
# /etc/passwd
|
||||
# /etc/group
|
||||
@@ -28,6 +28,19 @@ node=$(grep NODENAME: /etc/confluent/confluent.info|head -n 1|awk '{print $2}')
|
||||
mgr=$(grep MANAGER: /etc/confluent/confluent.info|head -n 1|awk '{print $2}')
|
||||
cp /opt/confluent/bin/clortho /clortho
|
||||
/clortho $node $mgr > /etc/confluent/confluent.apikey
|
||||
APIKEY=$(cat /etc/confluent/confluent.apikey)
|
||||
while [ -z "$APIKEY" ]; do
|
||||
if [ ! -f /var/run/vmware/show-esx-shell-login ]; then
|
||||
chvt 1
|
||||
sleep 1
|
||||
chvt 2
|
||||
/etc/init.d/ESXShell start
|
||||
fi
|
||||
echo "Deployment server failed to grant node deployment token, retrying token retrieval"
|
||||
echo "A debug session has been made available on Alt-F1"
|
||||
/clortho $node $mgr > /etc/confluent/confluent.apikey
|
||||
APIKEY=$(cat /etc/confluent/confluent.apikey)
|
||||
done
|
||||
cat /tls/*.pem > /etc/confluent/ca.pem
|
||||
/opt/confluent/bin/apiclient /confluent-api/self/deploycfg > /etc/confluent/confluent.deploycfg.new
|
||||
profile=$(grep ^profile: /etc/confluent/confluent.deploycfg.new | sed -e 's/^profile: //')
|
||||
|
||||
@@ -59,6 +59,7 @@ hostname $nodename
|
||||
ifidx=$(cat /tmp/confluent.ifidx)
|
||||
ifname=$(ip link |grep ^$ifidx:|awk '{print $2}')
|
||||
ifname=${ifname%:}
|
||||
autoconfigmethod=$(grep ipv4_method /etc/confluent/confluent.deploycfg |awk '{print $2}')
|
||||
if [ "$autoconfigmethod" = "dhcp" ]; then
|
||||
echo -n "Attempting to use dhcp to bring up $ifname..."
|
||||
dhclient $ifname
|
||||
@@ -80,7 +81,7 @@ else
|
||||
ip route add default via $v4gw
|
||||
fi
|
||||
fi
|
||||
echo -n "Initialiing ssh..."
|
||||
echo -n "Initializing ssh..."
|
||||
ssh-keygen -A
|
||||
for pubkey in /etc/ssh/ssh_host*key.pub; do
|
||||
certfile=${pubkey/.pub/-cert.pub}
|
||||
@@ -92,11 +93,11 @@ for pubkey in /etc/ssh/ssh_host*key.pub; do
|
||||
echo HostKey $privfile >> /etc/ssh/sshd_config
|
||||
done
|
||||
/usr/sbin/sshd
|
||||
profile=$(grep ^profile: /etc/confluent/confluent.deploycfg | awk '{print $2}')
|
||||
mgr=$(grep ^deploy_server: /etc/confluent/confluent.deploycfg | awk '{print $2}')
|
||||
export profile mgr
|
||||
echo "Running https://$mgr/confluent-public/os/$profile/scripts/onboot.sh"
|
||||
/usr/libexec/platform-python /opt/confluent/bin/apiclient /confluent-public/os/$profile/scripts/functions > /tmp/functions
|
||||
confluent_profile=$(grep ^profile: /etc/confluent/confluent.deploycfg | awk '{print $2}')
|
||||
confluent_mgr=$(grep ^deploy_server: /etc/confluent/confluent.deploycfg | awk '{print $2}')
|
||||
export confluent_profile confluent_mgr
|
||||
echo "Running https://$confluent_mgr/confluent-public/os/$confluent_profile/scripts/onboot.sh"
|
||||
/usr/libexec/platform-python /opt/confluent/bin/apiclient /confluent-public/os/$confluent_profile/scripts/functions > /tmp/functions
|
||||
. /tmp/functions
|
||||
run_remote onboot.sh
|
||||
while :; do
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
Ansible playbooks ending in .yml or .yaml that are placed into this directory will be executed at the
|
||||
appropriate phase of the install process.
|
||||
|
||||
The 'hosts' may be omitted, and if included will be ignored, replaced with the host that is specifically
|
||||
requesting the playbooks be executed.
|
||||
|
||||
Also, the playbooks will be executed on the deployment server. Hence it may be slower in aggregate than
|
||||
running content under scripts/ which ask much less of the deployment server
|
||||
|
||||
Here is an example of what a playbook would look like broadly:
|
||||
|
||||
- name: Example
|
||||
gather_facts: no
|
||||
tasks:
|
||||
- name: Example1
|
||||
lineinfile:
|
||||
path: /etc/hosts
|
||||
line: 1.2.3.4 test1
|
||||
create: yes
|
||||
- name: Example2
|
||||
lineinfile:
|
||||
path: /etc/hosts
|
||||
line: 1.2.3.5 test2
|
||||
create: yes
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
function set_confluent_vars() {
|
||||
if [ -z "$mgr" ]; then
|
||||
mgr=$(grep ^deploy_server: /etc/confluent/confluent.deploycfg | sed -e 's/[^ ]*: //')
|
||||
if [ -z "$confluent_mgr" ]; then
|
||||
confluent_mgr=$(grep ^deploy_server: /etc/confluent/confluent.deploycfg | sed -e 's/[^ ]*: //')
|
||||
fi
|
||||
if [ -z "$profile" ]; then
|
||||
profile=$(grep ^profile: /etc/confluent/confluent.deploycfg | sed -e 's/[^ ]*: //')
|
||||
if [ -z "$confluent_profile" ]; then
|
||||
confluent_profile=$(grep ^profile: /etc/confluent/confluent.deploycfg | sed -e 's/[^ ]*: //')
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ fetch_remote() {
|
||||
curlargs=" --cacert /etc/confluent/ca.pem"
|
||||
fi
|
||||
set_confluent_vars
|
||||
curl -f -sS $curlargs https://$mgr/confluent-public/os/$profile/scripts/$1 > $1
|
||||
curl -f -sS $curlargs https://$confluent_mgr/confluent-public/os/$confluent_profile/scripts/$1 > $1
|
||||
if [ $? != 0 ]; then echo $1 failed to download; return 1; fi
|
||||
}
|
||||
|
||||
@@ -25,11 +25,11 @@ run_remote() {
|
||||
set_confluent_vars
|
||||
echo
|
||||
echo '---------------------------------------------------------------------------'
|
||||
echo Running $requestedcmd from https://$mgr/confluent-public/os/$profile/scripts/
|
||||
echo Running $requestedcmd from https://$confluent_mgr/confluent-public/os/$confluent_profile/scripts/
|
||||
tmpdir=$(mktemp -d)
|
||||
echo Executing in $tmpdir
|
||||
cd $tmpdir
|
||||
curl -f -sS $curlargs https://$mgr/confluent-public/os/$profile/scripts/$1 > $1
|
||||
curl -f -sS $curlargs https://$confluent_mgr/confluent-public/os/$confluent_profile/scripts/$1 > $1
|
||||
if [ $? != 0 ]; then echo $requestedcmd failed to download; return 1; fi
|
||||
chmod +x $1
|
||||
cmd=$1
|
||||
@@ -51,11 +51,11 @@ run_remote_python() {
|
||||
curlargs=" --cacert /etc/confluent/ca.pem"
|
||||
fi
|
||||
echo '---------------------------------------------------------------------------'
|
||||
echo Running python script "'$*'" from https://$mgr/confluent-public/os/$profile/scripts/
|
||||
echo Running python script "'$*'" from https://$confluent_mgr/confluent-public/os/$confluent_profile/scripts/
|
||||
tmpdir=$(mktemp -d)
|
||||
echo Executing in $tmpdir
|
||||
cd $tmpdir
|
||||
curl -f -sS $curlargs https://$mgr/confluent-public/os/$profile/scripts/$1 > $1
|
||||
curl -f -sS $curlargs https://$confluent_mgr/confluent-public/os/$confluent_profile/scripts/$1 > $1
|
||||
if [ $? != 0 ]; then echo "'$*'" failed to download; return 1; fi
|
||||
/usr/libexec/platform-python $*
|
||||
retcode=$?
|
||||
@@ -63,3 +63,20 @@ run_remote_python() {
|
||||
cd - > /dev/null
|
||||
return $retcode
|
||||
}
|
||||
|
||||
run_remote_config() {
|
||||
echo
|
||||
set_confluent_vars
|
||||
apiclient=/opt/confluent/bin/apiclient
|
||||
if [ -f /etc/confluent/apiclient ]; then
|
||||
apiclient=/etc/confluent/apiclient
|
||||
fi
|
||||
echo '---------------------------------------------------------------------------'
|
||||
echo Requesting to run remote configuration for "'$*'" from $confluent_mgr under profile $confluent_profile
|
||||
/usr/libexec/platform-python $apiclient /confluent-api/self/remoteconfig/"$*" -d {}
|
||||
/usr/libexec/platform-python $apiclient /confluent-api/self/remoteconfig/status -w 204
|
||||
echo
|
||||
echo 'Completed remote configuration'
|
||||
echo '---------------------------------------------------------------------------'
|
||||
return
|
||||
}
|
||||
|
||||
@@ -6,6 +6,13 @@
|
||||
# run_remote and run_remote_python are available to download scripts and
|
||||
# execute them.
|
||||
|
||||
# This will induce server side processing of the syncfile contents if
|
||||
# present
|
||||
run_remote_python syncfileclient
|
||||
|
||||
# Induce execution of remote configuration, e.g. ansible plays in ansible/onboot.d/
|
||||
run_remote_config onboot
|
||||
|
||||
# This is an example to request the BMC be configured on the network
|
||||
# according to how confluent has things configured:
|
||||
# run_remote_python configbmc -c
|
||||
|
||||
@@ -0,0 +1,219 @@
|
||||
#!/usr/bin/python
|
||||
import importlib
|
||||
import tempfile
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
from importlib.machinery import SourceFileLoader
|
||||
try:
|
||||
apiclient = SourceFileLoader('apiclient', '/opt/confluent/bin/apiclient').load_module()
|
||||
except FileNotFoundError:
|
||||
apiclient = SourceFileLoader('apiclient', '/etc/confluent/apiclient').load_module()
|
||||
|
||||
|
||||
def partitionhostsline(line):
|
||||
comment = ''
|
||||
try:
|
||||
cmdidx = line.index('#')
|
||||
comment = line[cmdidx:]
|
||||
line = line[:cmdidx].strip()
|
||||
except ValueError:
|
||||
pass
|
||||
if not line:
|
||||
return '', [], comment
|
||||
ipaddr, names = line.split(maxsplit=1)
|
||||
names = names.split()
|
||||
return ipaddr, names, comment
|
||||
|
||||
class HostMerger:
|
||||
def __init__(self):
|
||||
self.byip = {}
|
||||
self.byname = {}
|
||||
self.sourcelines = []
|
||||
self.targlines = []
|
||||
|
||||
def read_source(self, sourcefile):
|
||||
with open(sourcefile, 'r') as hfile:
|
||||
self.sourcelines = hfile.read().split('\n')
|
||||
while not self.sourcelines[-1]:
|
||||
self.sourcelines = self.sourcelines[:-1]
|
||||
for x in range(len(self.sourcelines)):
|
||||
line = self.sourcelines[x]
|
||||
currip, names, comment = partitionhostsline(line)
|
||||
if currip:
|
||||
self.byip[currip] = x
|
||||
for name in names:
|
||||
self.byname[name] = x
|
||||
|
||||
def read_target(self, targetfile):
|
||||
with open(targetfile, 'r') as hfile:
|
||||
lines = hfile.read().split('\n')
|
||||
if not lines[-1]:
|
||||
lines = lines[:-1]
|
||||
for y in range(len(lines)):
|
||||
line = lines[y]
|
||||
currip, names, comment = partitionhostsline(line)
|
||||
if currip in self.byip:
|
||||
x = self.byip[currip]
|
||||
if self.sourcelines[x] is None:
|
||||
# have already consumed this enntry
|
||||
continue
|
||||
self.targlines.append(self.sourcelines[x])
|
||||
self.sourcelines[x] = None
|
||||
continue
|
||||
for name in names:
|
||||
if name in self.byname:
|
||||
x = self.byname[name]
|
||||
if self.sourcelines[x] is None:
|
||||
break
|
||||
self.targlines.append(self.sourcelines[x])
|
||||
self.sourcelines[x] = None
|
||||
break
|
||||
else:
|
||||
self.targlines.append(line)
|
||||
|
||||
def write_out(self, targetfile):
|
||||
while not self.targlines[-1]:
|
||||
self.targlines = self.targlines[:-1]
|
||||
while not self.sourcelines[-1]:
|
||||
self.sourcelines = self.sourcelines[:-1]
|
||||
with open(targetfile, 'w') as hosts:
|
||||
for line in self.targlines:
|
||||
hosts.write(line + '\n')
|
||||
for line in self.sourcelines:
|
||||
if line is not None:
|
||||
hosts.write(line + '\n')
|
||||
|
||||
|
||||
class CredMerger:
|
||||
def __init__(self):
|
||||
try:
|
||||
with open('/etc/login.defs', 'r') as ldefs:
|
||||
defs = ldefs.read().split('\n')
|
||||
except FileNotFoundError:
|
||||
defs = []
|
||||
lkup = {}
|
||||
self.discardnames = {}
|
||||
self.shadowednames = {}
|
||||
for line in defs:
|
||||
try:
|
||||
line = line[:line.index('#')]
|
||||
except ValueError:
|
||||
pass
|
||||
keyval = line.split()
|
||||
if len(keyval) < 2:
|
||||
continue
|
||||
lkup[keyval[0]] = keyval[1]
|
||||
self.uidmin = int(lkup.get('UID_MIN', 1000))
|
||||
self.uidmax = int(lkup.get('UID_MAX', 60000))
|
||||
self.gidmin = int(lkup.get('GID_MIN', 1000))
|
||||
self.gidmax = int(lkup.get('GID_MAX', 60000))
|
||||
self.shadowlines = None
|
||||
|
||||
def read_passwd(self, source, targfile=False):
|
||||
self.read_generic(source, self.uidmin, self.uidmax, targfile)
|
||||
|
||||
def read_group(self, source, targfile=False):
|
||||
self.read_generic(source, self.gidmin, self.gidmax, targfile)
|
||||
|
||||
def read_generic(self, source, minid, maxid, targfile):
|
||||
if targfile:
|
||||
self.targdata = []
|
||||
else:
|
||||
self.sourcedata = []
|
||||
with open(source, 'r') as inputfile:
|
||||
for line in inputfile.read().split('\n'):
|
||||
try:
|
||||
name, _, uid, _ = line.split(':', 3)
|
||||
uid = int(uid)
|
||||
except ValueError:
|
||||
continue
|
||||
if targfile:
|
||||
if uid <= minid or uid >= maxid:
|
||||
self.targdata.append(line)
|
||||
else:
|
||||
self.discardnames[name] = 1
|
||||
else:
|
||||
if uid >= minid and uid <= maxid:
|
||||
self.sourcedata.append(line)
|
||||
|
||||
def read_shadow(self, source):
|
||||
self.shadowlines = []
|
||||
try:
|
||||
with open(source, 'r') as inshadow:
|
||||
for line in inshadow.read().split('\n'):
|
||||
try:
|
||||
name, _ = line.split(':' , 1)
|
||||
except ValueError:
|
||||
continue
|
||||
if name in self.discardnames:
|
||||
continue
|
||||
self.shadowednames[name] = 1
|
||||
self.shadowlines.append(line)
|
||||
except FileNotFoundError:
|
||||
return
|
||||
|
||||
def write_out(self, outfile):
|
||||
with open(outfile, 'w') as targ:
|
||||
for line in self.targdata:
|
||||
targ.write(line + '\n')
|
||||
for line in self.sourcedata:
|
||||
targ.write(line + '\n')
|
||||
if outfile == '/etc/passwd':
|
||||
if self.shadowlines is None:
|
||||
self.read_shadow('/etc/shadow')
|
||||
with open('/etc/shadow', 'w') as shadout:
|
||||
for line in self.shadowlines:
|
||||
shadout.write(line + '\n')
|
||||
for line in self.sourcedata:
|
||||
name, _ = line.split(':', 1)
|
||||
if name in self.shadowednames:
|
||||
continue
|
||||
shadout.write(name + ':!:::::::\n')
|
||||
if outfile == '/etc/group':
|
||||
if self.shadowlines is None:
|
||||
self.read_shadow('/etc/gshadow')
|
||||
with open('/etc/gshadow', 'w') as shadout:
|
||||
for line in self.shadowlines:
|
||||
shadout.write(line + '\n')
|
||||
for line in self.sourcedata:
|
||||
name, _ = line.split(':' , 1)
|
||||
if name in self.shadowednames:
|
||||
continue
|
||||
shadout.write(name + ':!::\n')
|
||||
|
||||
def synchronize():
|
||||
tmpdir = tempfile.mkdtemp()
|
||||
try:
|
||||
ac = apiclient.HTTPSClient()
|
||||
data = json.dumps({'merge': tmpdir})
|
||||
status, rsp = ac.grab_url_with_status('/confluent-api/self/remotesyncfiles', data)
|
||||
if status == 202:
|
||||
while status != 204:
|
||||
status, rsp = ac.grab_url_with_status('/confluent-api/self/remotesyncfiles')
|
||||
if not isinstance(rsp, str):
|
||||
rsp = rsp.decode('utf8')
|
||||
pendpasswd = os.path.join(tmpdir, 'etc/passwd')
|
||||
if os.path.exists(pendpasswd):
|
||||
cm = CredMerger()
|
||||
cm.read_passwd(pendpasswd, targfile=False)
|
||||
cm.read_passwd('/etc/passwd', targfile=True)
|
||||
cm.write_out('/etc/passwd')
|
||||
pendgroup = os.path.join(tmpdir, 'etc/group')
|
||||
if os.path.exists(pendgroup):
|
||||
cm = CredMerger()
|
||||
cm.read_group(pendgroup, targfile=False)
|
||||
cm.read_group('/etc/group', targfile=True)
|
||||
cm.write_out('/etc/group')
|
||||
pendhosts = os.path.join(tmpdir, 'etc/hosts')
|
||||
if os.path.exists(pendhosts):
|
||||
cm = HostMerger()
|
||||
cm.read_source(pendhosts)
|
||||
cm.read_target('/etc/hosts')
|
||||
cm.write_out('/etc/hosts')
|
||||
finally:
|
||||
shutil.rmtree(tmpdir)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
synchronize()
|
||||
@@ -0,0 +1,25 @@
|
||||
Ansible playbooks ending in .yml or .yaml that are placed into this directory will be executed at the
|
||||
appropriate phase of the install process.
|
||||
|
||||
The 'hosts' may be omitted, and if included will be ignored, replaced with the host that is specifically
|
||||
requesting the playbooks be executed.
|
||||
|
||||
Also, the playbooks will be executed on the deployment server. Hence it may be slower in aggregate than
|
||||
running content under scripts/ which ask much less of the deployment server
|
||||
|
||||
Here is an example of what a playbook would look like broadly:
|
||||
|
||||
- name: Example
|
||||
gather_facts: no
|
||||
tasks:
|
||||
- name: Example1
|
||||
lineinfile:
|
||||
path: /etc/hosts
|
||||
line: 1.2.3.4 test1
|
||||
create: yes
|
||||
- name: Example2
|
||||
lineinfile:
|
||||
path: /etc/hosts
|
||||
line: 1.2.3.5 test2
|
||||
create: yes
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
Ansible playbooks ending in .yml or .yaml that are placed into this directory will be executed at the
|
||||
appropriate phase of the install process.
|
||||
|
||||
The 'hosts' may be omitted, and if included will be ignored, replaced with the host that is specifically
|
||||
requesting the playbooks be executed.
|
||||
|
||||
Also, the playbooks will be executed on the deployment server. Hence it may be slower in aggregate than
|
||||
running content under scripts/ which ask much less of the deployment server
|
||||
|
||||
Here is an example of what a playbook would look like broadly:
|
||||
|
||||
- name: Example
|
||||
gather_facts: no
|
||||
tasks:
|
||||
- name: Example1
|
||||
lineinfile:
|
||||
path: /etc/hosts
|
||||
line: 1.2.3.4 test1
|
||||
create: yes
|
||||
- name: Example2
|
||||
lineinfile:
|
||||
path: /etc/hosts
|
||||
line: 1.2.3.5 test2
|
||||
create: yes
|
||||
|
||||
@@ -39,6 +39,7 @@ dynamic behavior and replace with static configuration.
|
||||
<pattern>base</pattern>
|
||||
</patterns>
|
||||
<packages config:type="list">
|
||||
<package>python3</package>
|
||||
<package>openssl</package>
|
||||
<package>chrony</package>
|
||||
<package>rsync</package>
|
||||
@@ -102,10 +103,10 @@ dynamic behavior and replace with static configuration.
|
||||
<source>
|
||||
<![CDATA[
|
||||
#!/bin/sh
|
||||
mgr=$(grep ^deploy_server /etc/confluent/confluent.deploycfg|awk '{print $2}')
|
||||
profile=$(grep ^profile: /etc/confluent/confluent.deploycfg|sed -e 's/^profile: //')
|
||||
confluent_mgr=$(grep ^deploy_server /etc/confluent/confluent.deploycfg|awk '{print $2}')
|
||||
confluent_profile=$(grep ^profile: /etc/confluent/confluent.deploycfg|sed -e 's/^profile: //')
|
||||
proto=$(grep ^protocol: /etc/confluent/confluent.deploycfg |awk '{print $2}')
|
||||
curl $proto://$mgr/confluent-public/os/$profile/scripts/pre.sh > /tmp/pre.sh
|
||||
curl $proto://$confluent_mgr/confluent-public/os/$confluent_profile/scripts/pre.sh > /tmp/pre.sh
|
||||
. /tmp/pre.sh
|
||||
]]>
|
||||
</source>
|
||||
@@ -118,13 +119,13 @@ curl $proto://$mgr/confluent-public/os/$profile/scripts/pre.sh > /tmp/pre.sh
|
||||
<source>
|
||||
<![CDATA[
|
||||
#!/bin/sh
|
||||
mgr=$(grep ^deploy_server /etc/confluent/confluent.deploycfg|awk '{print $2}')
|
||||
profile=$(grep ^profile: /etc/confluent/confluent.deploycfg|sed -e 's/^profile: //')
|
||||
confluent_mgr=$(grep ^deploy_server /etc/confluent/confluent.deploycfg|awk '{print $2}')
|
||||
confluent_profile=$(grep ^profile: /etc/confluent/confluent.deploycfg|sed -e 's/^profile: //')
|
||||
proto=$(grep ^protocol: /etc/confluent/confluent.deploycfg |awk '{print $2}')
|
||||
curl $proto://$mgr/confluent-public/os/$profile/scripts/prechroot.sh > /tmp/prechroot.sh
|
||||
curl $proto://$confluent_mgr/confluent-public/os/$confluent_profile/scripts/prechroot.sh > /tmp/prechroot.sh
|
||||
. /tmp/prechroot.sh
|
||||
curl -f $proto://$mgr/confluent-public/os/$profile/scripts/firstboot.sh > /mnt/etc/confluent/firstboot.sh
|
||||
curl -f $proto://$mgr/confluent-public/os/$profile/scripts/post.sh > /mnt/etc/confluent/post.sh
|
||||
curl -f $proto://$confluent_mgr/confluent-public/os/$confluent_profile/scripts/firstboot.sh > /mnt/etc/confluent/firstboot.sh
|
||||
curl -f $proto://$confluent_mgr/confluent-public/os/$confluent_profile/scripts/post.sh > /mnt/etc/confluent/post.sh
|
||||
chmod +x /mnt/etc/confluent/firstboot.sh
|
||||
chmod +x /mnt/etc/confluent/post.sh
|
||||
cp /mnt/etc/confluent/post.sh /mnt/var/adm/autoinstall/scripts/
|
||||
|
||||
@@ -1,13 +1,27 @@
|
||||
#!/bin/sh
|
||||
#!/bin/bash
|
||||
|
||||
# This script runs at the end of the final boot, updating status
|
||||
exec >> /var/log/confluent/confluent-firstboot.log
|
||||
exec 2>> /var/log/confluent/confluent-firstboot.log
|
||||
|
||||
nodename=$(grep ^NODENAME /etc/confluent/confluent.info|awk '{print $2}')
|
||||
mgr=$(grep ^deploy_server /etc/confluent/confluent.deploycfg|awk '{print $2}')
|
||||
profile=$(grep ^profile: /etc/confluent/confluent.deploycfg|sed -e 's/^rootpassword: //')
|
||||
confluent_mgr=$(grep ^deploy_server /etc/confluent/confluent.deploycfg|awk '{print $2}')
|
||||
confluent_profile=$(grep ^profile: /etc/confluent/confluent.deploycfg|sed -e 's/^rootpassword: //')
|
||||
proto=$(grep ^protocol: /etc/confluent/confluent.deploycfg |awk '{print $2}')
|
||||
apikey=$(cat /etc/confluent/confluent.apikey)
|
||||
confluent_apikey=$(cat /etc/confluent/confluent.apikey)
|
||||
. /etc/confluent/functions
|
||||
|
||||
for i in /etc/ssh/ssh_host*key.pub; do
|
||||
certname=${i/.pub/-cert.pub}
|
||||
curl -f -X POST -H "CONFLUENT_NODENAME: $nodename" -H "CONFLUENT_APIKEY: $(cat /etc/confluent/confluent.apikey)" -d @$i https://$confluent_mgr/confluent-api/self/sshcert > $certname
|
||||
done
|
||||
systemctl restart sshd
|
||||
run_remote firstboot.custom
|
||||
curl --capath /etc/confluent/tls -H "CONFLUENT_NODENAME: $nodename" -H "CONFLUENT_APIKEY: $apikey" -f -X POST -d "status: complete" https://$mgr/confluent-api/self/updatestatus
|
||||
|
||||
# Firstboot scripts may be placed into firstboot.d, e.g. firstboot.d/01-firstaction.sh, firstboot.d/02-secondaction.sh
|
||||
run_remote_parts firstboot.d
|
||||
|
||||
# Induce execution of remote configuration, e.g. ansible plays in ansible/firstboot.d/
|
||||
run_remote_config firstboot.d
|
||||
|
||||
curl --capath /etc/confluent/tls -H "CONFLUENT_NODENAME: $nodename" -H "CONFLUENT_APIKEY: $confluent_apikey" -f -X POST -d "status: complete" https://$confluent_mgr/confluent-api/self/updatestatus
|
||||
|
||||
@@ -1,12 +1,23 @@
|
||||
function set_confluent_vars() {
|
||||
if [ -z "$confluent_mgr" ]; then
|
||||
confluent_mgr=$(grep ^deploy_server: /etc/confluent/confluent.deploycfg | sed -e 's/[^ ]*: //')
|
||||
fi
|
||||
if [ -z "$confluent_profile" ]; then
|
||||
confluent_profile=$(grep ^profile: /etc/confluent/confluent.deploycfg | sed -e 's/[^ ]*: //')
|
||||
fi
|
||||
}
|
||||
|
||||
run_remote() {
|
||||
set_confluent_vars
|
||||
requestedcmd="'$*'"
|
||||
echo
|
||||
echo '---------------------------------------------------------------------------'
|
||||
echo Running $requestedcmd from https://$mgr/confluent-public/os/$profile/scripts/
|
||||
echo Running $requestedcmd from https://$confluent_mgr/confluent-public/os/$confluent_profile/scripts/
|
||||
tmpdir=$(mktemp -d)
|
||||
echo Executing in $tmpdir
|
||||
cd $tmpdir
|
||||
curl -f -sS https://$mgr/confluent-public/os/$profile/scripts/$1 > $1
|
||||
mkdir -p $(dirname $1)
|
||||
curl -f -sS https://$confluent_mgr/confluent-public/os/$confluent_profile/scripts/$1 > $1
|
||||
if [ $? != 0 ]; then echo $requestedcmd failed to download; return 1; fi
|
||||
chmod +x $1
|
||||
cmd=$1
|
||||
@@ -18,14 +29,41 @@ run_remote() {
|
||||
return $retcode
|
||||
}
|
||||
|
||||
source_remote_parts() {
|
||||
confluentscripttmpdir=$(mktemp -d)
|
||||
apiclient=/opt/confluent/bin/apiclient
|
||||
if [ -f /etc/confluent/apiclient ]; then
|
||||
apiclient=/etc/confluent/apiclient
|
||||
fi
|
||||
scriptlist=$(/usr/bin/python3 $apiclient /confluent-api/self/scriptlist/$1|sed -e 's/^- //')
|
||||
for script in $scriptlist; do
|
||||
source_remote $1/$script
|
||||
done
|
||||
unset confluentscripttmpdir
|
||||
}
|
||||
|
||||
run_remote_parts() {
|
||||
confluentscripttmpdir=$(mktemp -d)
|
||||
apiclient=/opt/confluent/bin/apiclient
|
||||
if [ -f /etc/confluent/apiclient ]; then
|
||||
apiclient=/etc/confluent/apiclient
|
||||
fi
|
||||
scriptlist=$(/usr/bin/python3 $apiclient /confluent-api/self/scriptlist/$1|sed -e 's/^- //')
|
||||
for script in $scriptlist; do
|
||||
run_remote $1/$script
|
||||
done
|
||||
unset confluentscripttmpdir
|
||||
}
|
||||
|
||||
run_remote_python() {
|
||||
echo
|
||||
echo '---------------------------------------------------------------------------'
|
||||
echo Running python script "'$*'" from https://$mgr/confluent-public/os/$profile/scripts/
|
||||
echo Running python script "'$*'" from https://$confluent_mgr/confluent-public/os/$confluent_profile/scripts/
|
||||
tmpdir=$(mktemp -d)
|
||||
echo Executing in $tmpdir
|
||||
cd $tmpdir
|
||||
curl -f -sS https://$mgr/confluent-public/os/$profile/scripts/$1 > $1
|
||||
mkdir -p $(dirname $1)
|
||||
curl -f -sS https://$confluent_mgr/confluent-public/os/$confluent_profile/scripts/$1 > $1
|
||||
if [ $? != 0 ]; then echo "'$*'" failed to download; return 1; fi
|
||||
python3 $*
|
||||
retcode=$?
|
||||
@@ -33,3 +71,20 @@ run_remote_python() {
|
||||
cd - > /dev/null
|
||||
return $retcode
|
||||
}
|
||||
|
||||
run_remote_config() {
|
||||
echo
|
||||
set_confluent_vars
|
||||
apiclient=/opt/confluent/bin/apiclient
|
||||
if [ -f /etc/confluent/apiclient ]; then
|
||||
apiclient=/etc/confluent/apiclient
|
||||
fi
|
||||
echo '---------------------------------------------------------------------------'
|
||||
echo Requesting to run remote configuration for "'$*'" from $confluent_mgr under profile $confluent_profile
|
||||
python3 $apiclient /confluent-api/self/remoteconfig/"$*" -d {}
|
||||
python3 $apiclient /confluent-api/self/remoteconfig/status -w 204
|
||||
echo
|
||||
echo 'Completed remote configuration'
|
||||
echo '---------------------------------------------------------------------------'
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/bin/sh
|
||||
#!/bin/bash
|
||||
|
||||
# This script runs at the end of install in the installed system
|
||||
# but still under the installer kernel.
|
||||
@@ -9,19 +9,31 @@
|
||||
# If there are dependencies on the kernel (drivers or special filesystems)
|
||||
# then firstboot.sh would be the script to customize.
|
||||
|
||||
mgr=$(grep ^deploy_server /etc/confluent/confluent.deploycfg|awk '{print $2}')
|
||||
profile=$(grep ^profile: /etc/confluent/confluent.deploycfg|sed -e 's/^profile: //')
|
||||
exec >> /var/log/confluent/confluent-post.log
|
||||
exec 2>> /var/log/confluent/confluent-post.log
|
||||
confluent_mgr=$(grep ^deploy_server /etc/confluent/confluent.deploycfg|awk '{print $2}')
|
||||
confluent_profile=$(grep ^profile: /etc/confluent/confluent.deploycfg|sed -e 's/^profile: //')
|
||||
nodename=$(grep ^NODENAME /etc/confluent/confluent.info|awk '{print $2}')
|
||||
apikey=$(cat /etc/confluent/confluent.apikey)
|
||||
confluent_apikey=$(cat /etc/confluent/confluent.apikey)
|
||||
|
||||
chmod 700 /etc/confluent
|
||||
chmod og-rwx /etc/confluent/*
|
||||
|
||||
|
||||
export mgr profile nodename
|
||||
export confluent_mgr confluent_profile nodename
|
||||
. /etc/confluent/functions
|
||||
|
||||
# This will induce server side processing of the syncfile contents if
|
||||
# present
|
||||
run_remote_python syncfileclient
|
||||
|
||||
run_remote post.custom
|
||||
|
||||
curl -X POST -d 'status: staged' -H "CONFLUENT_NODENAME: $nodename" -H "CONFLUENT_APIKEY: $apikey" https://$mgr/confluent-api/self/updatestatus
|
||||
# Also, scripts may be placed into 'post.d', e.g. post.d/01-runfirst.sh, post.d/02-runsecond.sh
|
||||
run_remote_parts post.d
|
||||
|
||||
# Induce execution of remote configuration, e.g. ansible plays in ansible/post.d/
|
||||
run_remote_config post.d
|
||||
|
||||
curl -X POST -d 'status: staged' -H "CONFLUENT_NODENAME: $nodename" -H "CONFLUENT_APIKEY: $confluent_apikey" https://$confluent_mgr/confluent-api/self/updatestatus
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
#!/bin/sh
|
||||
#!/bin/bash
|
||||
|
||||
# This script runs before the installer executes, and sets up ssh during install as well
|
||||
# as rewriting the autoyast file with any substitutions prior to it being evaluated for real
|
||||
|
||||
exec >> /tmp/confluent-pre.log
|
||||
exec 2>> /tmp/confluent-pre.log
|
||||
nodename=$(grep ^NODENAME /etc/confluent/confluent.info|awk '{print $2}')
|
||||
rootpw=$(grep rootpassword: /etc/confluent/confluent.deploycfg|sed -e 's/^rootpassword: //')
|
||||
if [ "$rootpw" = "null" ]; then
|
||||
@@ -21,12 +23,12 @@ cat /ssh/*pubkey > ~/.ssh/authorized_keys 2>/dev/null
|
||||
ssh-keygen -A
|
||||
for i in /etc/ssh/ssh_host*key.pub; do
|
||||
certname=${i/.pub/-cert.pub}
|
||||
curl -f -X POST -H "CONFLUENT_NODENAME: $nodename" -H "CONFLUENT_APIKEY: $(cat /etc/confluent/confluent.apikey)" -d @$i https://$mgr/confluent-api/self/sshcert > $certname
|
||||
curl -f -X POST -H "CONFLUENT_NODENAME: $nodename" -H "CONFLUENT_APIKEY: $(cat /etc/confluent/confluent.apikey)" -d @$i https://$confluent_mgr/confluent-api/self/sshcert > $certname
|
||||
echo HostKey ${i%.pub} >> /etc/ssh/sshd_config
|
||||
echo HostCertificate $certname >> /etc/ssh/sshd_config
|
||||
done
|
||||
/usr/sbin/sshd
|
||||
curl -f https://$mgr/confluent-public/os/$profile/scripts/functions > /tmp/functions
|
||||
curl -f https://$confluent_mgr/confluent-public/os/$confluent_profile/scripts/functions > /tmp/functions
|
||||
. /tmp/functions
|
||||
ntpcfg=""
|
||||
if grep ^ntpservers: /etc/confluent/confluent.deploycfg > /dev/null; then
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
# is still running, with the to-be-booted system mounted in /mnt
|
||||
|
||||
# carry over deployment configuration and api key for OS install action
|
||||
mgr=$(grep ^deploy_server /etc/confluent/confluent.deploycfg|awk '{print $2}')
|
||||
profile=$(grep ^profile: /etc/confluent/confluent.deploycfg|sed -e 's/^profile: //')
|
||||
confluent_mgr=$(grep ^deploy_server /etc/confluent/confluent.deploycfg|awk '{print $2}')
|
||||
confluent_profile=$(grep ^profile: /etc/confluent/confluent.deploycfg|sed -e 's/^profile: //')
|
||||
nodename=$(grep ^NODENAME /etc/confluent/confluent.info|awk '{print $2}')
|
||||
export mgr profile nodename
|
||||
export confluent_mgr confluent_profile nodename
|
||||
mkdir -p /mnt/etc/confluent
|
||||
chmod 700 /mnt/etc/confluent
|
||||
cp /tmp/functions /mnt/etc/confluent/
|
||||
@@ -17,5 +17,16 @@ cp -a /tls /mnt/etc/confluent/
|
||||
cp -a /tls/* /mnt/var/lib/ca-certificates/openssl
|
||||
cp -a /tls/* /mnt/var/lib/ca-certificates/pem
|
||||
cp -a /tls/*.pem /mnt/etc/pki/trust/anchors
|
||||
cat /tls/*.pem > /mnt/etc/confluent/ca.pem
|
||||
cp /opt/confluent/bin/apiclient /mnt/etc/confluent
|
||||
|
||||
run_remote setupssh.sh
|
||||
|
||||
echo Port 22 >> /etc/ssh/sshd_config
|
||||
echo Port 2222 >> /etc/ssh/sshd_config
|
||||
echo Match LocalPort 22 >> /etc/ssh/sshd_config
|
||||
echo " ChrootDirectory /mnt" >> /etc/ssh/sshd_config
|
||||
kill -HUP $(cat /run/sshd.pid)
|
||||
mkdir -p /mnt/var/log/confluent
|
||||
cp /tmp/confluent*log /mnt/var/log/confluent
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ echo /usr/lib/ssh/ssh-keysign root:root 4711 >> /mnt/etc/permissions.local
|
||||
chmod 4711 /mnt/usr/lib/ssh/ssh-keysign
|
||||
|
||||
# Download list of nodes from confluent, and put it into shosts.equiv (for most users) and .shosts (for root)
|
||||
curl -f -H "CONFLUENT_NODENAME: $nodename" -H "CONFLUENT_APIKEY: $(cat /etc/confluent/confluent.apikey)" https://$mgr/confluent-api/self/nodelist > /tmp/allnodes
|
||||
curl -f -H "CONFLUENT_NODENAME: $nodename" -H "CONFLUENT_APIKEY: $(cat /etc/confluent/confluent.apikey)" https://$confluent_mgr/confluent-api/self/nodelist > /tmp/allnodes
|
||||
cp /tmp/allnodes /mnt/root/.shosts
|
||||
cp /tmp/allnodes /mnt/etc/ssh/shosts.equiv
|
||||
|
||||
|
||||
@@ -0,0 +1,219 @@
|
||||
#!/usr/bin/python
|
||||
import importlib
|
||||
import tempfile
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
from importlib.machinery import SourceFileLoader
|
||||
try:
|
||||
apiclient = SourceFileLoader('apiclient', '/opt/confluent/bin/apiclient').load_module()
|
||||
except FileNotFoundError:
|
||||
apiclient = SourceFileLoader('apiclient', '/etc/confluent/apiclient').load_module()
|
||||
|
||||
|
||||
def partitionhostsline(line):
|
||||
comment = ''
|
||||
try:
|
||||
cmdidx = line.index('#')
|
||||
comment = line[cmdidx:]
|
||||
line = line[:cmdidx].strip()
|
||||
except ValueError:
|
||||
pass
|
||||
if not line:
|
||||
return '', [], comment
|
||||
ipaddr, names = line.split(maxsplit=1)
|
||||
names = names.split()
|
||||
return ipaddr, names, comment
|
||||
|
||||
class HostMerger:
|
||||
def __init__(self):
|
||||
self.byip = {}
|
||||
self.byname = {}
|
||||
self.sourcelines = []
|
||||
self.targlines = []
|
||||
|
||||
def read_source(self, sourcefile):
|
||||
with open(sourcefile, 'r') as hfile:
|
||||
self.sourcelines = hfile.read().split('\n')
|
||||
while not self.sourcelines[-1]:
|
||||
self.sourcelines = self.sourcelines[:-1]
|
||||
for x in range(len(self.sourcelines)):
|
||||
line = self.sourcelines[x]
|
||||
currip, names, comment = partitionhostsline(line)
|
||||
if currip:
|
||||
self.byip[currip] = x
|
||||
for name in names:
|
||||
self.byname[name] = x
|
||||
|
||||
def read_target(self, targetfile):
|
||||
with open(targetfile, 'r') as hfile:
|
||||
lines = hfile.read().split('\n')
|
||||
if not lines[-1]:
|
||||
lines = lines[:-1]
|
||||
for y in range(len(lines)):
|
||||
line = lines[y]
|
||||
currip, names, comment = partitionhostsline(line)
|
||||
if currip in self.byip:
|
||||
x = self.byip[currip]
|
||||
if self.sourcelines[x] is None:
|
||||
# have already consumed this enntry
|
||||
continue
|
||||
self.targlines.append(self.sourcelines[x])
|
||||
self.sourcelines[x] = None
|
||||
continue
|
||||
for name in names:
|
||||
if name in self.byname:
|
||||
x = self.byname[name]
|
||||
if self.sourcelines[x] is None:
|
||||
break
|
||||
self.targlines.append(self.sourcelines[x])
|
||||
self.sourcelines[x] = None
|
||||
break
|
||||
else:
|
||||
self.targlines.append(line)
|
||||
|
||||
def write_out(self, targetfile):
|
||||
while not self.targlines[-1]:
|
||||
self.targlines = self.targlines[:-1]
|
||||
while not self.sourcelines[-1]:
|
||||
self.sourcelines = self.sourcelines[:-1]
|
||||
with open(targetfile, 'w') as hosts:
|
||||
for line in self.targlines:
|
||||
hosts.write(line + '\n')
|
||||
for line in self.sourcelines:
|
||||
if line is not None:
|
||||
hosts.write(line + '\n')
|
||||
|
||||
|
||||
class CredMerger:
|
||||
def __init__(self):
|
||||
try:
|
||||
with open('/etc/login.defs', 'r') as ldefs:
|
||||
defs = ldefs.read().split('\n')
|
||||
except FileNotFoundError:
|
||||
defs = []
|
||||
lkup = {}
|
||||
self.discardnames = {}
|
||||
self.shadowednames = {}
|
||||
for line in defs:
|
||||
try:
|
||||
line = line[:line.index('#')]
|
||||
except ValueError:
|
||||
pass
|
||||
keyval = line.split()
|
||||
if len(keyval) < 2:
|
||||
continue
|
||||
lkup[keyval[0]] = keyval[1]
|
||||
self.uidmin = int(lkup.get('UID_MIN', 1000))
|
||||
self.uidmax = int(lkup.get('UID_MAX', 60000))
|
||||
self.gidmin = int(lkup.get('GID_MIN', 1000))
|
||||
self.gidmax = int(lkup.get('GID_MAX', 60000))
|
||||
self.shadowlines = None
|
||||
|
||||
def read_passwd(self, source, targfile=False):
|
||||
self.read_generic(source, self.uidmin, self.uidmax, targfile)
|
||||
|
||||
def read_group(self, source, targfile=False):
|
||||
self.read_generic(source, self.gidmin, self.gidmax, targfile)
|
||||
|
||||
def read_generic(self, source, minid, maxid, targfile):
|
||||
if targfile:
|
||||
self.targdata = []
|
||||
else:
|
||||
self.sourcedata = []
|
||||
with open(source, 'r') as inputfile:
|
||||
for line in inputfile.read().split('\n'):
|
||||
try:
|
||||
name, _, uid, _ = line.split(':', 3)
|
||||
uid = int(uid)
|
||||
except ValueError:
|
||||
continue
|
||||
if targfile:
|
||||
if uid <= minid or uid >= maxid:
|
||||
self.targdata.append(line)
|
||||
else:
|
||||
self.discardnames[name] = 1
|
||||
else:
|
||||
if uid >= minid and uid <= maxid:
|
||||
self.sourcedata.append(line)
|
||||
|
||||
def read_shadow(self, source):
|
||||
self.shadowlines = []
|
||||
try:
|
||||
with open(source, 'r') as inshadow:
|
||||
for line in inshadow.read().split('\n'):
|
||||
try:
|
||||
name, _ = line.split(':' , 1)
|
||||
except ValueError:
|
||||
continue
|
||||
if name in self.discardnames:
|
||||
continue
|
||||
self.shadowednames[name] = 1
|
||||
self.shadowlines.append(line)
|
||||
except FileNotFoundError:
|
||||
return
|
||||
|
||||
def write_out(self, outfile):
|
||||
with open(outfile, 'w') as targ:
|
||||
for line in self.targdata:
|
||||
targ.write(line + '\n')
|
||||
for line in self.sourcedata:
|
||||
targ.write(line + '\n')
|
||||
if outfile == '/etc/passwd':
|
||||
if self.shadowlines is None:
|
||||
self.read_shadow('/etc/shadow')
|
||||
with open('/etc/shadow', 'w') as shadout:
|
||||
for line in self.shadowlines:
|
||||
shadout.write(line + '\n')
|
||||
for line in self.sourcedata:
|
||||
name, _ = line.split(':', 1)
|
||||
if name in self.shadowednames:
|
||||
continue
|
||||
shadout.write(name + ':!:::::::\n')
|
||||
if outfile == '/etc/group':
|
||||
if self.shadowlines is None:
|
||||
self.read_shadow('/etc/gshadow')
|
||||
with open('/etc/gshadow', 'w') as shadout:
|
||||
for line in self.shadowlines:
|
||||
shadout.write(line + '\n')
|
||||
for line in self.sourcedata:
|
||||
name, _ = line.split(':' , 1)
|
||||
if name in self.shadowednames:
|
||||
continue
|
||||
shadout.write(name + ':!::\n')
|
||||
|
||||
def synchronize():
|
||||
tmpdir = tempfile.mkdtemp()
|
||||
try:
|
||||
ac = apiclient.HTTPSClient()
|
||||
data = json.dumps({'merge': tmpdir})
|
||||
status, rsp = ac.grab_url_with_status('/confluent-api/self/remotesyncfiles', data)
|
||||
if status == 202:
|
||||
while status != 204:
|
||||
status, rsp = ac.grab_url_with_status('/confluent-api/self/remotesyncfiles')
|
||||
if not isinstance(rsp, str):
|
||||
rsp = rsp.decode('utf8')
|
||||
pendpasswd = os.path.join(tmpdir, 'etc/passwd')
|
||||
if os.path.exists(pendpasswd):
|
||||
cm = CredMerger()
|
||||
cm.read_passwd(pendpasswd, targfile=False)
|
||||
cm.read_passwd('/etc/passwd', targfile=True)
|
||||
cm.write_out('/etc/passwd')
|
||||
pendgroup = os.path.join(tmpdir, 'etc/group')
|
||||
if os.path.exists(pendgroup):
|
||||
cm = CredMerger()
|
||||
cm.read_group(pendgroup, targfile=False)
|
||||
cm.read_group('/etc/group', targfile=True)
|
||||
cm.write_out('/etc/group')
|
||||
pendhosts = os.path.join(tmpdir, 'etc/hosts')
|
||||
if os.path.exists(pendhosts):
|
||||
cm = HostMerger()
|
||||
cm.read_source(pendhosts)
|
||||
cm.read_target('/etc/hosts')
|
||||
cm.write_out('/etc/hosts')
|
||||
finally:
|
||||
shutil.rmtree(tmpdir)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
synchronize()
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
deploycfg=/custom-installation/confluent/confluent.deploycfg
|
||||
mgr=$(grep ^deploy_server $deploycfg|awk '{print $2}')
|
||||
profile=$(grep ^profile: $deploycfg|awk '{print $2}')
|
||||
export deploycfg mgr profile
|
||||
curl -f https://$mgr/confluent-public/os/$profile/scripts/post.sh > /tmp/post.sh
|
||||
confluent_mgr=$(grep ^deploy_server $deploycfg|awk '{print $2}')
|
||||
confluent_profile=$(grep ^profile: $deploycfg|awk '{print $2}')
|
||||
export deploycfg confluent_mgr confluent_profile
|
||||
curl -f https://$confluent_mgr/confluent-public/os/$confluent_profile/scripts/post.sh > /tmp/post.sh
|
||||
. /tmp/post.sh
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/bin/bash
|
||||
deploycfg=/custom-installation/confluent/confluent.deploycfg
|
||||
mgr=$(grep ^deploy_server $deploycfg|awk '{print $2}')
|
||||
profile=$(grep ^profile: $deploycfg|awk '{print $2}')
|
||||
curl -f https://$mgr/confluent-public/os/$profile/scripts/pre.sh > /tmp/pre.sh
|
||||
confluent_mgr=$(grep ^deploy_server $deploycfg|awk '{print $2}')
|
||||
confluent_profile=$(grep ^profile: $deploycfg|awk '{print $2}')
|
||||
curl -f https://$confluent_mgr/confluent-public/os/$confluent_profile/scripts/pre.sh > /tmp/pre.sh
|
||||
. /tmp/pre.sh
|
||||
|
||||
@@ -7,8 +7,12 @@ if [ ! -z "$rootpw" -a "$rootpw" != "null" ]; then
|
||||
echo root:$rootpw | chpasswd -e
|
||||
fi
|
||||
nodename=$(grep ^NODENAME: /etc/confluent/confluent.info | awk '{print $2}')
|
||||
apikey=$(cat /etc/confluent/confluent.apikey)
|
||||
mgr=$(grep ^deploy_server: /etc/confluent/confluent.deploycfg |awk '{print $2}')
|
||||
confluent_apikey=$(cat /etc/confluent/confluent.apikey)
|
||||
confluent_mgr=$(grep ^deploy_server: /etc/confluent/confluent.deploycfg |awk '{print $2}')
|
||||
hostnamectl set-hostname $(grep ^NODENAME: /etc/confluent/confluent.info | awk '{print $2}')
|
||||
touch /etc/cloud/cloud-init.disabled
|
||||
curl --capath /etc/confluent/tls -f -H "CONFLUENT_NODENAME: $nodename" -H "CONFLUENT_APIKEY: $apikey" -X POST -d "status: complete" https://$mgr/confluent-api/self/updatestatus
|
||||
source /etc/confluent/functions
|
||||
|
||||
run_remote_parts firstboot.d
|
||||
run_remote_config firstboot.d
|
||||
curl --capath /etc/confluent/tls -f -H "CONFLUENT_NODENAME: $nodename" -H "CONFLUENT_APIKEY: $confluent_apikey" -X POST -d "status: complete" https://$confluent_mgr/confluent-api/self/updatestatus
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
function set_confluent_vars() {
|
||||
if [ -z "$confluent_mgr" ]; then
|
||||
confluent_mgr=$(grep ^deploy_server: /etc/confluent/confluent.deploycfg | sed -e 's/[^ ]*: //')
|
||||
fi
|
||||
if [ -z "$confluent_profile" ]; then
|
||||
confluent_profile=$(grep ^profile: /etc/confluent/confluent.deploycfg | sed -e 's/[^ ]*: //')
|
||||
fi
|
||||
}
|
||||
|
||||
fetch_remote() {
|
||||
if [ -f /etc/confluent/ca.pem ]; then
|
||||
curlargs=" --cacert /etc/confluent/ca.pem"
|
||||
fi
|
||||
set_confluent_vars
|
||||
mkdir -p $(dirname $1)
|
||||
curl -f -sS $curlargs https://$confluent_mgr/confluent-public/os/$confluent_profile/scripts/$1 > $1
|
||||
if [ $? != 0 ]; then echo $1 failed to download; return 1; fi
|
||||
}
|
||||
|
||||
run_remote_parts() {
|
||||
scriptlist=$(python3 /etc/confluent/apiclient /confluent-api/self/scriptlist/$1|sed -e 's/^- //')
|
||||
for script in $scriptlist; do
|
||||
run_remote $1/$script
|
||||
done
|
||||
}
|
||||
|
||||
run_remote() {
|
||||
requestedcmd="'$*'"
|
||||
curlargs=""
|
||||
if [ -f /etc/confluent/ca.pem ]; then
|
||||
curlargs=" --cacert /etc/confluent/ca.pem"
|
||||
fi
|
||||
set_confluent_vars
|
||||
echo
|
||||
echo '---------------------------------------------------------------------------'
|
||||
echo Running $requestedcmd from https://$confluent_mgr/confluent-public/os/$confluent_profile/scripts/
|
||||
tmpdir=$(mktemp -d)
|
||||
echo Executing in $tmpdir
|
||||
cd $tmpdir
|
||||
mkdir -p $(dirname $1)
|
||||
curl -f -sS $curlargs https://$confluent_mgr/confluent-public/os/$confluent_profile/scripts/$1 > $1
|
||||
if [ $? != 0 ]; then echo $requestedcmd failed to download; return 1; fi
|
||||
chmod +x $1
|
||||
cmd=$1
|
||||
#if [ -x /usr/bin/chcon ]; then
|
||||
# chcon system_u:object_r:bin_t:s0 $cmd
|
||||
#fi
|
||||
shift
|
||||
./$cmd $*
|
||||
retcode=$?
|
||||
echo "$requestedcmd exited with code $retcode"
|
||||
cd - > /dev/null
|
||||
return $retcode
|
||||
}
|
||||
|
||||
run_remote_python() {
|
||||
echo
|
||||
set_confluent_vars
|
||||
if [ -f /etc/confluent/ca.pem ]; then
|
||||
curlargs=" --cacert /etc/confluent/ca.pem"
|
||||
fi
|
||||
echo '---------------------------------------------------------------------------'
|
||||
echo Running python script "'$*'" from https://$confluent_mgr/confluent-public/os/$confluent_profile/scripts/
|
||||
tmpdir=$(mktemp -d)
|
||||
echo Executing in $tmpdir
|
||||
cd $tmpdir
|
||||
mkdir -p $(dirname $1)
|
||||
curl -f -sS $curlargs https://$confluent_mgr/confluent-public/os/$confluent_profile/scripts/$1 > $1
|
||||
if [ $? != 0 ]; then echo "'$*'" failed to download; return 1; fi
|
||||
python3 $*
|
||||
retcode=$?
|
||||
echo "'$*' exited with code $retcode"
|
||||
cd - > /dev/null
|
||||
return $retcode
|
||||
}
|
||||
|
||||
run_remote_config() {
|
||||
echo
|
||||
set_confluent_vars
|
||||
apiclient=/opt/confluent/bin/apiclient
|
||||
if [ -f /etc/confluent/apiclient ]; then
|
||||
apiclient=/etc/confluent/apiclient
|
||||
fi
|
||||
echo '---------------------------------------------------------------------------'
|
||||
echo Requesting to run remote configuration for "'$*'" from $confluent_mgr under profile $confluent_profile
|
||||
python3 $apiclient /confluent-api/self/remoteconfig/"$*" -d {}
|
||||
python3 $apiclient /confluent-api/self/remoteconfig/status -w 204
|
||||
echo
|
||||
echo 'Completed remote configuration'
|
||||
echo '---------------------------------------------------------------------------'
|
||||
return
|
||||
}
|
||||
|
||||
@@ -20,7 +20,9 @@ echo ' HostbasedAuthentication yes' >> $sshconf
|
||||
echo ' EnableSSHKeysign yes' >> $sshconf
|
||||
echo ' HostbasedKeyTypes *ed25519*' >> $sshconf
|
||||
|
||||
curl -f https://$mgr/confluent-public/os/$profile/scripts/firstboot.sh > /target/etc/confluent/firstboot.sh
|
||||
curl -f https://$confluent_mgr/confluent-public/os/$confluent_profile/scripts/firstboot.sh > /target/etc/confluent/firstboot.sh
|
||||
curl -f https://$confluent_mgr/confluent-public/os/$confluent_profile/scripts/functions > /target/etc/confluent/functions
|
||||
source /target/etc/confluent/functions
|
||||
chmod +x /target/etc/confluent/firstboot.sh
|
||||
cp /tmp/allnodes /target/root/.shosts
|
||||
cp /tmp/allnodes /target/etc/ssh/shosts.equiv
|
||||
@@ -40,15 +42,34 @@ if [ "$textcons" = "true" ] && ! grep console= /proc/cmdline > /dev/null; then
|
||||
updategrub=1
|
||||
fi
|
||||
fi
|
||||
kargs=$(curl https://$mgr/confluent-public/os/$profile/profile.yaml | grep ^installedargs: | sed -e 's/#.*//')
|
||||
kargs=$(curl https://$confluent_mgr/confluent-public/os/$confluent_profile/profile.yaml | grep ^installedargs: | sed -e 's/#.*//')
|
||||
if [ ! -z "$kargs" ]; then
|
||||
sed -i 's/GRUB_CMDLINE_LINUX="\([^"]*\)"/GRUB_CMDLINE_LINUX="\1 '"${kargs}"'"/' /target/etc/default/grub
|
||||
fi
|
||||
if [ 1 = $updategrub ]; then
|
||||
mount -o bind /dev /target/dev
|
||||
mount -o bind /proc /target/proc
|
||||
mount -o bind /sys /target/sys
|
||||
chroot /target update-grub
|
||||
umount /target/sys /target/dev /target/proc
|
||||
fi
|
||||
mkdir -p /opt/confluent/bin
|
||||
mkdir -p /etc/confluent
|
||||
cp -a /target/etc/confluent/* /etc/confluent
|
||||
cp /custom-installation/confluent/bin/apiclient /opt/confluent/bin/
|
||||
cp /custom-installation/confluent/bin/apiclient /target/etc/confluent/
|
||||
|
||||
mount -o bind /dev /target/dev
|
||||
mount -o bind /proc /target/proc
|
||||
mount -o bind /sys /target/sys
|
||||
if [ 1 = $updategrub ]; then
|
||||
chroot /target update-grub
|
||||
fi
|
||||
echo "Port 22" >> /etc/ssh/sshd_config
|
||||
echo "Port 2222" >> /etc/ssh/sshd_config
|
||||
echo "Match LocalPort 22" >> /etc/ssh/sshd_config
|
||||
echo " ChrootDirectory /target" >> /etc/ssh/sshd_config
|
||||
kill -HUP $(cat /run/sshd.pid)
|
||||
cat /target/etc/confluent/tls/*.pem > /target/etc/confluent/ca.pem
|
||||
cat /target/etc/confluent/tls/*.pem > /etc/confluent/ca.pem
|
||||
chroot /target bash -c "source /etc/confluent/functions; run_remote_python syncfileclient"
|
||||
chroot /target bash -c "source /etc/confluent/functions; run_remote_parts post.d"
|
||||
source /target/etc/confluent/functions
|
||||
|
||||
run_remote_config post
|
||||
|
||||
umount /target/sys /target/dev /target/proc
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ apikey=$(cat /custom-installation/confluent/confluent.apikey)
|
||||
for pubkey in /etc/ssh/ssh_host*key.pub; do
|
||||
certfile=${pubkey/.pub/-cert.pub}
|
||||
keyfile=${pubkey%.pub}
|
||||
curl -f -X POST -H "CONFLUENT_NODENAME: $nodename" -H "CONFLUENT_APIKEY: $apikey" -d @$pubkey https://$mgr/confluent-api/self/sshcert > $certfile
|
||||
curl -f -X POST -H "CONFLUENT_NODENAME: $nodename" -H "CONFLUENT_APIKEY: $apikey" -d @$pubkey https://$confluent_mgr/confluent-api/self/sshcert > $certfile
|
||||
echo HostKey $keyfile >> /etc/ssh/sshd_config.d/confluent.conf
|
||||
echo HostCertificate $certfile >> /etc/ssh/sshd_config.d/confluent.conf
|
||||
done
|
||||
@@ -23,7 +23,7 @@ echo HostbasedAuthentication yes >> /etc/ssh/sshd_config.d/confluent.conf
|
||||
echo HostbasedUsesNameFromPacketOnly yes >> /etc/ssh/sshd_config.d/confluent.conf
|
||||
echo IgnoreRhosts no >> /etc/ssh/sshd_config.d/confluent.conf
|
||||
systemctl restart sshd
|
||||
curl -f -X POST -H "CONFLUENT_NODENAME: $nodename" -H "CONFLUENT_APIKEY: $apikey" https://$mgr/confluent-api/self/nodelist > /tmp/allnodes
|
||||
curl -f https://$mgr/confluent-public/os/$profile/scripts/getinstalldisk > /custom-installation/getinstalldisk
|
||||
curl -f -X POST -H "CONFLUENT_NODENAME: $nodename" -H "CONFLUENT_APIKEY: $apikey" https://$confluent_mgr/confluent-api/self/nodelist > /tmp/allnodes
|
||||
curl -f https://$confluent_mgr/confluent-public/os/$confluent_profile/scripts/getinstalldisk > /custom-installation/getinstalldisk
|
||||
python3 /custom-installation/getinstalldisk
|
||||
sed -i s!%%INSTALLDISK%%!/dev/$(cat /tmp/installdisk)! /autoinstall.yaml
|
||||
|
||||
@@ -0,0 +1,219 @@
|
||||
#!/usr/bin/python
|
||||
import importlib
|
||||
import tempfile
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
from importlib.machinery import SourceFileLoader
|
||||
try:
|
||||
apiclient = SourceFileLoader('apiclient', '/opt/confluent/bin/apiclient').load_module()
|
||||
except FileNotFoundError:
|
||||
apiclient = SourceFileLoader('apiclient', '/etc/confluent/apiclient').load_module()
|
||||
|
||||
|
||||
def partitionhostsline(line):
|
||||
comment = ''
|
||||
try:
|
||||
cmdidx = line.index('#')
|
||||
comment = line[cmdidx:]
|
||||
line = line[:cmdidx].strip()
|
||||
except ValueError:
|
||||
pass
|
||||
if not line:
|
||||
return '', [], comment
|
||||
ipaddr, names = line.split(maxsplit=1)
|
||||
names = names.split()
|
||||
return ipaddr, names, comment
|
||||
|
||||
class HostMerger:
|
||||
def __init__(self):
|
||||
self.byip = {}
|
||||
self.byname = {}
|
||||
self.sourcelines = []
|
||||
self.targlines = []
|
||||
|
||||
def read_source(self, sourcefile):
|
||||
with open(sourcefile, 'r') as hfile:
|
||||
self.sourcelines = hfile.read().split('\n')
|
||||
while not self.sourcelines[-1]:
|
||||
self.sourcelines = self.sourcelines[:-1]
|
||||
for x in range(len(self.sourcelines)):
|
||||
line = self.sourcelines[x]
|
||||
currip, names, comment = partitionhostsline(line)
|
||||
if currip:
|
||||
self.byip[currip] = x
|
||||
for name in names:
|
||||
self.byname[name] = x
|
||||
|
||||
def read_target(self, targetfile):
|
||||
with open(targetfile, 'r') as hfile:
|
||||
lines = hfile.read().split('\n')
|
||||
if not lines[-1]:
|
||||
lines = lines[:-1]
|
||||
for y in range(len(lines)):
|
||||
line = lines[y]
|
||||
currip, names, comment = partitionhostsline(line)
|
||||
if currip in self.byip:
|
||||
x = self.byip[currip]
|
||||
if self.sourcelines[x] is None:
|
||||
# have already consumed this enntry
|
||||
continue
|
||||
self.targlines.append(self.sourcelines[x])
|
||||
self.sourcelines[x] = None
|
||||
continue
|
||||
for name in names:
|
||||
if name in self.byname:
|
||||
x = self.byname[name]
|
||||
if self.sourcelines[x] is None:
|
||||
break
|
||||
self.targlines.append(self.sourcelines[x])
|
||||
self.sourcelines[x] = None
|
||||
break
|
||||
else:
|
||||
self.targlines.append(line)
|
||||
|
||||
def write_out(self, targetfile):
|
||||
while not self.targlines[-1]:
|
||||
self.targlines = self.targlines[:-1]
|
||||
while not self.sourcelines[-1]:
|
||||
self.sourcelines = self.sourcelines[:-1]
|
||||
with open(targetfile, 'w') as hosts:
|
||||
for line in self.targlines:
|
||||
hosts.write(line + '\n')
|
||||
for line in self.sourcelines:
|
||||
if line is not None:
|
||||
hosts.write(line + '\n')
|
||||
|
||||
|
||||
class CredMerger:
|
||||
def __init__(self):
|
||||
try:
|
||||
with open('/etc/login.defs', 'r') as ldefs:
|
||||
defs = ldefs.read().split('\n')
|
||||
except FileNotFoundError:
|
||||
defs = []
|
||||
lkup = {}
|
||||
self.discardnames = {}
|
||||
self.shadowednames = {}
|
||||
for line in defs:
|
||||
try:
|
||||
line = line[:line.index('#')]
|
||||
except ValueError:
|
||||
pass
|
||||
keyval = line.split()
|
||||
if len(keyval) < 2:
|
||||
continue
|
||||
lkup[keyval[0]] = keyval[1]
|
||||
self.uidmin = int(lkup.get('UID_MIN', 1000))
|
||||
self.uidmax = int(lkup.get('UID_MAX', 60000))
|
||||
self.gidmin = int(lkup.get('GID_MIN', 1000))
|
||||
self.gidmax = int(lkup.get('GID_MAX', 60000))
|
||||
self.shadowlines = None
|
||||
|
||||
def read_passwd(self, source, targfile=False):
|
||||
self.read_generic(source, self.uidmin, self.uidmax, targfile)
|
||||
|
||||
def read_group(self, source, targfile=False):
|
||||
self.read_generic(source, self.gidmin, self.gidmax, targfile)
|
||||
|
||||
def read_generic(self, source, minid, maxid, targfile):
|
||||
if targfile:
|
||||
self.targdata = []
|
||||
else:
|
||||
self.sourcedata = []
|
||||
with open(source, 'r') as inputfile:
|
||||
for line in inputfile.read().split('\n'):
|
||||
try:
|
||||
name, _, uid, _ = line.split(':', 3)
|
||||
uid = int(uid)
|
||||
except ValueError:
|
||||
continue
|
||||
if targfile:
|
||||
if uid <= minid or uid >= maxid:
|
||||
self.targdata.append(line)
|
||||
else:
|
||||
self.discardnames[name] = 1
|
||||
else:
|
||||
if uid >= minid and uid <= maxid:
|
||||
self.sourcedata.append(line)
|
||||
|
||||
def read_shadow(self, source):
|
||||
self.shadowlines = []
|
||||
try:
|
||||
with open(source, 'r') as inshadow:
|
||||
for line in inshadow.read().split('\n'):
|
||||
try:
|
||||
name, _ = line.split(':' , 1)
|
||||
except ValueError:
|
||||
continue
|
||||
if name in self.discardnames:
|
||||
continue
|
||||
self.shadowednames[name] = 1
|
||||
self.shadowlines.append(line)
|
||||
except FileNotFoundError:
|
||||
return
|
||||
|
||||
def write_out(self, outfile):
|
||||
with open(outfile, 'w') as targ:
|
||||
for line in self.targdata:
|
||||
targ.write(line + '\n')
|
||||
for line in self.sourcedata:
|
||||
targ.write(line + '\n')
|
||||
if outfile == '/etc/passwd':
|
||||
if self.shadowlines is None:
|
||||
self.read_shadow('/etc/shadow')
|
||||
with open('/etc/shadow', 'w') as shadout:
|
||||
for line in self.shadowlines:
|
||||
shadout.write(line + '\n')
|
||||
for line in self.sourcedata:
|
||||
name, _ = line.split(':', 1)
|
||||
if name in self.shadowednames:
|
||||
continue
|
||||
shadout.write(name + ':!:::::::\n')
|
||||
if outfile == '/etc/group':
|
||||
if self.shadowlines is None:
|
||||
self.read_shadow('/etc/gshadow')
|
||||
with open('/etc/gshadow', 'w') as shadout:
|
||||
for line in self.shadowlines:
|
||||
shadout.write(line + '\n')
|
||||
for line in self.sourcedata:
|
||||
name, _ = line.split(':' , 1)
|
||||
if name in self.shadowednames:
|
||||
continue
|
||||
shadout.write(name + ':!::\n')
|
||||
|
||||
def synchronize():
|
||||
tmpdir = tempfile.mkdtemp()
|
||||
try:
|
||||
ac = apiclient.HTTPSClient()
|
||||
data = json.dumps({'merge': tmpdir})
|
||||
status, rsp = ac.grab_url_with_status('/confluent-api/self/remotesyncfiles', data)
|
||||
if status == 202:
|
||||
while status != 204:
|
||||
status, rsp = ac.grab_url_with_status('/confluent-api/self/remotesyncfiles')
|
||||
if not isinstance(rsp, str):
|
||||
rsp = rsp.decode('utf8')
|
||||
pendpasswd = os.path.join(tmpdir, 'etc/passwd')
|
||||
if os.path.exists(pendpasswd):
|
||||
cm = CredMerger()
|
||||
cm.read_passwd(pendpasswd, targfile=False)
|
||||
cm.read_passwd('/etc/passwd', targfile=True)
|
||||
cm.write_out('/etc/passwd')
|
||||
pendgroup = os.path.join(tmpdir, 'etc/group')
|
||||
if os.path.exists(pendgroup):
|
||||
cm = CredMerger()
|
||||
cm.read_group(pendgroup, targfile=False)
|
||||
cm.read_group('/etc/group', targfile=True)
|
||||
cm.write_out('/etc/group')
|
||||
pendhosts = os.path.join(tmpdir, 'etc/hosts')
|
||||
if os.path.exists(pendhosts):
|
||||
cm = HostMerger()
|
||||
cm.read_source(pendhosts)
|
||||
cm.read_target('/etc/hosts')
|
||||
cm.write_out('/etc/hosts')
|
||||
finally:
|
||||
shutil.rmtree(tmpdir)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
synchronize()
|
||||
@@ -120,6 +120,8 @@ int main(int argc, char* argv[]) {
|
||||
ws.ws_col = 100;
|
||||
ws.ws_row = 31;
|
||||
}
|
||||
if (ws.ws_col < 80) { ws.ws_col = 80; }
|
||||
if (ws.ws_row < 24) { ws.ws_col = 24; }
|
||||
ioctl(ttyf, TIOCSWINSZ, &ws);
|
||||
tcsetattr(ttyf, TCSANOW, &tty);
|
||||
ioctl(ttyf, TIOCCONS, 0);
|
||||
|
||||
@@ -82,9 +82,15 @@ int main(int argc, char* argv[]) {
|
||||
if (sock < 0) continue;
|
||||
setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &(int){1}, sizeof(int));
|
||||
if (curr->ai_family == AF_INET) {
|
||||
bind(sock, (struct sockaddr*)&net4bind, sizeof(struct sockaddr_in));
|
||||
if (bind(sock, (struct sockaddr*)&net4bind, sizeof(struct sockaddr_in)) < 0) {
|
||||
fprintf(stderr, "Unable to bind port 302\n");
|
||||
exit(1);
|
||||
}
|
||||
} else if (curr->ai_family == AF_INET6) {
|
||||
bind(sock, (struct sockaddr*)&net6bind, sizeof(struct sockaddr_in6));
|
||||
if (bind(sock, (struct sockaddr*)&net6bind, sizeof(struct sockaddr_in6)) < 0) {
|
||||
fprintf(stderr, "Unable to bind port 302\n");
|
||||
exit(1);
|
||||
}
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ if path.startswith('/opt'):
|
||||
sys.path.append(path)
|
||||
|
||||
import confluent.client as client
|
||||
import confluent.sortutil as sortutil
|
||||
import confluent.tlvdata as tlvdata
|
||||
|
||||
try:
|
||||
@@ -107,11 +108,11 @@ def show_collective():
|
||||
if 'active' in res['collective']:
|
||||
if res['collective']['active']:
|
||||
print('Active collective members:')
|
||||
for member in res['collective']['active']:
|
||||
for member in sortutil.natural_sort(res['collective']['active']):
|
||||
print(' {0}'.format(member))
|
||||
if res['collective']['offline']:
|
||||
print('Offline collective members:')
|
||||
for member in res['collective']['offline']:
|
||||
for member in sortutil.natural_sort(res['collective']['offline']):
|
||||
print(' {0}'.format(member))
|
||||
else:
|
||||
print('Run collective show on leader for more data')
|
||||
|
||||
@@ -38,7 +38,7 @@ def main(args):
|
||||
ap = argparse.ArgumentParser(description='Manage OS deployment resources')
|
||||
sp = ap.add_subparsers(dest='command')
|
||||
wiz = sp.add_parser('initialize', help='Do OS deployment preparation')
|
||||
wiz.add_argument('-a', help='Initialize SSH access by confluent to nodes for automation such as ansible playbook execution', action='store_true')
|
||||
wiz.add_argument('-a', help='Initialize SSH access by confluent to nodes for automation such as ansible playbook execution or syncfiles', action='store_true')
|
||||
wiz.add_argument('-g', help='Initialize a Genesis profile to boot systems into a rescue or staging environment', action='store_true')
|
||||
wiz.add_argument('-u', help='Pull in root user key for node deployment', action='store_true')
|
||||
wiz.add_argument('-s', help='Set up SSH CA for managing node to node ssh and known hosts', action='store_true')
|
||||
@@ -101,6 +101,8 @@ def initialize_genesis():
|
||||
'/var/lib/confluent/public/os/genesis-x86_64/boot/initramfs/addons.cpio')
|
||||
os.symlink('/opt/confluent/genesis/x86_64/boot/kernel',
|
||||
'/var/lib/confluent/public/os/genesis-x86_64/boot/kernel')
|
||||
shutil.copytree('/opt/confluent/lib/osdeploy/genesis/profiles/default/ansible/',
|
||||
'/var/lib/confluent/public/os/genesis-x86_64/ansible/')
|
||||
shutil.copytree('/opt/confluent/lib/osdeploy/genesis/profiles/default/scripts/',
|
||||
'/var/lib/confluent/public/os/genesis-x86_64/scripts/')
|
||||
shutil.copyfile('/opt/confluent/lib/osdeploy/genesis/profiles/default/profile.yaml',
|
||||
@@ -359,22 +361,24 @@ def osimport(imagefile):
|
||||
print('Importing from {0} to {1}'.format(imagefile, rsp['target']))
|
||||
else:
|
||||
print(repr(rsp))
|
||||
while importing:
|
||||
for rsp in c.read('/deployment/importing/{0}'.format(shortname)):
|
||||
if 'progress' in rsp:
|
||||
sys.stdout.write('{0}: {1:.2f}% \r'.format(rsp['phase'],
|
||||
rsp['progress']))
|
||||
if rsp['phase'] == 'complete':
|
||||
importing = False
|
||||
sys.stdout.write('\n')
|
||||
for profile in rsp['profiles']:
|
||||
print('Deployment profile created: {0}'.format(profile))
|
||||
sys.stdout.flush()
|
||||
else:
|
||||
print(repr(rsp))
|
||||
time.sleep(0.5)
|
||||
if shortname:
|
||||
list(c.delete('/deployment/importing/{0}'.format(shortname)))
|
||||
try:
|
||||
while importing:
|
||||
for rsp in c.read('/deployment/importing/{0}'.format(shortname)):
|
||||
if 'progress' in rsp:
|
||||
sys.stdout.write('{0}: {1:.2f}% \r'.format(rsp['phase'],
|
||||
rsp['progress']))
|
||||
if rsp['phase'] == 'complete':
|
||||
importing = False
|
||||
sys.stdout.write('\n')
|
||||
for profile in rsp['profiles']:
|
||||
print('Deployment profile created: {0}'.format(profile))
|
||||
sys.stdout.flush()
|
||||
else:
|
||||
print(repr(rsp))
|
||||
time.sleep(0.5)
|
||||
finally:
|
||||
if shortname:
|
||||
list(c.delete('/deployment/importing/{0}'.format(shortname)))
|
||||
|
||||
if __name__ == '__main__':
|
||||
main(sys.argv)
|
||||
|
||||
@@ -72,7 +72,7 @@ def create_certificate(keyout=None, certout=None):
|
||||
if not keyout:
|
||||
raise Exception('Unable to locate TLS certificate path automatically')
|
||||
shortname = socket.gethostname().split('.')[0]
|
||||
longname = socket.getfqdn()
|
||||
longname = shortname # socket.getfqdn()
|
||||
subprocess.check_call(
|
||||
['openssl', 'ecparam', '-name', 'secp384r1', '-genkey', '-out',
|
||||
keyout])
|
||||
@@ -81,7 +81,7 @@ def create_certificate(keyout=None, certout=None):
|
||||
# there exists non-compliant clients that fail with them as IP
|
||||
san.extend(['DNS:{0}'.format(x) for x in get_ip_addresses()])
|
||||
san.append('DNS:{0}'.format(shortname))
|
||||
san.append('DNS:{0}'.format(longname))
|
||||
#san.append('DNS:{0}'.format(longname))
|
||||
san = ','.join(san)
|
||||
sslcfg = get_openssl_conf_location()
|
||||
tmpconfig = tempfile.mktemp()
|
||||
|
||||
@@ -23,9 +23,11 @@ import confluent.noderange as noderange
|
||||
import confluent.tlvdata as tlvdata
|
||||
import confluent.util as util
|
||||
import eventlet
|
||||
import eventlet.greenpool as greenpool
|
||||
import eventlet.green.socket as socket
|
||||
import eventlet.green.ssl as ssl
|
||||
import eventlet.green.threading as threading
|
||||
import confluent.sortutil as sortutil
|
||||
import greenlet
|
||||
import random
|
||||
import time
|
||||
@@ -41,6 +43,8 @@ currentleader = None
|
||||
follower = None
|
||||
retrythread = None
|
||||
failovercheck = None
|
||||
initting = True
|
||||
reassimilate = None
|
||||
|
||||
class ContextBool(object):
|
||||
def __init__(self):
|
||||
@@ -58,7 +62,7 @@ class ContextBool(object):
|
||||
connecting = ContextBool()
|
||||
leader_init = ContextBool()
|
||||
|
||||
def connect_to_leader(cert=None, name=None, leader=None):
|
||||
def connect_to_leader(cert=None, name=None, leader=None, remote=None):
|
||||
global currentleader
|
||||
global follower
|
||||
if leader is None:
|
||||
@@ -66,7 +70,7 @@ def connect_to_leader(cert=None, name=None, leader=None):
|
||||
log.log({'info': 'Attempting connection to leader {0}'.format(leader),
|
||||
'subsystem': 'collective'})
|
||||
try:
|
||||
remote = connect_to_collective(cert, leader)
|
||||
remote = connect_to_collective(cert, leader, remote)
|
||||
except socket.error as e:
|
||||
log.log({'error': 'Collective connection attempt to {0} failed: {1}'
|
||||
''.format(leader, str(e)),
|
||||
@@ -116,7 +120,7 @@ def connect_to_leader(cert=None, name=None, leader=None):
|
||||
follower.kill()
|
||||
cfm.stop_following()
|
||||
follower = None
|
||||
if follower:
|
||||
if follower is not None:
|
||||
follower.kill()
|
||||
cfm.stop_following()
|
||||
follower = None
|
||||
@@ -156,15 +160,20 @@ def connect_to_leader(cert=None, name=None, leader=None):
|
||||
raise
|
||||
currentleader = leader
|
||||
#spawn this as a thread...
|
||||
remote.settimeout(90)
|
||||
follower = eventlet.spawn(follow_leader, remote, leader)
|
||||
return True
|
||||
|
||||
|
||||
def follow_leader(remote, leader):
|
||||
global currentleader
|
||||
global retrythread
|
||||
global follower
|
||||
cleanexit = False
|
||||
newleader = None
|
||||
try:
|
||||
cfm.follow_channel(remote)
|
||||
exitcause = cfm.follow_channel(remote)
|
||||
newleader = exitcause.get('newleader', None)
|
||||
except greenlet.GreenletExit:
|
||||
cleanexit = True
|
||||
finally:
|
||||
@@ -172,20 +181,39 @@ def follow_leader(remote, leader):
|
||||
log.log({'info': 'Previous following cleanly closed',
|
||||
'subsystem': 'collective'})
|
||||
return
|
||||
if newleader:
|
||||
log.log(
|
||||
{'info': 'Previous leader directed us to join new leader {}'.format(newleader)})
|
||||
if connect_to_leader(None, get_myname(), newleader):
|
||||
return
|
||||
log.log({'info': 'Current leader ({0}) has disappeared, restarting '
|
||||
'collective membership'.format(leader), 'subsystem': 'collective'})
|
||||
# The leader has folded, time to startup again...
|
||||
follower = None
|
||||
cfm.stop_following()
|
||||
currentleader = None
|
||||
eventlet.spawn_n(start_collective)
|
||||
if retrythread is None: # start a recovery
|
||||
retrythread = eventlet.spawn_after(
|
||||
random.random(), start_collective)
|
||||
|
||||
def create_connection(member):
|
||||
remote = None
|
||||
try:
|
||||
remote = socket.create_connection((member, 13001), 2)
|
||||
remote.settimeout(15)
|
||||
# TLS cert validation is custom and will not pass normal CA vetting
|
||||
# to override completely in the right place requires enormous effort, so just defer until after connect
|
||||
remote = ssl.wrap_socket(remote, cert_reqs=ssl.CERT_NONE, keyfile='/etc/confluent/privkey.pem',
|
||||
certfile='/etc/confluent/srvcert.pem')
|
||||
except Exception as e:
|
||||
return member, e
|
||||
return member, remote
|
||||
|
||||
def connect_to_collective(cert, member):
|
||||
remote = socket.create_connection((member, 13001))
|
||||
# TLS cert validation is custom and will not pass normal CA vetting
|
||||
# to override completely in the right place requires enormous effort, so just defer until after connect
|
||||
remote = ssl.wrap_socket(remote, cert_reqs=ssl.CERT_NONE, keyfile='/etc/confluent/privkey.pem',
|
||||
certfile='/etc/confluent/srvcert.pem')
|
||||
def connect_to_collective(cert, member, remote=None):
|
||||
if remote is None:
|
||||
_, remote = create_connection(member)
|
||||
if isinstance(remote, Exception):
|
||||
raise remote
|
||||
if cert:
|
||||
fprint = cert
|
||||
else:
|
||||
@@ -216,6 +244,8 @@ def get_myname():
|
||||
def handle_connection(connection, cert, request, local=False):
|
||||
global currentleader
|
||||
global retrythread
|
||||
global initting
|
||||
connection.settimeout(5)
|
||||
operation = request['operation']
|
||||
if cert:
|
||||
cert = crypto.dump_certificate(crypto.FILETYPE_ASN1, cert)
|
||||
@@ -229,9 +259,9 @@ def handle_connection(connection, cert, request, local=False):
|
||||
'enabled on this '
|
||||
'system'}})
|
||||
return
|
||||
if follower:
|
||||
if follower is not None:
|
||||
linfo = cfm.get_collective_member_by_address(currentleader)
|
||||
remote = socket.create_connection((currentleader, 13001))
|
||||
remote = socket.create_connection((currentleader, 13001), 15)
|
||||
remote = ssl.wrap_socket(remote, cert_reqs=ssl.CERT_NONE,
|
||||
keyfile='/etc/confluent/privkey.pem',
|
||||
certfile='/etc/confluent/srvcert.pem')
|
||||
@@ -307,7 +337,7 @@ def handle_connection(connection, cert, request, local=False):
|
||||
return
|
||||
host = request['server']
|
||||
try:
|
||||
remote = socket.create_connection((host, 13001))
|
||||
remote = socket.create_connection((host, 13001), 15)
|
||||
# This isn't what it looks like. We do CERT_NONE to disable
|
||||
# openssl verification, but then use the invitation as a
|
||||
# shared secret to validate the certs as part of the join
|
||||
@@ -366,6 +396,11 @@ def handle_connection(connection, cert, request, local=False):
|
||||
tlvdata.send(connection, {'error': 'Invalid token'})
|
||||
connection.close()
|
||||
return
|
||||
if not list(cfm.list_collective()):
|
||||
# First enrollment of a collective, since the collective doesn't
|
||||
# quite exist, then set initting false to let the enrollment action
|
||||
# drive this particular initialization
|
||||
initting = False
|
||||
myrsp = base64.b64encode(myrsp)
|
||||
fprint = util.get_fingerprint(cert)
|
||||
myfprint = util.get_fingerprint(mycert)
|
||||
@@ -399,6 +434,36 @@ def handle_connection(connection, cert, request, local=False):
|
||||
'transaction count',
|
||||
'txcount': cfm._txcount,})
|
||||
return
|
||||
if cfm.cfgstreams and request['txcount'] == cfm._txcount:
|
||||
try:
|
||||
cfm.check_quorum()
|
||||
tlvdata.send(connection,
|
||||
{'error': 'Refusing to be assimilated as I am a leader with quorum',
|
||||
'txcount': cfm._txcount,})
|
||||
return
|
||||
except exc.DegradedCollective:
|
||||
followcount = request.get('followcount', None)
|
||||
myfollowcount = len(list(cfm.cfgstreams))
|
||||
if followcount is not None:
|
||||
if followcount < myfollowcount:
|
||||
tlvdata.send(connection,
|
||||
{'error': 'Refusing to be assimilated by leader with fewer followers',
|
||||
'txcount': cfm._txcount,})
|
||||
return
|
||||
elif followcount == myfollowcount:
|
||||
myname = sortutil.naturalize_string(get_myname())
|
||||
if myname < sortutil.naturalize_string(request['name']):
|
||||
tlvdata.send(connection,
|
||||
{'error': 'Refusing, my name is better',
|
||||
'txcount': cfm._txcount,})
|
||||
return
|
||||
if follower is not None and not follower.dead:
|
||||
tlvdata.send(
|
||||
connection,
|
||||
{'error': 'Already following, assimilate leader first',
|
||||
'leader': currentleader})
|
||||
connection.close()
|
||||
return
|
||||
if connecting.active:
|
||||
# don't try to connect while actively already trying to connect
|
||||
tlvdata.send(connection, {'status': 0})
|
||||
@@ -413,10 +478,15 @@ def handle_connection(connection, cert, request, local=False):
|
||||
return
|
||||
log.log({'info': 'Connecting in response to assimilation',
|
||||
'subsystem': 'collective'})
|
||||
eventlet.spawn_n(connect_to_leader, None, None,
|
||||
leader=connection.getpeername()[0])
|
||||
if cfm.cfgstreams:
|
||||
retire_as_leader(connection.getpeername())
|
||||
tlvdata.send(connection, {'status': 0})
|
||||
newleader = connection.getpeername()[0]
|
||||
connection.close()
|
||||
if not connect_to_leader(None, None, leader=newleader):
|
||||
if retrythread is None:
|
||||
retrythread = eventlet.spawn_after(random.random(),
|
||||
start_collective)
|
||||
if 'getinfo' == operation:
|
||||
drone = request['name']
|
||||
droneinfo = cfm.get_collective_member(drone)
|
||||
@@ -441,7 +511,7 @@ def handle_connection(connection, cert, request, local=False):
|
||||
connection.close()
|
||||
return
|
||||
myself = connection.getsockname()[0]
|
||||
if connecting.active:
|
||||
if connecting.active or initting:
|
||||
tlvdata.send(connection, {'error': 'Connecting right now',
|
||||
'backoff': True})
|
||||
connection.close()
|
||||
@@ -460,12 +530,15 @@ def handle_connection(connection, cert, request, local=False):
|
||||
'should assimilate me, connecting..',
|
||||
'txcount': cfm._txcount})
|
||||
log.log({'info': 'Connecting to leader due to superior '
|
||||
'transaction count', 'subsystem': collective})
|
||||
eventlet.spawn_n(connect_to_leader, None, None,
|
||||
connection.getpeername()[0])
|
||||
'transaction count', 'subsystem': 'collective'})
|
||||
connection.close()
|
||||
if not connect_to_leader(
|
||||
None, None, connection.getpeername()[0]):
|
||||
if retrythread is None:
|
||||
retrythread = eventlet.spawn_after(5 + random.random(),
|
||||
start_collective)
|
||||
return
|
||||
if retrythread:
|
||||
if retrythread is not None:
|
||||
retrythread.cancel()
|
||||
retrythread = None
|
||||
with leader_init:
|
||||
@@ -480,10 +553,13 @@ def handle_connection(connection, cert, request, local=False):
|
||||
connection.sendall(cfgdata)
|
||||
#tlvdata.send(connection, {'tenants': 0}) # skip the tenants for now,
|
||||
# so far unused anyway
|
||||
connection.settimeout(90)
|
||||
if not cfm.relay_slaved_requests(drone, connection):
|
||||
if not retrythread: # start a recovery if everyone else seems
|
||||
log.log({'info': 'All clients have disconnected, starting recovery process',
|
||||
'subsystem': 'collective'})
|
||||
if retrythread is None: # start a recovery if everyone else seems
|
||||
# to have disappeared
|
||||
retrythread = eventlet.spawn_after(30 + random.random(),
|
||||
retrythread = eventlet.spawn_after(5 + random.random(),
|
||||
start_collective)
|
||||
# ok, we have a connecting member whose certificate checks out
|
||||
# He needs to bootstrap his configuration and subscribe it to updates
|
||||
@@ -501,15 +577,17 @@ def populate_collinfo(collinfo):
|
||||
collinfo['offline'].append(member)
|
||||
|
||||
|
||||
def try_assimilate(drone):
|
||||
def try_assimilate(drone, followcount, remote):
|
||||
global retrythread
|
||||
try:
|
||||
remote = connect_to_collective(None, drone)
|
||||
remote = connect_to_collective(None, drone, remote)
|
||||
except socket.error:
|
||||
# Oh well, unable to connect, hopefully the rest will be
|
||||
# in order
|
||||
return
|
||||
tlvdata.send(remote, {'collective': {'operation': 'assimilate',
|
||||
'name': get_myname(),
|
||||
'followcount': followcount,
|
||||
'txcount': cfm._txcount}})
|
||||
tlvdata.recv(remote) # the banner
|
||||
tlvdata.recv(remote) # authpassed... 0..
|
||||
@@ -520,24 +598,33 @@ def try_assimilate(drone):
|
||||
'No answer from {0} while trying to assimilate'.format(
|
||||
drone),
|
||||
'subsystem': 'collective'})
|
||||
return
|
||||
return True
|
||||
if 'txcount' in answer:
|
||||
log.log({'info': 'Deferring to {0} due to transaction count'.format(
|
||||
log.log({'info': 'Deferring to {0} due to target being a better leader'.format(
|
||||
drone), 'subsystem': 'collective'})
|
||||
connect_to_leader(None, None, leader=remote.getpeername()[0])
|
||||
return
|
||||
retire_as_leader(drone)
|
||||
if not connect_to_leader(None, None, leader=remote.getpeername()[0]):
|
||||
if retrythread is None:
|
||||
retrythread = eventlet.spawn_after(random.random(),
|
||||
start_collective)
|
||||
return False
|
||||
if 'leader' in answer:
|
||||
# Will wait for leader to see about assimilation
|
||||
return True
|
||||
if 'error' in answer:
|
||||
log.log({
|
||||
'error': 'Error encountered while attempting to '
|
||||
'assimilate {0}: {1}'.format(drone, answer['error']),
|
||||
'subsystem': 'collective'})
|
||||
return
|
||||
return True
|
||||
log.log({'info': 'Assimilated {0} into collective'.format(drone),
|
||||
'subsystem': 'collective'})
|
||||
return True
|
||||
|
||||
|
||||
def get_leader(connection):
|
||||
if currentleader is None or connection.getpeername()[0] == currentleader:
|
||||
# cancel retry if a retry is pending
|
||||
if currentleader is None:
|
||||
msg = 'Becoming leader as no leader known'
|
||||
else:
|
||||
@@ -547,36 +634,62 @@ def get_leader(connection):
|
||||
become_leader(connection)
|
||||
return currentleader
|
||||
|
||||
def retire_as_leader():
|
||||
def retire_as_leader(newleader=None):
|
||||
global currentleader
|
||||
cfm.stop_leading()
|
||||
cfm.stop_leading(newleader)
|
||||
currentleader = None
|
||||
|
||||
def become_leader(connection):
|
||||
global currentleader
|
||||
global follower
|
||||
global retrythread
|
||||
global reassimilate
|
||||
log.log({'info': 'Becoming leader of collective',
|
||||
'subsystem': 'collective'})
|
||||
if follower:
|
||||
if follower is not None:
|
||||
follower.kill()
|
||||
cfm.stop_following()
|
||||
follower = None
|
||||
if retrythread:
|
||||
if retrythread is not None:
|
||||
retrythread.cancel()
|
||||
retrythread = None
|
||||
currentleader = connection.getsockname()[0]
|
||||
skipaddr = connection.getpeername()[0]
|
||||
if _assimilate_missing(skipaddr):
|
||||
schedule_rebalance()
|
||||
if reassimilate is not None:
|
||||
reassimilate.kill()
|
||||
reassimilate = eventlet.spawn(reassimilate_missing)
|
||||
|
||||
def reassimilate_missing():
|
||||
eventlet.sleep(30)
|
||||
while cfm.cfgstreams and _assimilate_missing():
|
||||
eventlet.sleep(30)
|
||||
|
||||
def _assimilate_missing(skipaddr=None):
|
||||
connecto = []
|
||||
myname = get_myname()
|
||||
skipem = set(cfm.cfgstreams)
|
||||
numfollowers = len(skipem)
|
||||
skipem.add(currentleader)
|
||||
skipem.add(skipaddr)
|
||||
if skipaddr is not None:
|
||||
skipem.add(skipaddr)
|
||||
for member in cfm.list_collective():
|
||||
dronecandidate = cfm.get_collective_member(member)['address']
|
||||
if dronecandidate in skipem or member == myname:
|
||||
if dronecandidate in skipem or member == myname or member in skipem:
|
||||
continue
|
||||
eventlet.spawn_n(try_assimilate, dronecandidate)
|
||||
schedule_rebalance()
|
||||
connecto.append(dronecandidate)
|
||||
if not connecto:
|
||||
return True
|
||||
conpool = greenpool.GreenPool(64)
|
||||
connections = conpool.imap(create_connection, connecto)
|
||||
for ent in connections:
|
||||
member, remote = ent
|
||||
if isinstance(remote, Exception):
|
||||
continue
|
||||
if not try_assimilate(member, numfollowers, remote):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def startup():
|
||||
@@ -589,6 +702,11 @@ def startup():
|
||||
def check_managers():
|
||||
global failovercheck
|
||||
if not follower:
|
||||
try:
|
||||
cfm.check_quorum()
|
||||
except exc.DegradedCollective:
|
||||
failovercheck = None
|
||||
return
|
||||
c = cfm.ConfigManager(None)
|
||||
collinfo = {}
|
||||
populate_collinfo(collinfo)
|
||||
@@ -624,44 +742,62 @@ def check_managers():
|
||||
continue
|
||||
c.set_node_attributes({node: {'collective.manager': {'value': targets[0]}}})
|
||||
availmanagers[targets[0]] += 1
|
||||
_assimilate_missing()
|
||||
failovercheck = None
|
||||
|
||||
def schedule_rebalance():
|
||||
global failovercheck
|
||||
if not failovercheck:
|
||||
failovercheck = True
|
||||
failovercheck = eventlet.spawn_after(10, check_managers)
|
||||
|
||||
def start_collective():
|
||||
cfm.membership_callback = schedule_rebalance
|
||||
global follower
|
||||
global retrythread
|
||||
if follower:
|
||||
follower.kill()
|
||||
cfm.stop_following()
|
||||
follower = None
|
||||
global initting
|
||||
initting = True
|
||||
retrythread = None
|
||||
try:
|
||||
if cfm.cfgstreams:
|
||||
cfm.check_quorum()
|
||||
# Do not start if we have quorum and are leader
|
||||
cfm.membership_callback = schedule_rebalance
|
||||
if follower is not None:
|
||||
initting = False
|
||||
return
|
||||
except exc.DegradedCollective:
|
||||
try:
|
||||
if cfm.cfgstreams:
|
||||
cfm.check_quorum()
|
||||
# Do not start if we have quorum and are leader
|
||||
return
|
||||
except exc.DegradedCollective:
|
||||
pass
|
||||
if leader_init.active: # do not start trying to connect if we are
|
||||
# xmitting data to a follower
|
||||
return
|
||||
myname = get_myname()
|
||||
connecto = []
|
||||
for member in sorted(list(cfm.list_collective())):
|
||||
if member == myname:
|
||||
continue
|
||||
if cfm.cfgleader is None:
|
||||
cfm.stop_following(True)
|
||||
ldrcandidate = cfm.get_collective_member(member)['address']
|
||||
connecto.append(ldrcandidate)
|
||||
conpool = greenpool.GreenPool(64)
|
||||
connections = conpool.imap(create_connection, connecto)
|
||||
for ent in connections:
|
||||
member, remote = ent
|
||||
if isinstance(remote, Exception):
|
||||
continue
|
||||
if follower is None:
|
||||
log.log({'info': 'Performing startup attempt to {0}'.format(
|
||||
member), 'subsystem': 'collective'})
|
||||
if not connect_to_leader(name=myname, leader=member, remote=remote):
|
||||
remote.close()
|
||||
else:
|
||||
remote.close()
|
||||
except Exception as e:
|
||||
pass
|
||||
if leader_init.active: # do not start trying to connect if we are
|
||||
# xmitting data to a follower
|
||||
return
|
||||
myname = get_myname()
|
||||
for member in sorted(list(cfm.list_collective())):
|
||||
if member == myname:
|
||||
continue
|
||||
if cfm.cfgleader is None:
|
||||
cfm.stop_following(True)
|
||||
ldrcandidate = cfm.get_collective_member(member)['address']
|
||||
log.log({'info': 'Performing startup attempt to {0}'.format(
|
||||
ldrcandidate), 'subsystem': 'collective'})
|
||||
if connect_to_leader(name=myname, leader=ldrcandidate):
|
||||
break
|
||||
else:
|
||||
retrythread = eventlet.spawn_after(30 + random.random(),
|
||||
start_collective)
|
||||
|
||||
|
||||
finally:
|
||||
if retrythread is None and follower is None:
|
||||
retrythread = eventlet.spawn_after(5 + random.random(),
|
||||
start_collective)
|
||||
initting = False
|
||||
|
||||
@@ -409,7 +409,10 @@ def _push_rpc(stream, payload):
|
||||
return True
|
||||
except Exception:
|
||||
logException()
|
||||
del cfgstreams[stream]
|
||||
try:
|
||||
del cfgstreams[stream]
|
||||
except KeyError:
|
||||
pass
|
||||
if membership_callback:
|
||||
membership_callback()
|
||||
stream.close()
|
||||
@@ -736,9 +739,14 @@ def stop_following(replacement=None):
|
||||
pass
|
||||
cfgleader = replacement
|
||||
|
||||
def stop_leading():
|
||||
def stop_leading(newleader=None):
|
||||
rpcpayload = None
|
||||
if newleader is not None:
|
||||
rpcpayload = msgpack.packb({'newleader': newleader}, use_bin_type=False)
|
||||
for stream in list(cfgstreams):
|
||||
try:
|
||||
if rpcpayload is not None:
|
||||
_push_rpc(cfgstreams[stream], rpcpayload)
|
||||
cfgstreams[stream].close()
|
||||
except Exception:
|
||||
pass
|
||||
@@ -824,6 +832,8 @@ def follow_channel(channel):
|
||||
rpc = msgpack.unpackb(rpc, raw=False)
|
||||
if 'txcount' in rpc:
|
||||
_txcount = rpc['txcount']
|
||||
if 'newleader' in rpc:
|
||||
return rpc
|
||||
if 'function' in rpc:
|
||||
if not (rpc['function'].startswith('_true') or rpc['function'].startswith('_rpc')):
|
||||
raise Exception("Received unsupported function call: {0}".format(rpc['function']))
|
||||
@@ -853,6 +863,7 @@ def follow_channel(channel):
|
||||
stop_following(None)
|
||||
else:
|
||||
stop_following(True)
|
||||
return {}
|
||||
|
||||
|
||||
def add_collective_member(name, address, fingerprint):
|
||||
|
||||
@@ -33,42 +33,70 @@ import confluent.tlvdata as tlvdata
|
||||
import confluent.util as util
|
||||
import eventlet
|
||||
import eventlet.event
|
||||
import eventlet.green.os as os
|
||||
import eventlet.green.select as select
|
||||
import eventlet.green.socket as socket
|
||||
import eventlet.green.subprocess as subprocess
|
||||
import eventlet.green.ssl as ssl
|
||||
import pyte
|
||||
import eventlet.semaphore as semaphore
|
||||
import fcntl
|
||||
import random
|
||||
import struct
|
||||
import time
|
||||
import traceback
|
||||
|
||||
_handled_consoles = {}
|
||||
|
||||
_tracelog = None
|
||||
_bufferdaemon = None
|
||||
_bufferlock = None
|
||||
|
||||
try:
|
||||
range = xrange
|
||||
except NameError:
|
||||
pass
|
||||
|
||||
pytecolors2ansi = {
|
||||
'black': 0,
|
||||
'red': 1,
|
||||
'green': 2,
|
||||
'brown': 3,
|
||||
'blue': 4,
|
||||
'magenta': 5,
|
||||
'cyan': 6,
|
||||
'white': 7,
|
||||
'default': 9,
|
||||
}
|
||||
# might be able to use IBMPC map from pyte charsets,
|
||||
# in that case, would have to mask out certain things (like ESC)
|
||||
# in the same way that Screen's draw method would do
|
||||
# for now at least get some of the arrows in there (note ESC is one
|
||||
# of those arrows... so skip it...
|
||||
ansichars = dict(zip((0x18, 0x19), u'\u2191\u2193'))
|
||||
|
||||
def chunk_output(output, n):
|
||||
for i in range(0, len(output), n):
|
||||
yield output[i:i + n]
|
||||
|
||||
def get_buffer_output(nodename):
|
||||
out = _bufferdaemon.stdin
|
||||
instream = _bufferdaemon.stdout
|
||||
if not isinstance(nodename, bytes):
|
||||
nodename = nodename.encode('utf8')
|
||||
outdata = bytearray()
|
||||
with _bufferlock:
|
||||
out.write(struct.pack('I', len(nodename)))
|
||||
out.write(nodename)
|
||||
out.flush()
|
||||
select.select((instream,), (), (), 30)
|
||||
while not outdata or outdata[-1]:
|
||||
try:
|
||||
chunk = os.read(instream.fileno(), 128)
|
||||
except IOError:
|
||||
chunk = None
|
||||
if chunk:
|
||||
outdata.extend(chunk)
|
||||
else:
|
||||
select.select((instream,), (), (), 0)
|
||||
return bytes(outdata[:-1])
|
||||
|
||||
|
||||
def _utf8_normalize(data, shiftin, decoder):
|
||||
def send_output(nodename, output):
|
||||
if not isinstance(nodename, bytes):
|
||||
nodename = nodename.encode('utf8')
|
||||
with _bufferlock:
|
||||
_bufferdaemon.stdin.write(struct.pack('I', len(nodename) | (1 << 29)))
|
||||
_bufferdaemon.stdin.write(nodename)
|
||||
_bufferdaemon.stdin.flush()
|
||||
for chunk in chunk_output(output, 8192):
|
||||
_bufferdaemon.stdin.write(struct.pack('I', len(chunk) | (2 << 29)))
|
||||
_bufferdaemon.stdin.write(chunk)
|
||||
_bufferdaemon.stdin.flush()
|
||||
|
||||
def _utf8_normalize(data, decoder):
|
||||
# first we give the stateful decoder a crack at the byte stream,
|
||||
# we may come up empty in the event of a partial multibyte
|
||||
try:
|
||||
@@ -88,60 +116,9 @@ def _utf8_normalize(data, shiftin, decoder):
|
||||
# Finally, the low part of ascii is valid utf-8, but we are going to be
|
||||
# more interested in the cp437 versions (since this is console *output*
|
||||
# not input
|
||||
if shiftin is None:
|
||||
data = data.translate(ansichars)
|
||||
return data.encode('utf-8')
|
||||
|
||||
|
||||
def pytechars2line(chars, maxlen=None):
|
||||
line = b'\x1b[m' # start at default params
|
||||
lb = False # last bold
|
||||
li = False # last italic
|
||||
lu = False # last underline
|
||||
ls = False # last strikethrough
|
||||
lr = False # last reverse
|
||||
lfg = 'default' # last fg color
|
||||
lbg = 'default' # last bg color
|
||||
hasdata = False
|
||||
len = 1
|
||||
for charidx in range(maxlen):
|
||||
char = chars[charidx]
|
||||
csi = bytearray([])
|
||||
if char.fg != lfg:
|
||||
csi.append(30 + pytecolors2ansi.get(char.fg, 9))
|
||||
lfg = char.fg
|
||||
if char.bg != lbg:
|
||||
csi.append(40 + pytecolors2ansi.get(char.bg, 9))
|
||||
lbg = char.bg
|
||||
if char.bold != lb:
|
||||
lb = char.bold
|
||||
csi.append(1 if lb else 22)
|
||||
if char.italics != li:
|
||||
li = char.italics
|
||||
csi.append(3 if li else 23)
|
||||
if char.underscore != lu:
|
||||
lu = char.underscore
|
||||
csi.append(4 if lu else 24)
|
||||
if char.strikethrough != ls:
|
||||
ls = char.strikethrough
|
||||
csi.append(9 if ls else 29)
|
||||
if char.reverse != lr:
|
||||
lr = char.reverse
|
||||
csi.append(7 if lr else 27)
|
||||
if csi:
|
||||
line += b'\x1b[' + b';'.join(['{0}'.format(x).encode('utf-8') for x in csi]) + b'm'
|
||||
if not hasdata and char.data.rstrip():
|
||||
hasdata = True
|
||||
chardata = char.data
|
||||
if not isinstance(chardata, bytes):
|
||||
chardata = chardata.encode('utf-8')
|
||||
line += chardata
|
||||
if maxlen and len >= maxlen:
|
||||
break
|
||||
len += 1
|
||||
return line, hasdata
|
||||
|
||||
|
||||
class ConsoleHandler(object):
|
||||
_plugin_path = '/nodes/{0}/_console/session'
|
||||
_logtobuffer = True
|
||||
@@ -161,15 +138,15 @@ class ConsoleHandler(object):
|
||||
self.node = node
|
||||
self.connectstate = 'unconnected'
|
||||
self._isalive = True
|
||||
self.buffer = pyte.Screen(100, 31)
|
||||
self.termstream = pyte.ByteStream()
|
||||
self.termstream.attach(self.buffer)
|
||||
#self.buffer = pyte.Screen(100, 31)
|
||||
#self.termstream = pyte.ByteStream()
|
||||
#self.termstream.attach(self.buffer)
|
||||
self.livesessions = set([])
|
||||
self.utf8decoder = codecs.getincrementaldecoder('utf-8')()
|
||||
if self._logtobuffer:
|
||||
self.logger = log.Logger(node, console=True,
|
||||
tenant=configmanager.tenant)
|
||||
(text, termstate, timestamp) = (b'', 0, False)
|
||||
timestamp = False
|
||||
# when reading from log file, we will use wall clock
|
||||
# it should usually match walltime.
|
||||
self.lasttime = 0
|
||||
@@ -182,13 +159,7 @@ class ConsoleHandler(object):
|
||||
# guess
|
||||
self.lasttime = util.monotonic_time()
|
||||
self.clearbuffer()
|
||||
self.appmodedetected = False
|
||||
self.shiftin = None
|
||||
self.reconnect = None
|
||||
if termstate & 1:
|
||||
self.appmodedetected = True
|
||||
if termstate & 2:
|
||||
self.shiftin = b'0'
|
||||
self.users = {}
|
||||
self._attribwatcher = None
|
||||
self._console = None
|
||||
@@ -216,10 +187,7 @@ class ConsoleHandler(object):
|
||||
if not isinstance(data, bytes):
|
||||
data = data.encode('utf-8')
|
||||
try:
|
||||
self.termstream.feed(data)
|
||||
except StopIteration: # corrupt parser state, start over
|
||||
self.termstream = pyte.ByteStream()
|
||||
self.termstream.attach(self.buffer)
|
||||
send_output(self.node, data)
|
||||
except Exception:
|
||||
_tracelog.log(traceback.format_exc(), ltype=log.DataTypes.event,
|
||||
event=log.Events.stacktrace)
|
||||
@@ -536,19 +504,7 @@ class ConsoleHandler(object):
|
||||
return
|
||||
if not isinstance(data, bytes):
|
||||
data = data.encode('utf-8')
|
||||
if b'\x1b[?1l' in data: # request for ansi mode cursor keys
|
||||
self.appmodedetected = False
|
||||
if b'\x1b[?1h' in data: # remember the session wants the client to use
|
||||
# 'application mode' Thus far only observed on esxi
|
||||
self.appmodedetected = True
|
||||
if b'\x1b)0' in data:
|
||||
# console indicates it wants access to special drawing characters
|
||||
self.shiftin = b'0'
|
||||
eventdata = 0
|
||||
if self.appmodedetected:
|
||||
eventdata |= 1
|
||||
if self.shiftin is not None:
|
||||
eventdata |= 2
|
||||
# TODO: analyze buffer for registered events, examples:
|
||||
# panics
|
||||
# certificate signing request
|
||||
@@ -557,7 +513,7 @@ class ConsoleHandler(object):
|
||||
self.clearerror = False
|
||||
self.feedbuffer(b'\x1bc\x1b[2J\x1b[1;1H')
|
||||
self._send_rcpts(b'\x1bc\x1b[2J\x1b[1;1H')
|
||||
self._send_rcpts(_utf8_normalize(data, self.shiftin, self.utf8decoder))
|
||||
self._send_rcpts(_utf8_normalize(data, self.utf8decoder))
|
||||
self.log(data, eventdata=eventdata)
|
||||
self.lasttime = util.monotonic_time()
|
||||
self.feedbuffer(data)
|
||||
@@ -583,36 +539,7 @@ class ConsoleHandler(object):
|
||||
'connectstate': self.connectstate,
|
||||
'clientcount': len(self.livesessions),
|
||||
}
|
||||
retdata = b'\x1b[H\x1b[J' # clear screen
|
||||
pendingbl = b'' # pending blank lines
|
||||
maxlen = 0
|
||||
for line in self.buffer.display:
|
||||
line = line.rstrip()
|
||||
if len(line) > maxlen:
|
||||
maxlen = len(line)
|
||||
for line in range(self.buffer.lines):
|
||||
nline, notblank = pytechars2line(self.buffer.buffer[line], maxlen)
|
||||
if notblank:
|
||||
if pendingbl:
|
||||
retdata += pendingbl
|
||||
pendingbl = b''
|
||||
retdata += nline + b'\r\n'
|
||||
else:
|
||||
pendingbl += nline + b'\r\n'
|
||||
if len(retdata) > 6:
|
||||
retdata = retdata[:-2] # remove the last \r\n
|
||||
cursordata = '\x1b[{0};{1}H'.format(self.buffer.cursor.y + 1,
|
||||
self.buffer.cursor.x + 1)
|
||||
if not isinstance(cursordata, bytes):
|
||||
cursordata = cursordata.encode('utf-8')
|
||||
retdata += cursordata
|
||||
if self.shiftin is not None: # detected that terminal requested a
|
||||
# shiftin character set, relay that to the terminal that cannected
|
||||
retdata += b'\x1b)' + self.shiftin
|
||||
#if self.appmodedetected:
|
||||
# retdata += b'\x1b[?1h'
|
||||
#else:
|
||||
# retdata += b'\x1b[?1l'
|
||||
retdata = get_buffer_output(self.node)
|
||||
return retdata, connstate
|
||||
|
||||
def write(self, data):
|
||||
@@ -659,9 +586,20 @@ def _start_tenant_sessions(cfm):
|
||||
cfm.watch_nodecollection(_nodechange)
|
||||
|
||||
|
||||
def start_console_sessions():
|
||||
def initialize():
|
||||
global _tracelog
|
||||
global _bufferdaemon
|
||||
global _bufferlock
|
||||
_bufferlock = semaphore.Semaphore()
|
||||
_tracelog = log.Logger('trace')
|
||||
_bufferdaemon = subprocess.Popen(
|
||||
['/opt/confluent/bin/vtbufferd'], bufsize=0, stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE)
|
||||
fl = fcntl.fcntl(_bufferdaemon.stdout.fileno(), fcntl.F_GETFL)
|
||||
fcntl.fcntl(_bufferdaemon.stdout.fileno(),
|
||||
fcntl.F_SETFL, fl | os.O_NONBLOCK)
|
||||
|
||||
def start_console_sessions():
|
||||
configmodule.hook_new_configmanagers(_start_tenant_sessions)
|
||||
|
||||
|
||||
|
||||
@@ -57,9 +57,11 @@ except ImportError:
|
||||
# Only required for collective mode
|
||||
crypto = None
|
||||
import confluent.util as util
|
||||
import eventlet
|
||||
import eventlet.greenpool as greenpool
|
||||
import eventlet.green.ssl as ssl
|
||||
import eventlet.queue as queue
|
||||
import eventlet.semaphore as semaphore
|
||||
import itertools
|
||||
import msgpack
|
||||
import os
|
||||
@@ -745,6 +747,12 @@ def abbreviate_noderange(configmanager, inputdata, operation):
|
||||
return (msg.KeyValueData({'noderange': noderange.ReverseNodeRange(inputdata['nodes'], configmanager).noderange}),)
|
||||
|
||||
|
||||
def _keepalivefn(connection, xmitlock):
|
||||
while True:
|
||||
eventlet.sleep(30)
|
||||
with xmitlock:
|
||||
connection.sendall(b'\x00\x00\x00\x00\x00\x00\x00\x01\x00')
|
||||
|
||||
def handle_dispatch(connection, cert, dispatch, peername):
|
||||
cert = crypto.dump_certificate(crypto.FILETYPE_ASN1, cert)
|
||||
if not util.cert_matches(
|
||||
@@ -757,6 +765,8 @@ def handle_dispatch(connection, cert, dispatch, peername):
|
||||
# under 0x20 or so.
|
||||
connection.close()
|
||||
return
|
||||
xmitlock = semaphore.Semaphore()
|
||||
keepalive = eventlet.spawn(_keepalivefn, connection, xmitlock)
|
||||
dispatch = msgpack.unpackb(dispatch[2:], raw=False)
|
||||
configmanager = cfm.ConfigManager(dispatch['tenant'])
|
||||
nodes = dispatch['nodes']
|
||||
@@ -796,9 +806,12 @@ def handle_dispatch(connection, cert, dispatch, peername):
|
||||
configmanager=configmanager,
|
||||
inputdata=inputdata))
|
||||
for res in itertools.chain(*passvalues):
|
||||
_forward_rsp(connection, res)
|
||||
with xmitlock:
|
||||
_forward_rsp(connection, res)
|
||||
except Exception as res:
|
||||
_forward_rsp(connection, res)
|
||||
with xmitlock:
|
||||
_forward_rsp(connection, res)
|
||||
keepalive.kill()
|
||||
connection.sendall('\x00\x00\x00\x00\x00\x00\x00\x00')
|
||||
|
||||
|
||||
@@ -1104,6 +1117,8 @@ def dispatch_request(nodes, manager, element, configmanager, inputdata,
|
||||
a['name']))
|
||||
return
|
||||
rsp += nrsp
|
||||
if rsp == b'\x00':
|
||||
continue
|
||||
try:
|
||||
rsp = msg.msg_deserialize(rsp)
|
||||
except Exception:
|
||||
|
||||
@@ -88,7 +88,7 @@ class CredServer(object):
|
||||
client.close()
|
||||
return
|
||||
echotoken = util.stringify(client.recv(tlv[1]))
|
||||
cfgupdate = {nodename: {'crypted.selfapikey': {'hashvalue': echotoken}, 'deployment.apiarmed': ''}}
|
||||
cfgupdate = {nodename: {'crypted.selfapikey': {'hashvalue': echotoken}, 'deployment.sealedapikey': '', 'deployment.apiarmed': ''}}
|
||||
if apiarmed == 'continuous':
|
||||
del cfgupdate[nodename]['deployment.apiarmed']
|
||||
self.cfm.set_node_attributes(cfgupdate)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright 2016-2017 Lenovo
|
||||
# Copyright 2016-2021 Lenovo
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -252,6 +252,8 @@ def send_discovery_datum(info):
|
||||
yield msg.KeyValueData({'lowest_u': info['u']})
|
||||
if 'hostname' in info:
|
||||
yield msg.KeyValueData({'hostname': info['hostname']})
|
||||
if 'modelname' in info:
|
||||
yield msg.KeyValueData({'modelname': info['modelname']})
|
||||
|
||||
|
||||
def _info_matches(info, criteria):
|
||||
@@ -910,8 +912,11 @@ def get_nodename_from_chained_smms(cfg, handler, info):
|
||||
nodename = newnodename
|
||||
return nodename
|
||||
|
||||
def get_node_by_uuid(uuid):
|
||||
return nodes_by_uuid.get(uuid, None)
|
||||
def get_node_by_uuid_or_mac(uuidormac):
|
||||
node = pxe.macmap.get(uuidormac, None)
|
||||
if node is not None:
|
||||
return node
|
||||
return nodes_by_uuid.get(uuidormac, None)
|
||||
|
||||
def get_nodename_from_enclosures(cfg, info):
|
||||
nodename = None
|
||||
@@ -1303,7 +1308,7 @@ def start_detection():
|
||||
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)
|
||||
eventlet.spawn_n(ssdp.snoop, None, None, ssdp, get_node_by_uuid_or_mac)
|
||||
|
||||
def stop_autosense():
|
||||
for watcher in list(autosensors):
|
||||
@@ -1322,42 +1327,38 @@ known_pxe_uuids = {}
|
||||
def _map_unique_ids(nodes=None):
|
||||
global nodes_by_uuid
|
||||
global nodes_by_fprint
|
||||
global known_pxe_uuids
|
||||
# Map current known ids based on uuid and fingperprints for fast lookup
|
||||
cfg = cfm.ConfigManager(None)
|
||||
if nodes is None:
|
||||
nodes_by_uuid = {}
|
||||
nodes_by_fprint = {}
|
||||
known_pxe_uuids = {}
|
||||
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
|
||||
for uuid in list(nodes_by_uuid):
|
||||
node = nodes_by_uuid[uuid]
|
||||
if node in bigmap:
|
||||
uuid_by_nodes[node] = uuid
|
||||
for fprint in nodes_by_fprint:
|
||||
del nodes_by_uuid[uuid]
|
||||
for uuid in list(known_pxe_uuids):
|
||||
node = known_pxe_uuids[uuid]
|
||||
if node in bigmap:
|
||||
del known_pxe_uuids[uuid]
|
||||
for fprint in list(nodes_by_fprint):
|
||||
node = nodes_by_fprint[fprint]
|
||||
if node in bigmap:
|
||||
fprint_by_nodes[node] = fprint
|
||||
del nodes_by_fprint[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', '').lower()
|
||||
if uuid_is_valid(uuid):
|
||||
nodes_by_uuid[uuid] = node
|
||||
known_pxe_uuids[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__':
|
||||
|
||||
@@ -63,6 +63,15 @@ class NodeHandler(immhandler.NodeHandler):
|
||||
# This is not adequate for being satisfied
|
||||
return bool(info.get('attributes', {}))
|
||||
|
||||
def scan(self):
|
||||
c = webclient.SecureHTTPConnection(self.ipaddr, 443,
|
||||
verifycallback=self.validate_cert)
|
||||
i = c.grab_json_response('/api/providers/logoninfo')
|
||||
modelname = i.get('items', [{}])[0].get('machine_name', None)
|
||||
if modelname:
|
||||
self.info['modelname'] = modelname
|
||||
super(NodeHandler, self).scan()
|
||||
|
||||
def preconfig(self, possiblenode):
|
||||
self.tmpnodename = possiblenode
|
||||
ff = self.info.get('attributes', {}).get('enclosure-form-factor', '')
|
||||
|
||||
@@ -252,8 +252,9 @@ def proxydhcp():
|
||||
optidx = rq.index(b'\x63\x82\x53\x63') + 4
|
||||
except ValueError:
|
||||
continue
|
||||
hwlen = rq[2]
|
||||
opts, disco = opts_to_dict(rq, optidx, 3)
|
||||
disco['uuid']
|
||||
disco['hwaddr'] = ':'.join(['{0:02x}'.format(x) for x in rq[28:28+hwlen]])
|
||||
node = None
|
||||
if disco.get('hwaddr', None) in macmap:
|
||||
node = macmap[disco['hwaddr']]
|
||||
@@ -261,7 +262,7 @@ def proxydhcp():
|
||||
node = uuidmap[disco['uuid']]
|
||||
if not node:
|
||||
continue
|
||||
hwlen = rq[2]
|
||||
|
||||
myipn = myipbypeer.get(rqv[28:28+hwlen].tobytes(), None)
|
||||
if not myipn:
|
||||
continue
|
||||
@@ -275,6 +276,13 @@ def proxydhcp():
|
||||
bootfile = b'confluent/x86_64/ipxe.efi'
|
||||
elif disco['arch'] == 'bios-x86':
|
||||
bootfile = b'confluent/x86_64/ipxe.kkpxe'
|
||||
if len(bootfile) > 127:
|
||||
log.log(
|
||||
{'info': 'Boot offer cannot be made to {0} as the '
|
||||
'profile name "{1}" is {2} characters longer than is supported '
|
||||
'for this boot method.'.format(
|
||||
node, profile, len(bootfile) - 127)})
|
||||
continue
|
||||
rpv[:240] = rqv[:240].tobytes()
|
||||
rpv[0:1] = b'\x02'
|
||||
rpv[108:108 + len(bootfile)] = bootfile
|
||||
@@ -484,6 +492,13 @@ def check_reply(node, info, packet, sock, cfg, reqview):
|
||||
)
|
||||
if not isinstance(bootfile, bytes):
|
||||
bootfile = bootfile.encode('utf8')
|
||||
if len(bootfile) > 127:
|
||||
log.log(
|
||||
{'info': 'Boot offer cannot be made to {0} as the '
|
||||
'profile name "{1}" is {2} characters longer than is supported '
|
||||
'for this boot method.'.format(
|
||||
node, profile, len(bootfile) - 127)})
|
||||
return
|
||||
repview[108:108 + len(bootfile)] = bootfile
|
||||
repview[20:24] = myipn
|
||||
gateway = None
|
||||
@@ -494,6 +509,7 @@ def check_reply(node, info, packet, sock, cfg, reqview):
|
||||
log.log({'error': nicerr})
|
||||
if niccfg.get('ipv4_broken', False):
|
||||
# Received a request over a nic with no ipv4 configured, ignore it
|
||||
log.log({'error': 'Skipping boot reply to {0} due to no viable IPv4 configuration on deployment system'.format(node)})
|
||||
return
|
||||
clipn = None
|
||||
if niccfg['ipv4_address'] and niccfg['ipv4_method'] != 'firmwaredhcp':
|
||||
@@ -557,6 +573,16 @@ def check_reply(node, info, packet, sock, cfg, reqview):
|
||||
staticassigns[fulladdr] = (clipn, repview[:replen + 28].tobytes())
|
||||
elif fulladdr in staticassigns:
|
||||
del staticassigns[fulladdr]
|
||||
if httpboot:
|
||||
boottype = 'HTTP'
|
||||
else:
|
||||
boottype = 'PXE'
|
||||
if clipn:
|
||||
ipinfo = 'with static address {0}'.format(niccfg['ipv4_address'])
|
||||
else:
|
||||
ipinfo = 'without address'
|
||||
log.log({
|
||||
'info': 'Offering {0} boot {1} to {2}'.format(boottype, ipinfo, node)})
|
||||
send_raw_packet(repview, replen + 28, reqview, info)
|
||||
|
||||
def send_raw_packet(repview, replen, reqview, info):
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user