`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>pull/5952/head
parent
867aee606e
commit
7e3c73ceb2
|
@ -0,0 +1,2 @@
|
||||||
|
minor_changes:
|
||||||
|
- lxd_container - add diff and check mode (https://github.com/ansible-collections/community.general/pull/5866).
|
|
@ -16,6 +16,15 @@ short_description: Manage LXD instances
|
||||||
description:
|
description:
|
||||||
- Management of LXD containers and virtual machines.
|
- Management of LXD containers and virtual machines.
|
||||||
author: "Hiroaki Nakamura (@hnakamur)"
|
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:
|
options:
|
||||||
name:
|
name:
|
||||||
description:
|
description:
|
||||||
|
@ -396,6 +405,7 @@ actions:
|
||||||
type: list
|
type: list
|
||||||
sample: ["create", "start"]
|
sample: ["create", "start"]
|
||||||
'''
|
'''
|
||||||
|
import copy
|
||||||
import datetime
|
import datetime
|
||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
|
@ -411,7 +421,7 @@ LXD_ANSIBLE_STATES = {
|
||||||
'stopped': '_stopped',
|
'stopped': '_stopped',
|
||||||
'restarted': '_restarted',
|
'restarted': '_restarted',
|
||||||
'absent': '_destroyed',
|
'absent': '_destroyed',
|
||||||
'frozen': '_frozen'
|
'frozen': '_frozen',
|
||||||
}
|
}
|
||||||
|
|
||||||
# ANSIBLE_LXD_STATES is a map of states of lxd containers to the Ansible
|
# 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'
|
'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):
|
class LXDContainerManagement(object):
|
||||||
def __init__(self, module):
|
def __init__(self, module):
|
||||||
|
@ -488,6 +502,9 @@ class LXDContainerManagement(object):
|
||||||
self.module.fail_json(msg=e.msg)
|
self.module.fail_json(msg=e.msg)
|
||||||
self.trust_password = self.module.params.get('trust_password', None)
|
self.trust_password = self.module.params.get('trust_password', None)
|
||||||
self.actions = []
|
self.actions = []
|
||||||
|
self.diff = {'before': {}, 'after': {}}
|
||||||
|
self.old_instance_json = {}
|
||||||
|
self.old_sections = {}
|
||||||
|
|
||||||
def _build_config(self):
|
def _build_config(self):
|
||||||
self.config = {}
|
self.config = {}
|
||||||
|
@ -521,6 +538,7 @@ class LXDContainerManagement(object):
|
||||||
body_json = {'action': action, 'timeout': self.timeout}
|
body_json = {'action': action, 'timeout': self.timeout}
|
||||||
if force_stop:
|
if force_stop:
|
||||||
body_json['force'] = True
|
body_json['force'] = True
|
||||||
|
if not self.module.check_mode:
|
||||||
return self.client.do('PUT', url, body_json=body_json)
|
return self.client.do('PUT', url, body_json=body_json)
|
||||||
|
|
||||||
def _create_instance(self):
|
def _create_instance(self):
|
||||||
|
@ -534,6 +552,7 @@ class LXDContainerManagement(object):
|
||||||
url = '{0}?{1}'.format(url, urlencode(url_params))
|
url = '{0}?{1}'.format(url, urlencode(url_params))
|
||||||
config = self.config.copy()
|
config = self.config.copy()
|
||||||
config['name'] = self.name
|
config['name'] = self.name
|
||||||
|
if not self.module.check_mode:
|
||||||
self.client.do('POST', url, config, wait_for_container=self.wait_for_container)
|
self.client.do('POST', url, config, wait_for_container=self.wait_for_container)
|
||||||
self.actions.append('create')
|
self.actions.append('create')
|
||||||
|
|
||||||
|
@ -553,6 +572,7 @@ class LXDContainerManagement(object):
|
||||||
url = '{0}/{1}'.format(self.api_endpoint, self.name)
|
url = '{0}/{1}'.format(self.api_endpoint, self.name)
|
||||||
if self.project:
|
if self.project:
|
||||||
url = '{0}?{1}'.format(url, urlencode(dict(project=self.project)))
|
url = '{0}?{1}'.format(url, urlencode(dict(project=self.project)))
|
||||||
|
if not self.module.check_mode:
|
||||||
self.client.do('DELETE', url)
|
self.client.do('DELETE', url)
|
||||||
self.actions.append('delete')
|
self.actions.append('delete')
|
||||||
|
|
||||||
|
@ -562,15 +582,13 @@ class LXDContainerManagement(object):
|
||||||
|
|
||||||
def _unfreeze_instance(self):
|
def _unfreeze_instance(self):
|
||||||
self._change_state('unfreeze')
|
self._change_state('unfreeze')
|
||||||
self.actions.append('unfreez')
|
self.actions.append('unfreeze')
|
||||||
|
|
||||||
def _instance_ipv4_addresses(self, ignore_devices=None):
|
def _instance_ipv4_addresses(self, ignore_devices=None):
|
||||||
ignore_devices = ['lo'] if ignore_devices is None else ignore_devices
|
ignore_devices = ['lo'] if ignore_devices is None else ignore_devices
|
||||||
|
data = (self._get_instance_state_json() or {}).get('metadata', None) or {}
|
||||||
resp_json = self._get_instance_state_json()
|
network = dict((k, v) for k, v in (data.get('network', None) or {}).items() if k not in ignore_devices)
|
||||||
network = resp_json['metadata']['network'] or {}
|
addresses = dict((k, [a['address'] for a in v['addresses'] if a['family'] == 'inet']) for k, v in network.items())
|
||||||
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 {}
|
|
||||||
return addresses
|
return addresses
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
@ -583,7 +601,7 @@ class LXDContainerManagement(object):
|
||||||
while datetime.datetime.now() < due:
|
while datetime.datetime.now() < due:
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
addresses = self._instance_ipv4_addresses()
|
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
|
self.addresses = addresses
|
||||||
return
|
return
|
||||||
except LXDClientException as e:
|
except LXDClientException as e:
|
||||||
|
@ -656,16 +674,10 @@ class LXDContainerManagement(object):
|
||||||
def _needs_to_change_instance_config(self, key):
|
def _needs_to_change_instance_config(self, key):
|
||||||
if key not in self.config:
|
if key not in self.config:
|
||||||
return False
|
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.'))
|
if key == 'config':
|
||||||
for k, v in self.config['config'].items():
|
# self.old_sections is already filtered for volatile keys if necessary
|
||||||
if k not in old_configs:
|
old_configs = dict(self.old_sections.get(key, None) or {})
|
||||||
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())
|
|
||||||
for k, v in self.config['config'].items():
|
for k, v in self.config['config'].items():
|
||||||
if k not in old_configs:
|
if k not in old_configs:
|
||||||
return True
|
return True
|
||||||
|
@ -673,42 +685,34 @@ class LXDContainerManagement(object):
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
old_configs = self.old_instance_json['metadata'][key]
|
old_configs = self.old_sections.get(key, {})
|
||||||
return self.config[key] != old_configs
|
return self.config[key] != old_configs
|
||||||
|
|
||||||
def _needs_to_apply_instance_configs(self):
|
def _needs_to_apply_instance_configs(self):
|
||||||
return (
|
for param in set(CONFIG_PARAMS) - set(CONFIG_CREATION_PARAMS):
|
||||||
self._needs_to_change_instance_config('architecture') or
|
if self._needs_to_change_instance_config(param):
|
||||||
self._needs_to_change_instance_config('config') or
|
return True
|
||||||
self._needs_to_change_instance_config('ephemeral') or
|
return False
|
||||||
self._needs_to_change_instance_config('devices') or
|
|
||||||
self._needs_to_change_instance_config('profiles')
|
|
||||||
)
|
|
||||||
|
|
||||||
def _apply_instance_configs(self):
|
def _apply_instance_configs(self):
|
||||||
old_metadata = self.old_instance_json['metadata']
|
old_metadata = copy.deepcopy(self.old_instance_json).get('metadata', None) or {}
|
||||||
body_json = {
|
body_json = {}
|
||||||
'architecture': old_metadata['architecture'],
|
for param in set(CONFIG_PARAMS) - set(CONFIG_CREATION_PARAMS):
|
||||||
'config': old_metadata['config'],
|
if param in old_metadata:
|
||||||
'devices': old_metadata['devices'],
|
body_json[param] = old_metadata[param]
|
||||||
'profiles': old_metadata['profiles']
|
|
||||||
}
|
|
||||||
|
|
||||||
if self._needs_to_change_instance_config('architecture'):
|
if self._needs_to_change_instance_config(param):
|
||||||
body_json['architecture'] = self.config['architecture']
|
if param == 'config':
|
||||||
if self._needs_to_change_instance_config('config'):
|
body_json['config'] = body_json.get('config', None) or {}
|
||||||
for k, v in self.config['config'].items():
|
for k, v in self.config['config'].items():
|
||||||
body_json['config'][k] = v
|
body_json['config'][k] = v
|
||||||
if self._needs_to_change_instance_config('ephemeral'):
|
else:
|
||||||
body_json['ephemeral'] = self.config['ephemeral']
|
body_json[param] = self.config[param]
|
||||||
if self._needs_to_change_instance_config('devices'):
|
self.diff['after']['instance'] = body_json
|
||||||
body_json['devices'] = self.config['devices']
|
|
||||||
if self._needs_to_change_instance_config('profiles'):
|
|
||||||
body_json['profiles'] = self.config['profiles']
|
|
||||||
|
|
||||||
url = '{0}/{1}'.format(self.api_endpoint, self.name)
|
url = '{0}/{1}'.format(self.api_endpoint, self.name)
|
||||||
if self.project:
|
if self.project:
|
||||||
url = '{0}?{1}'.format(url, urlencode(dict(project=self.project)))
|
url = '{0}?{1}'.format(url, urlencode(dict(project=self.project)))
|
||||||
|
if not self.module.check_mode:
|
||||||
self.client.do('PUT', url, body_json=body_json)
|
self.client.do('PUT', url, body_json=body_json)
|
||||||
self.actions.append('apply_instance_configs')
|
self.actions.append('apply_instance_configs')
|
||||||
|
|
||||||
|
@ -721,7 +725,22 @@ class LXDContainerManagement(object):
|
||||||
self.ignore_volatile_options = self.module.params.get('ignore_volatile_options')
|
self.ignore_volatile_options = self.module.params.get('ignore_volatile_options')
|
||||||
|
|
||||||
self.old_instance_json = self._get_instance_json()
|
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.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 = getattr(self, LXD_ANSIBLE_STATES[self.state])
|
||||||
action()
|
action()
|
||||||
|
|
||||||
|
@ -730,7 +749,8 @@ class LXDContainerManagement(object):
|
||||||
'log_verbosity': self.module._verbosity,
|
'log_verbosity': self.module._verbosity,
|
||||||
'changed': state_changed,
|
'changed': state_changed,
|
||||||
'old_state': self.old_state,
|
'old_state': self.old_state,
|
||||||
'actions': self.actions
|
'actions': self.actions,
|
||||||
|
'diff': self.diff,
|
||||||
}
|
}
|
||||||
if self.client.debug:
|
if self.client.debug:
|
||||||
result_json['logs'] = self.client.logs
|
result_json['logs'] = self.client.logs
|
||||||
|
@ -742,7 +762,8 @@ class LXDContainerManagement(object):
|
||||||
fail_params = {
|
fail_params = {
|
||||||
'msg': e.msg,
|
'msg': e.msg,
|
||||||
'changed': state_changed,
|
'changed': state_changed,
|
||||||
'actions': self.actions
|
'actions': self.actions,
|
||||||
|
'diff': self.diff,
|
||||||
}
|
}
|
||||||
if self.client.debug:
|
if self.client.debug:
|
||||||
fail_params['logs'] = e.kwargs['logs']
|
fail_params['logs'] = e.kwargs['logs']
|
||||||
|
@ -756,7 +777,7 @@ def main():
|
||||||
argument_spec=dict(
|
argument_spec=dict(
|
||||||
name=dict(
|
name=dict(
|
||||||
type='str',
|
type='str',
|
||||||
required=True
|
required=True,
|
||||||
),
|
),
|
||||||
project=dict(
|
project=dict(
|
||||||
type='str',
|
type='str',
|
||||||
|
@ -786,7 +807,7 @@ def main():
|
||||||
),
|
),
|
||||||
state=dict(
|
state=dict(
|
||||||
choices=list(LXD_ANSIBLE_STATES.keys()),
|
choices=list(LXD_ANSIBLE_STATES.keys()),
|
||||||
default='started'
|
default='started',
|
||||||
),
|
),
|
||||||
target=dict(
|
target=dict(
|
||||||
type='str',
|
type='str',
|
||||||
|
@ -802,35 +823,35 @@ def main():
|
||||||
),
|
),
|
||||||
wait_for_container=dict(
|
wait_for_container=dict(
|
||||||
type='bool',
|
type='bool',
|
||||||
default=False
|
default=False,
|
||||||
),
|
),
|
||||||
wait_for_ipv4_addresses=dict(
|
wait_for_ipv4_addresses=dict(
|
||||||
type='bool',
|
type='bool',
|
||||||
default=False
|
default=False,
|
||||||
),
|
),
|
||||||
force_stop=dict(
|
force_stop=dict(
|
||||||
type='bool',
|
type='bool',
|
||||||
default=False
|
default=False,
|
||||||
),
|
),
|
||||||
url=dict(
|
url=dict(
|
||||||
type='str',
|
type='str',
|
||||||
default=ANSIBLE_LXD_DEFAULT_URL
|
default=ANSIBLE_LXD_DEFAULT_URL,
|
||||||
),
|
),
|
||||||
snap_url=dict(
|
snap_url=dict(
|
||||||
type='str',
|
type='str',
|
||||||
default='unix:/var/snap/lxd/common/lxd/unix.socket'
|
default='unix:/var/snap/lxd/common/lxd/unix.socket',
|
||||||
),
|
),
|
||||||
client_key=dict(
|
client_key=dict(
|
||||||
type='path',
|
type='path',
|
||||||
aliases=['key_file']
|
aliases=['key_file'],
|
||||||
),
|
),
|
||||||
client_cert=dict(
|
client_cert=dict(
|
||||||
type='path',
|
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)
|
lxd_manage = LXDContainerManagement(module=module)
|
||||||
|
|
|
@ -35,7 +35,7 @@ options:
|
||||||
type: str
|
type: str
|
||||||
config:
|
config:
|
||||||
description:
|
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)'
|
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
|
- If the profile already exists and its "config" value in metadata
|
||||||
obtained from
|
obtained from
|
||||||
|
@ -247,7 +247,7 @@ CONFIG_PARAMS = [
|
||||||
|
|
||||||
class LXDProfileManagement(object):
|
class LXDProfileManagement(object):
|
||||||
def __init__(self, module):
|
def __init__(self, module):
|
||||||
"""Management of LXC containers via Ansible.
|
"""Management of LXC profiles via Ansible.
|
||||||
|
|
||||||
:param module: Processed Ansible Module.
|
:param module: Processed Ansible Module.
|
||||||
:type module: ``object``
|
:type module: ``object``
|
||||||
|
|
Loading…
Reference in New Issue