1
0
Fork 0
mirror of https://github.com/ansible-collections/community.general.git synced 2024-09-14 20:13:21 +02:00

lxd_container: add check- and diff-mode support (#5866)

* lxd_container module: Automate CONFIG_PARAM handling.

Signed-off-by: InsanePrawn <insane.prawny@gmail.com>

* lxd_container: check- and diff mode

Signed-off-by: InsanePrawn <insane.prawny@gmail.com>

* Make JSON lookups safer and fix crashes in check mode when instance is absent

* lxd_profile: fix docstring typos

* lxd_container: simplify _needs_to_change_instance_config()

* lxd_container: add docstring for check- and diff-mode and changelog fragment

* style fixes

* lxd_container: fix typo in actions: "unfreez" lacks an "e"

---------

Signed-off-by: InsanePrawn <insane.prawny@gmail.com>
This commit is contained in:
Prawn 2023-02-22 14:41:46 +01:00 committed by GitHub
parent 867aee606e
commit 7e3c73ceb2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 87 additions and 64 deletions

View file

@ -0,0 +1,2 @@
minor_changes:
- lxd_container - add diff and check mode (https://github.com/ansible-collections/community.general/pull/5866).

View file

@ -16,6 +16,15 @@ short_description: Manage LXD instances
description:
- Management of LXD containers and virtual machines.
author: "Hiroaki Nakamura (@hnakamur)"
extends_documentation_fragment:
- community.general.attributes
attributes:
check_mode:
support: full
version_added: 6.4.0
diff_mode:
support: full
version_added: 6.4.0
options:
name:
description:
@ -396,6 +405,7 @@ actions:
type: list
sample: ["create", "start"]
'''
import copy
import datetime
import os
import time
@ -411,7 +421,7 @@ LXD_ANSIBLE_STATES = {
'stopped': '_stopped',
'restarted': '_restarted',
'absent': '_destroyed',
'frozen': '_frozen'
'frozen': '_frozen',
}
# ANSIBLE_LXD_STATES is a map of states of lxd containers to the Ansible
@ -430,6 +440,10 @@ CONFIG_PARAMS = [
'architecture', 'config', 'devices', 'ephemeral', 'profiles', 'source'
]
# CONFIG_CREATION_PARAMS is a list of attribute names that are only applied
# on instance creation.
CONFIG_CREATION_PARAMS = ['source']
class LXDContainerManagement(object):
def __init__(self, module):
@ -488,6 +502,9 @@ class LXDContainerManagement(object):
self.module.fail_json(msg=e.msg)
self.trust_password = self.module.params.get('trust_password', None)
self.actions = []
self.diff = {'before': {}, 'after': {}}
self.old_instance_json = {}
self.old_sections = {}
def _build_config(self):
self.config = {}
@ -521,7 +538,8 @@ class LXDContainerManagement(object):
body_json = {'action': action, 'timeout': self.timeout}
if force_stop:
body_json['force'] = True
return self.client.do('PUT', url, body_json=body_json)
if not self.module.check_mode:
return self.client.do('PUT', url, body_json=body_json)
def _create_instance(self):
url = self.api_endpoint
@ -534,7 +552,8 @@ class LXDContainerManagement(object):
url = '{0}?{1}'.format(url, urlencode(url_params))
config = self.config.copy()
config['name'] = self.name
self.client.do('POST', url, config, wait_for_container=self.wait_for_container)
if not self.module.check_mode:
self.client.do('POST', url, config, wait_for_container=self.wait_for_container)
self.actions.append('create')
def _start_instance(self):
@ -553,7 +572,8 @@ class LXDContainerManagement(object):
url = '{0}/{1}'.format(self.api_endpoint, self.name)
if self.project:
url = '{0}?{1}'.format(url, urlencode(dict(project=self.project)))
self.client.do('DELETE', url)
if not self.module.check_mode:
self.client.do('DELETE', url)
self.actions.append('delete')
def _freeze_instance(self):
@ -562,15 +582,13 @@ class LXDContainerManagement(object):
def _unfreeze_instance(self):
self._change_state('unfreeze')
self.actions.append('unfreez')
self.actions.append('unfreeze')
def _instance_ipv4_addresses(self, ignore_devices=None):
ignore_devices = ['lo'] if ignore_devices is None else ignore_devices
resp_json = self._get_instance_state_json()
network = resp_json['metadata']['network'] or {}
network = dict((k, v) for k, v in network.items() if k not in ignore_devices) or {}
addresses = dict((k, [a['address'] for a in v['addresses'] if a['family'] == 'inet']) for k, v in network.items()) or {}
data = (self._get_instance_state_json() or {}).get('metadata', None) or {}
network = dict((k, v) for k, v in (data.get('network', None) or {}).items() if k not in ignore_devices)
addresses = dict((k, [a['address'] for a in v['addresses'] if a['family'] == 'inet']) for k, v in network.items())
return addresses
@staticmethod
@ -583,7 +601,7 @@ class LXDContainerManagement(object):
while datetime.datetime.now() < due:
time.sleep(1)
addresses = self._instance_ipv4_addresses()
if self._has_all_ipv4_addresses(addresses):
if self._has_all_ipv4_addresses(addresses) or self.module.check_mode:
self.addresses = addresses
return
except LXDClientException as e:
@ -656,16 +674,10 @@ class LXDContainerManagement(object):
def _needs_to_change_instance_config(self, key):
if key not in self.config:
return False
if key == 'config' and self.ignore_volatile_options: # the old behavior is to ignore configurations by keyword "volatile"
old_configs = dict((k, v) for k, v in self.old_instance_json['metadata'][key].items() if not k.startswith('volatile.'))
for k, v in self.config['config'].items():
if k not in old_configs:
return True
if old_configs[k] != v:
return True
return False
elif key == 'config': # next default behavior
old_configs = dict((k, v) for k, v in self.old_instance_json['metadata'][key].items())
if key == 'config':
# self.old_sections is already filtered for volatile keys if necessary
old_configs = dict(self.old_sections.get(key, None) or {})
for k, v in self.config['config'].items():
if k not in old_configs:
return True
@ -673,43 +685,35 @@ class LXDContainerManagement(object):
return True
return False
else:
old_configs = self.old_instance_json['metadata'][key]
old_configs = self.old_sections.get(key, {})
return self.config[key] != old_configs
def _needs_to_apply_instance_configs(self):
return (
self._needs_to_change_instance_config('architecture') or
self._needs_to_change_instance_config('config') or
self._needs_to_change_instance_config('ephemeral') or
self._needs_to_change_instance_config('devices') or
self._needs_to_change_instance_config('profiles')
)
for param in set(CONFIG_PARAMS) - set(CONFIG_CREATION_PARAMS):
if self._needs_to_change_instance_config(param):
return True
return False
def _apply_instance_configs(self):
old_metadata = self.old_instance_json['metadata']
body_json = {
'architecture': old_metadata['architecture'],
'config': old_metadata['config'],
'devices': old_metadata['devices'],
'profiles': old_metadata['profiles']
}
if self._needs_to_change_instance_config('architecture'):
body_json['architecture'] = self.config['architecture']
if self._needs_to_change_instance_config('config'):
for k, v in self.config['config'].items():
body_json['config'][k] = v
if self._needs_to_change_instance_config('ephemeral'):
body_json['ephemeral'] = self.config['ephemeral']
if self._needs_to_change_instance_config('devices'):
body_json['devices'] = self.config['devices']
if self._needs_to_change_instance_config('profiles'):
body_json['profiles'] = self.config['profiles']
old_metadata = copy.deepcopy(self.old_instance_json).get('metadata', None) or {}
body_json = {}
for param in set(CONFIG_PARAMS) - set(CONFIG_CREATION_PARAMS):
if param in old_metadata:
body_json[param] = old_metadata[param]
if self._needs_to_change_instance_config(param):
if param == 'config':
body_json['config'] = body_json.get('config', None) or {}
for k, v in self.config['config'].items():
body_json['config'][k] = v
else:
body_json[param] = self.config[param]
self.diff['after']['instance'] = body_json
url = '{0}/{1}'.format(self.api_endpoint, self.name)
if self.project:
url = '{0}?{1}'.format(url, urlencode(dict(project=self.project)))
self.client.do('PUT', url, body_json=body_json)
if not self.module.check_mode:
self.client.do('PUT', url, body_json=body_json)
self.actions.append('apply_instance_configs')
def run(self):
@ -721,7 +725,22 @@ class LXDContainerManagement(object):
self.ignore_volatile_options = self.module.params.get('ignore_volatile_options')
self.old_instance_json = self._get_instance_json()
self.old_sections = dict(
(section, content) if not isinstance(content, dict)
else (section, dict((k, v) for k, v in content.items()
if not (self.ignore_volatile_options and k.startswith('volatile.'))))
for section, content in (self.old_instance_json.get('metadata', None) or {}).items()
if section in set(CONFIG_PARAMS) - set(CONFIG_CREATION_PARAMS)
)
self.diff['before']['instance'] = self.old_sections
# preliminary, will be overwritten in _apply_instance_configs() if called
self.diff['after']['instance'] = self.config
self.old_state = self._instance_json_to_module_state(self.old_instance_json)
self.diff['before']['state'] = self.old_state
self.diff['after']['state'] = self.state
action = getattr(self, LXD_ANSIBLE_STATES[self.state])
action()
@ -730,7 +749,8 @@ class LXDContainerManagement(object):
'log_verbosity': self.module._verbosity,
'changed': state_changed,
'old_state': self.old_state,
'actions': self.actions
'actions': self.actions,
'diff': self.diff,
}
if self.client.debug:
result_json['logs'] = self.client.logs
@ -742,7 +762,8 @@ class LXDContainerManagement(object):
fail_params = {
'msg': e.msg,
'changed': state_changed,
'actions': self.actions
'actions': self.actions,
'diff': self.diff,
}
if self.client.debug:
fail_params['logs'] = e.kwargs['logs']
@ -756,7 +777,7 @@ def main():
argument_spec=dict(
name=dict(
type='str',
required=True
required=True,
),
project=dict(
type='str',
@ -786,7 +807,7 @@ def main():
),
state=dict(
choices=list(LXD_ANSIBLE_STATES.keys()),
default='started'
default='started',
),
target=dict(
type='str',
@ -802,35 +823,35 @@ def main():
),
wait_for_container=dict(
type='bool',
default=False
default=False,
),
wait_for_ipv4_addresses=dict(
type='bool',
default=False
default=False,
),
force_stop=dict(
type='bool',
default=False
default=False,
),
url=dict(
type='str',
default=ANSIBLE_LXD_DEFAULT_URL
default=ANSIBLE_LXD_DEFAULT_URL,
),
snap_url=dict(
type='str',
default='unix:/var/snap/lxd/common/lxd/unix.socket'
default='unix:/var/snap/lxd/common/lxd/unix.socket',
),
client_key=dict(
type='path',
aliases=['key_file']
aliases=['key_file'],
),
client_cert=dict(
type='path',
aliases=['cert_file']
aliases=['cert_file'],
),
trust_password=dict(type='str', no_log=True)
trust_password=dict(type='str', no_log=True),
),
supports_check_mode=False,
supports_check_mode=True,
)
lxd_manage = LXDContainerManagement(module=module)

View file

@ -35,7 +35,7 @@ options:
type: str
config:
description:
- 'The config for the container (e.g. {"limits.memory": "4GB"}).
- 'The config for the instance (e.g. {"limits.memory": "4GB"}).
See U(https://github.com/lxc/lxd/blob/master/doc/rest-api.md#patch-3)'
- If the profile already exists and its "config" value in metadata
obtained from
@ -247,7 +247,7 @@ CONFIG_PARAMS = [
class LXDProfileManagement(object):
def __init__(self, module):
"""Management of LXC containers via Ansible.
"""Management of LXC profiles via Ansible.
:param module: Processed Ansible Module.
:type module: ``object``