Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
minor_changes:
- lxd_container - add diff and check mode (https://github.com/ansible-collections/community.general/pull/5866).
147 changes: 84 additions & 63 deletions plugins/modules/lxd_container.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,15 @@
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:
Expand Down Expand Up @@ -396,6 +405,7 @@
type: list
sample: ["create", "start"]
'''
import copy
import datetime
import os
import time
Expand All @@ -411,7 +421,7 @@
'stopped': '_stopped',
'restarted': '_restarted',
'absent': '_destroyed',
'frozen': '_frozen'
'frozen': '_frozen',
}

# ANSIBLE_LXD_STATES is a map of states of lxd containers to the Ansible
Expand All @@ -430,6 +440,10 @@
'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):
Expand Down Expand Up @@ -488,6 +502,9 @@ def __init__(self, module):
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 = {}
Expand Down Expand Up @@ -521,7 +538,8 @@ def _change_state(self, action, force_stop=False):
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
Expand All @@ -534,7 +552,8 @@ def _create_instance(self):
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):
Expand All @@ -553,7 +572,8 @@ def _delete_instance(self):
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):
Expand All @@ -562,15 +582,13 @@ def _freeze_instance(self):

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
Expand All @@ -583,7 +601,7 @@ def _get_addresses(self):
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:
Expand Down Expand Up @@ -656,60 +674,46 @@ def _frozen(self):
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
if old_configs[k] != v:
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):
Expand All @@ -721,7 +725,22 @@ def run(self):
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()

Expand All @@ -730,7 +749,8 @@ def run(self):
'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
Expand All @@ -742,7 +762,8 @@ def run(self):
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']
Expand All @@ -756,7 +777,7 @@ def main():
argument_spec=dict(
name=dict(
type='str',
required=True
required=True,
),
project=dict(
type='str',
Expand Down Expand Up @@ -786,7 +807,7 @@ def main():
),
state=dict(
choices=list(LXD_ANSIBLE_STATES.keys()),
default='started'
default='started',
),
target=dict(
type='str',
Expand All @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions plugins/modules/lxd_profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
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
Expand Down Expand Up @@ -247,7 +247,7 @@

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``
Expand Down