2
0
mirror of https://github.com/xcat2/confluent.git synced 2026-06-18 09:30:50 +00:00

Implement ability for user to kick off confluent ansible runs

Add nodeapply -A and associated API.

This permits orchestrating plays without touching the nodes directly by the user.
This commit is contained in:
Jarrod Johnson
2026-03-06 16:24:26 -05:00
parent 69beaad3c9
commit e185f2224f
5 changed files with 116 additions and 22 deletions
+45 -7
View File
@@ -36,6 +36,36 @@ import confluent.client as client
import confluent.sortutil as sortutil
devnull = None
def run_automation(noderange, category, c):
automationbynode = {}
for res in c.update('/noderange/{0}/deployment/remote_config/run'.format(noderange), {
'category': category,
}):
if 'error' in res:
sys.stderr.write(res['error'] + '\n')
exitcode |= res.get('errorcode', 1)
if 'created' in res:
nodename = res['created'].split('/')[2]
automationbynode[nodename] = res['created']
while automationbynode:
for node in list(automationbynode):
for res in c.read(automationbynode[node]):
if 'error' in res:
sys.stderr.write(res['error'] + '\n')
exitcode |= res.get('errorcode', 1)
for result in res.get('results', []):
sys.stdout.write('{0}: Task [{1}] {2}\n'.format(
node, result['task_name'], result['state']))
for warning in result.get('warnings', []):
sys.stderr.write('{0}: [WARNING] {1}\n'.format(node, warning))
if 'errorinfo' in result:
for errorline in result['errorinfo'].splitlines():
sys.stderr.write('{0}: [ERROR] {1}\n'.format(node, errorline))
if res.get('complete', False):
del automationbynode[node]
sys.stdout.write('{0}: Automation complete\n'.format(node))
def run():
global devnull
devnull = open(os.devnull, 'rb')
@@ -51,6 +81,8 @@ def run():
help='Run the syncfiles associated with the currently completed OS profile on the noderange')
argparser.add_option('-P', '--scripts',
help='Re-run specified scripts, with full path under scripts, e.g. post.d/first,firstboot.d/second')
argparser.add_option('-A', '--automation',
help='Run the automation scripts associated with the current OS profile on the noderange, specifying category (onboot.d/firstboot.d/post.d)')
argparser.add_option('-m', '--maxnodes', type='int',
help='Specify a maximum number of '
'nodes to run remote ssh command to, '
@@ -74,16 +106,13 @@ def run():
exitcode = 0
c.stop_if_noderange_over(args[0], options.maxnodes)
if options.automation:
run_automation(args[0], options.automation, c)
nodemap = {}
cmdparms = []
nodes = []
for res in c.read('/noderange/{0}/nodes/'.format(args[0])):
if 'error' in res:
sys.stderr.write(res['error'] + '\n')
exitcode |= res.get('errorcode', 1)
break
node = res['item']['href'][:-1]
nodes.append(node)
cmdstorun = []
if options.security:
@@ -94,8 +123,17 @@ def run():
for script in options.scripts.split(','):
cmdstorun.append(['run_remote', script])
if not cmdstorun:
if options.automation:
sys.exit(0)
argparser.print_help()
sys.exit(1)
for res in c.read('/noderange/{0}/nodes/'.format(args[0])):
if 'error' in res:
sys.stderr.write(res['error'] + '\n')
exitcode |= res.get('errorcode', 1)
break
node = res['item']['href'][:-1]
nodes.append(node)
idxbynode = {}
cmdvbase = ['bash', '/etc/confluent/functions']
for sshnode in nodes:
+10 -2
View File
@@ -76,7 +76,7 @@ import shutil
vinz = None
pluginmap = {}
dispatch_plugins = (b'ipmi', u'ipmi', b'redfish', u'redfish', b'tsmsol', u'tsmsol', b'geist', u'geist', b'deltapdu', u'deltapdu', b'eatonpdu', u'eatonpdu', b'affluent', u'affluent', b'cnos', u'cnos', b'enos', u'enos')
dispatch_plugins = (b'remoteconfig', b'ipmi', u'ipmi', b'redfish', u'redfish', b'tsmsol', u'tsmsol', b'geist', u'geist', b'deltapdu', u'deltapdu', b'eatonpdu', u'eatonpdu', b'affluent', u'affluent', b'cnos', u'cnos', b'enos', u'enos')
PluginCollection = plugin.PluginCollection
@@ -476,7 +476,15 @@ def _init_core():
}),
'ident_image': PluginRoute({
'handler': 'identimage'
})
}),
'remote_config': {
'run': PluginRoute({
'handler': 'remoteconfig'
}),
'active': PluginCollection({
'handler': 'remoteconfig'
}),
},
},
'events': {
'hardware': {
+6
View File
@@ -580,6 +580,8 @@ def get_input_message(path, operation, inputdata, nodes=None, multinode=False,
return InputLicense(path, nodes, inputdata, configmanager)
elif path == ['deployment', 'lock'] and inputdata:
return InputDeploymentLock(path, nodes, inputdata)
elif path == ['deployment', 'remote_config', 'run'] and inputdata:
return InputRemoteConfig(path, nodes, inputdata)
elif path == ['deployment', 'ident_image']:
return InputIdentImage(path, nodes, inputdata)
elif path == ['console', 'ikvm']:
@@ -994,6 +996,10 @@ class InputDeploymentLock(ConfluentInputMessage):
keyname = 'lock'
valid_values = ['autolock', 'unlocked', 'locked']
class InputRemoteConfig(ConfluentInputMessage):
keyname = 'category'
valid_values = ['post.d', 'firstboot.d', 'onboot.d']
class DeploymentLock(ConfluentChoiceMessage):
valid_values = set([
'autolock',
@@ -0,0 +1,38 @@
import confluent.selfservice as selfservice
import confluent.messages as msg
import confluent.runansible as runansible
_user_initiated_runs = {}
def update(nodes, element, configmanager, inputdata):
if element[-1] != 'run':
raise ValueError('Invalid element for remoteconfig plugin')
for node in nodes:
category = inputdata.inputbynode[node]['category']
playlist = selfservice.list_ansible_scripts(configmanager, node, category)
if playlist:
_user_initiated_runs[node] = True
runansible.run_playbooks(playlist, [node])
yield msg.CreatedResource(
'/nodes/{0}/deployment/remote_config/active/{0}'.format(node))
else:
yield msg.ConfluentNodeError('No remote configuration for category "{0}"', node)
def retrieve(nodes, element, configmanager, inputdata):
for node in nodes:
if element[-1] == 'active':
rst = runansible.running_status.get(node, None)
if not rst:
return
yield msg.ChildCollection(node)
elif element[-2] == 'active' and element[-1] == node:
rst = runansible.running_status.get(node, None)
if not rst:
return
playstatus = rst.dump_dict()
if playstatus['complete'] and _user_initiated_runs.get(node, False):
del runansible.running_status[node]
del _user_initiated_runs[node]
yield msg.KeyValueData(playstatus, node)
+17 -13
View File
@@ -519,19 +519,7 @@ def handle_request(env, start_response):
yield ''
elif env['PATH_INFO'].startswith('/self/remoteconfig/') and 'POST' == operation:
scriptcat = env['PATH_INFO'].replace('/self/remoteconfig/', '')
playlist = []
for privacy in ('public', 'private'):
slist, profile = get_scriptlist(
scriptcat, cfg, nodename,
'/var/lib/confluent/{0}/os/{{0}}/ansible/{{1}}'.format(privacy))
dirname = '/var/lib/confluent/{2}/os/{0}/ansible/{1}/'.format(
profile, scriptcat, privacy)
if not os.path.isdir(dirname):
dirname = '/var/lib/confluent/{2}/os/{0}/ansible/{1}.d/'.format(
profile, scriptcat, privacy)
for filename in slist:
if filename.endswith('.yaml') or filename.endswith('.yml'):
playlist.append(os.path.join(dirname, filename))
playlist = list_ansible_scripts(cfg, nodename, scriptcat)
if playlist:
runansible.run_playbooks(playlist, [nodename])
start_response('202 Queued', ())
@@ -603,6 +591,22 @@ def handle_request(env, start_response):
start_response('404 Not Found', ())
yield 'Not found'
def list_ansible_scripts(cfg, nodename, scriptcat):
playlist = []
for privacy in ('public', 'private'):
slist, profile = get_scriptlist(
scriptcat, cfg, nodename,
'/var/lib/confluent/{0}/os/{{0}}/ansible/{{1}}'.format(privacy))
dirname = '/var/lib/confluent/{2}/os/{0}/ansible/{1}/'.format(
profile, scriptcat, privacy)
if not os.path.isdir(dirname):
dirname = '/var/lib/confluent/{2}/os/{0}/ansible/{1}.d/'.format(
profile, scriptcat, privacy)
for filename in slist:
if filename.endswith('.yaml') or filename.endswith('.yml'):
playlist.append(os.path.join(dirname, filename))
return playlist
def get_scriptlist(scriptcat, cfg, nodename, pathtemplate):
if '..' in scriptcat:
return None, None