From 71f5ce2b29c2408e39542c985f35a7f51c9da7f7 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Thu, 1 May 2025 09:25:05 -0400 Subject: [PATCH] Add deployment lock mechanism This allows users to opt into disabling setting further profile changes. Nodes may be 'unlocked' (normal), 'autolock' (will lock on next completion), or 'locked' (unable to change the pending OS profile) --- confluent_client/bin/nodedeploy | 10 ++++++++ .../confluent/config/attributes.py | 7 ++++++ confluent_server/confluent/core.py | 3 +++ confluent_server/confluent/messages.py | 14 +++++++++++ .../plugins/configuration/attributes.py | 24 +++++++++++++++++++ confluent_server/confluent/selfservice.py | 4 ++++ 6 files changed, 62 insertions(+) diff --git a/confluent_client/bin/nodedeploy b/confluent_client/bin/nodedeploy index 15e78f37..1e172fea 100755 --- a/confluent_client/bin/nodedeploy +++ b/confluent_client/bin/nodedeploy @@ -117,6 +117,16 @@ def main(args): else: sys.stderr.write('No deployment profiles available, try osdeploy import or imgutil capture\n') sys.exit(1) + 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) armonce(args.noderange, c) setpending(args.noderange, args.profile, c) else: diff --git a/confluent_server/confluent/config/attributes.py b/confluent_server/confluent/config/attributes.py index 248063f2..4f6531bd 100644 --- a/confluent_server/confluent/config/attributes.py +++ b/confluent_server/confluent/config/attributes.py @@ -215,6 +215,13 @@ node = { 'Using this requires that collective members be ' 'defined as nodes for noderange expansion') }, + 'deployment.lock': { + 'description': ('Indicates whether deployment actions should be impeded. ' + 'If locked, it indicates that a pending profile should not be applied. ' + 'If "autolock", then locked will be set when current pending deployment completes. ' + ), + 'validlist': ('autolock', 'locked') + }, 'deployment.pendingprofile': { 'description': ('An OS profile that is pending deployment. This indicates to ' 'the network boot subsystem what should be offered when a potential ' diff --git a/confluent_server/confluent/core.py b/confluent_server/confluent/core.py index e25e82d2..0e754b9f 100644 --- a/confluent_server/confluent/core.py +++ b/confluent_server/confluent/core.py @@ -453,6 +453,9 @@ def _init_core(): 'default': 'ipmi', }), 'deployment': { + 'lock': PluginRoute({ + 'handler': 'attributes' + }), 'ident_image': PluginRoute({ 'handler': 'identimage' }) diff --git a/confluent_server/confluent/messages.py b/confluent_server/confluent/messages.py index 6dbe031f..04ca43f7 100644 --- a/confluent_server/confluent/messages.py +++ b/confluent_server/confluent/messages.py @@ -574,6 +574,8 @@ def get_input_message(path, operation, inputdata, nodes=None, multinode=False, elif '/'.join(path).startswith( 'configuration/management_controller/licenses') and inputdata: return InputLicense(path, nodes, inputdata, configmanager) + elif path == ['deployment', 'lock'] and inputdata: + return InputDeploymentLock(path, nodes, inputdata) elif path == ['deployment', 'ident_image']: return InputIdentImage(path, nodes, inputdata) elif path == ['console', 'ikvm']: @@ -957,6 +959,18 @@ class InputIdentImage(ConfluentInputMessage): keyname = 'ident_image' valid_values = ['create'] +class InputDeploymentLock(ConfluentInputMessage): + keyname = 'lock' + valid_values = ['autolock', 'unlocked', 'locked'] + +class DeploymentLock(ConfluentChoiceMessage): + valid_values = set([ + 'autolock', + 'locked', + 'unlocked', + ]) + keyname = 'lock' + class InputIkvmParams(ConfluentInputMessage): keyname = 'method' valid_values = ['unix', 'wss', 'url'] diff --git a/confluent_server/confluent/plugins/configuration/attributes.py b/confluent_server/confluent/plugins/configuration/attributes.py index 14607af5..434d6c50 100644 --- a/confluent_server/confluent/plugins/configuration/attributes.py +++ b/confluent_server/confluent/plugins/configuration/attributes.py @@ -109,6 +109,13 @@ def retrieve_nodegroup(nodegroup, element, configmanager, inputdata, clearwarnby def retrieve_nodes(nodes, element, configmanager, inputdata, clearwarnbynode): attributes = configmanager.get_node_attributes(nodes) + if element[-1] == 'lock': + for node in nodes: + lockstate = attributes.get(node, {}).get('deployment.lock', {}).get('value', None) + if lockstate not in ('locked', 'autolock'): + lockstate = 'unlocked' + yield msg.DeploymentLock(node, lockstate) + return if element[-1] == 'all': for node in util.natural_sort(nodes): if clearwarnbynode and node in clearwarnbynode: @@ -247,12 +254,20 @@ def yield_rename_resources(namemap, isnode): else: yield msg.RenamedResource(node, namemap[node]) +def update_locks(nodes, configmanager, inputdata): + for node in nodes: + updatestate = inputdata.inputbynode[node] + configmanager.set_node_attributes({node: {'deployment.lock': updatestate}}) + yield msg.DeploymentLock(node, updatestate) + def update_nodes(nodes, element, configmanager, inputdata): updatedict = {} if not nodes: raise exc.InvalidArgumentException( 'No action to take, noderange is empty (if trying to define ' 'group attributes, use nodegroupattrib)') + if element[-1] == 'lock': + return update_locks(nodes, configmanager, inputdata) if element[-1] == 'check': for node in nodes: check = inputdata.get_attributes(node, allattributes.node) @@ -273,6 +288,15 @@ def update_nodes(nodes, element, configmanager, inputdata): configmanager.rename_nodes(namemap) return yield_rename_resources(namemap, isnode=True) clearwarnbynode = {} + for node in nodes: + updatenode = inputdata.get_attributes(node, allattributes.node) + if updatenode and 'deployment.lock' in updatenode: + raise exc.InvalidArgumentException('Deployment lock must be manipulated by {node}/deployment/lock api') + if updatenode and ('deployment.pendingprofile' in updatenode or 'deployment.apiarmed' in updatenode): + lockcheck = configmanager.get_node_attributes(node, 'deployment.lock') + lockstate = lockcheck.get(node, {}).get('deployment.lock', {}).get('value', None) + if lockstate == 'locked': + raise exc.InvalidArgumentException('Request to set deployment for a node that has locked deployment') for node in nodes: updatenode = inputdata.get_attributes(node, allattributes.node) clearattribs = [] diff --git a/confluent_server/confluent/selfservice.py b/confluent_server/confluent/selfservice.py index de8eb832..6df8ff17 100644 --- a/confluent_server/confluent/selfservice.py +++ b/confluent_server/confluent/selfservice.py @@ -490,6 +490,10 @@ def handle_request(env, start_response): updates['deployment.pendingprofile'] = {'value': ''} if targattr == 'deployment.profile': updates['deployment.stagedprofile'] = {'value': ''} + dls = cfg.get_node_attributes(nodename, 'deployment.lock') + dls = dls.get(nodename, {}).get('deployment.lock', {}).get('value', None) + if dls == 'autolock': + updates['deployment.lock'] = 'locked' currprof = currattr.get(targattr, {}).get('value', '') if currprof != pending: updates[targattr] = {'value': pending}