mirror of
https://github.com/xcat2/confluent.git
synced 2026-08-25 08:06:46 +00:00
8a3fce85c0
Every one of these raises NameError if its code path is reached: - nodeapply: run_automation accumulated into an exitcode that only existed in run(), so any automation error crashed instead of being reported. It now keeps and returns its own, tracked separately from the exit code of the ssh commands: the early exit after the spawn loop tests that one, and folding automation failures into it would exit with children already running and their pipes abandoned. Both are reported at the real exits. - nodeconsole: redraw() reads firstnodename, which was local to do_screenshot(); promote it to a module global like the other drawing state. - nodedeploy: the redeploy path appended to a lockednodes list that did not exist yet. The block that follows re-reads the same lock state and acts on it, so drop the dead duplicate. - samples/nodeattrib_from_switch.py, misc/filterpasswd: missing import sys. - xcc3: fixuuid was never imported. xcc imports xcc3, so take a local copy the way the smm handler does instead of creating an import cycle. - httpapi: the async session call still passed the WSGI-era env and an extra argument to handle_async(), which has taken only querydict since the aiohttp port. Calling it correctly exposed that handle_async() registers an AsyncSession before raising on the discontinued long poll path, so every request to it would leak a session that is never reaped. It now only creates one when there is a websocket handler to yield it to. - messages: the InputFirmwareUpdate.filename property checked self.filebynode[node] with no node in scope. __init__ already validates every expanded path and nodefile() rechecks per node, so drop the checks. - pam: drop the python2 branches referencing unicode and raw_input. The server has been python3 only since the asyncio port. - cooltera: the sensor-name listing referenced a nonexistent sensors dict. The available sensors depend on the model, which is only known after reading the device, so list them from the same status data the readings use. - deltapdu, eatonpdu, geist: the not-implemented response in update() used node outside the loop, unlike retrieve() in the same files and unlike raritan/enlogic. - confluentdbgcli: stray self. on a module-level socket connect.
235 lines
11 KiB
Python
Executable File
235 lines
11 KiB
Python
Executable File
#!/usr/bin/python3
|
|
|
|
|
|
import argparse
|
|
import datetime
|
|
import os
|
|
import sys
|
|
path = os.path.dirname(os.path.realpath(__file__))
|
|
path = os.path.realpath(os.path.join(path, '..', 'lib', 'python'))
|
|
if path.startswith('/opt'):
|
|
sys.path.append(path)
|
|
|
|
import confluent.client as client
|
|
import confluent.sortutil as sortutil
|
|
|
|
def cleararm(nr, cli):
|
|
nodes = set([])
|
|
for rsp in cli.read('/noderange/{0}/attributes/current'.format(nr)):
|
|
for node in rsp.get('databynode', {}):
|
|
nodeinfo = rsp['databynode'][node]
|
|
for attr in nodeinfo:
|
|
if attr == 'deployment.apiarmed':
|
|
curr = nodeinfo[attr].get('value', '')
|
|
if curr == 'continuous':
|
|
nodes.add(node)
|
|
noderange = nr
|
|
if nodes:
|
|
noderange += ',-({0})'.format(','.join(nodes))
|
|
for rsp in cli.update('/noderange/{0}/attributes/current'.format(noderange),
|
|
{'deployment.apiarmed': ''}):
|
|
pass
|
|
|
|
|
|
def armonce(nr, cli):
|
|
nodes = set([])
|
|
for rsp in cli.read('/noderange/{0}/attributes/current'.format(nr)):
|
|
for node in rsp.get('databynode', {}):
|
|
nodeinfo = rsp['databynode'][node]
|
|
for attr in nodeinfo:
|
|
if attr == 'deployment.apiarmed':
|
|
curr = nodeinfo[attr].get('value', '')
|
|
if curr == 'continuous':
|
|
nodes.add(node)
|
|
noderange = nr
|
|
if nodes:
|
|
noderange += ',-({0})'.format(','.join(nodes))
|
|
for rsp in cli.update('/noderange/{0}/attributes/current'.format(noderange),
|
|
{'deployment.apiarmed': 'once'}):
|
|
pass
|
|
|
|
|
|
def setpending(nr, profile, profilebynodes, cli):
|
|
if profilebynodes:
|
|
for node in sortutil.natural_sort(profilebynodes):
|
|
prof = profilebynodes[node]
|
|
args = {'deployment.pendingprofile': prof, 'deployment.state': '', 'deployment.state_detail': '', 'deployment.state_last_updated': ''}
|
|
if not prof.startswith('genesis-'):
|
|
args['deployment.stagedprofile'] = ''
|
|
args['deployment.profile'] = ''
|
|
for rsp in cli.update('/nodes/{0}/attributes/current'.format(node),
|
|
args):
|
|
pass
|
|
return
|
|
args = {'deployment.pendingprofile': profile, 'deployment.state': '', 'deployment.state_detail': '', 'deployment.state_last_updated': ''}
|
|
if not profile.startswith('genesis-'):
|
|
args['deployment.stagedprofile'] = ''
|
|
args['deployment.profile'] = ''
|
|
for rsp in cli.update('/noderange/{0}/attributes/current'.format(nr),
|
|
args):
|
|
pass
|
|
|
|
|
|
def clearpending(nr, cli):
|
|
for rsp in cli.update('/noderange/{0}/attributes/current'.format(nr),
|
|
{'deployment.pendingprofile': ''}):
|
|
pass
|
|
|
|
def main(args):
|
|
ap = argparse.ArgumentParser(description='Deploy OS to nodes')
|
|
ap.add_argument('-c', '--clear', help='Clear any pending deployment action', action='store_true')
|
|
ap.add_argument('-n', '--network', help='Initiate deployment over PXE/HTTP', action='store_true')
|
|
ap.add_argument('-b', '--bootmethod', help='Specify network boot method (e.g., network, http)', default='network')
|
|
ap.add_argument('-p', '--prepareonly', help='Prepare only, skip any interaction with a BMC associated with this deployment action', action='store_true')
|
|
ap.add_argument('-m', '--maxnodes', help='Specify a maximum nodes to be deployed')
|
|
ap.add_argument('-r', '--redeploy', help='Redeploy nodes with the current or pending profile', action='store_true')
|
|
ap.add_argument('noderange', help='Set of nodes to deploy')
|
|
ap.add_argument('profile', nargs='?', help='Profile name to deploy')
|
|
args, extra = ap.parse_known_args(args)
|
|
if args.profile is None and len(extra) == 1:
|
|
args.profile = extra[0]
|
|
extra = extra[1:]
|
|
if args.profile and not args.network:
|
|
sys.stderr.write('-n is a required argument currently to perform an install, optionally with -p\n')
|
|
return 1
|
|
if not args.profile and args.network and not args.redeploy:
|
|
sys.stderr.write('Both noderange and a profile name are required arguments to request a network deployment\n')
|
|
return 1
|
|
if args.clear and args.profile:
|
|
sys.stderr.write(
|
|
'The -c/--clear option should not be used with a profile, '
|
|
'it is a request to not deploy any profile, and will clear '
|
|
'whatever the current profile is without being specified\n')
|
|
return 1
|
|
if extra:
|
|
sys.stderr.write('Unrecognized arguments: ' + repr(extra) + '\n')
|
|
c = client.Command()
|
|
c.stop_if_noderange_over(args.noderange, args.maxnodes)
|
|
errnodes = set([])
|
|
for rsp in c.read('/noderange/{0}/nodes/'.format(args.noderange)):
|
|
if 'error' in rsp:
|
|
sys.stderr.write(rsp['error'] + '\n')
|
|
sys.exit(1)
|
|
profilebynode = {}
|
|
if args.clear:
|
|
cleararm(args.noderange, c)
|
|
clearpending(args.noderange, c)
|
|
elif args.redeploy:
|
|
hadpending = {}
|
|
for rsp in c.read('/noderange/{0}/attributes/current'.format(args.noderange)):
|
|
for node in rsp.get('databynode', {}):
|
|
nodeinfo = rsp['databynode'][node]
|
|
for attr in nodeinfo:
|
|
if attr == 'deployment.pendingprofile':
|
|
curr = nodeinfo[attr].get('value', '')
|
|
if curr:
|
|
hadpending[node] = True
|
|
profilebynode[node] = curr
|
|
if attr == 'deployment.stagedprofile':
|
|
curr = nodeinfo[attr].get('value', '')
|
|
if curr and node not in hadpending:
|
|
profilebynode[node] = curr
|
|
if attr == 'deployment.profile':
|
|
curr = nodeinfo[attr].get('value', '')
|
|
if curr and node not in profilebynode:
|
|
profilebynode[node] = curr
|
|
if args.profile and profilebynode:
|
|
sys.stderr.write('The -r/--redeploy option cannot be used with a profile, it redeploys the current or pending profile\n')
|
|
return 1
|
|
if args.profile or profilebynode:
|
|
lockednodes = []
|
|
for lockinfo in c.read('/noderange/{0}/deployment/lock'.format(args.noderange)):
|
|
for node in lockinfo.get('databynode', {}):
|
|
lockstate = lockinfo['databynode'][node]['lock']['value']
|
|
if lockstate == 'locked':
|
|
lockednodes.append(node)
|
|
if lockednodes:
|
|
sys.stderr.write('Requested noderange has nodes with locked deployment: ' + ','.join(lockednodes))
|
|
sys.stderr.write('\n')
|
|
sys.exit(1)
|
|
if args.profile:
|
|
profnames = []
|
|
for prof in c.read('/deployment/profiles/'):
|
|
profname = prof.get('item', {}).get('href', None)
|
|
if profname:
|
|
profname = profname.replace('/', '')
|
|
profnames.append(profname)
|
|
if profname == args.profile:
|
|
break
|
|
else:
|
|
sys.stderr.write('The specified profile "{}" is not an available profile\n'.format(args.profile))
|
|
if profnames:
|
|
sys.stderr.write('The following profiles are available:\n')
|
|
for profname in profnames:
|
|
sys.stderr.write(' ' + profname + '\n')
|
|
else:
|
|
sys.stderr.write('No deployment profiles available, try osdeploy import or imgutil capture\n')
|
|
sys.exit(1)
|
|
armonce(args.noderange, c)
|
|
setpending(args.noderange, args.profile, profilebynode, c)
|
|
else:
|
|
databynode = {}
|
|
for r in c.read('/noderange/{0}/attributes/current'.format(args.noderange)):
|
|
dbn = r.get('databynode', {})
|
|
for node in dbn:
|
|
if node not in databynode:
|
|
databynode[node] = {}
|
|
for attr in dbn[node]:
|
|
if attr in ('deployment.pendingprofile', 'deployment.apiarmed', 'deployment.stagedprofile', 'deployment.profile', 'deployment.state', 'deployment.state_detail', 'deployment.state_last_updated'):
|
|
databynode[node][attr] = dbn[node][attr].get('value', '')
|
|
for node in sortutil.natural_sort(databynode):
|
|
profile = databynode[node].get('deployment.pendingprofile', '')
|
|
if profile:
|
|
profile = 'pending: {}'.format(profile)
|
|
else:
|
|
profile = databynode[node].get('deployment.stagedprofile', '')
|
|
if profile:
|
|
profile = 'staged: {}'.format(profile)
|
|
else:
|
|
profile = databynode[node].get('deployment.profile', '')
|
|
if profile:
|
|
profile = 'completed: {}'.format(profile)
|
|
else:
|
|
profile= 'No profile pending or applied'
|
|
armed = databynode[node].get('deployment.apiarmed', '')
|
|
if armed in ('once', 'continuous'):
|
|
armed = ' (node authentication armed)'
|
|
else:
|
|
armed = ''
|
|
stateinfo = ''
|
|
deploymentstate = databynode[node].get('deployment.state', '')
|
|
if deploymentstate:
|
|
deploymentdate = databynode[node].get('deployment.state_last_updated', '')
|
|
if deploymentdate:
|
|
try:
|
|
deploymentdate = datetime.datetime.fromisoformat(deploymentdate).strftime('%m/%d/%Y %H:%M:%S')
|
|
except ValueError:
|
|
pass
|
|
statedetails = databynode[node].get('deployment.state_detail', '')
|
|
if statedetails:
|
|
stateinfo = '{}: {}'.format(deploymentstate, statedetails)
|
|
else:
|
|
stateinfo = deploymentstate
|
|
if deploymentdate:
|
|
stateinfo += ' - {}'.format(deploymentdate)
|
|
if stateinfo:
|
|
print('{0}: {1} ({2})'.format(node, profile, stateinfo))
|
|
else:
|
|
print('{0}: {1}{2}'.format(node, profile, armed))
|
|
sys.exit(0)
|
|
if not args.clear and args.network and not args.prepareonly:
|
|
rc = c.simple_noderange_command(args.noderange, '/boot/nextdevice', args.bootmethod,
|
|
bootmode='uefi',
|
|
persistent=False,
|
|
errnodes=errnodes)
|
|
if errnodes:
|
|
sys.stderr.write(
|
|
'Unable to set boot device for following nodes: {0}\n'.format(
|
|
','.join(errnodes)))
|
|
return 1
|
|
rc |= c.simple_noderange_command(args.noderange, '/power/state', 'boot')
|
|
return 0
|
|
|
|
if __name__ == '__main__':
|
|
sys.exit(main(sys.argv[1:]))
|