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: support lxd instance types (#3661)

* lxd_container: support lxd instance types

Update the lxd_container module to enable the new LXD API endpoint,
which supports different types of instances, such as containers and virtual machines.
The type attributes can be set explicitly to create containers or virtual machines.

* lxd_container: rename references from containers to instances

* lxd_container: add an example of creating vms

* lxd_container: update doc

* lxd_container: fix pylint

* resolve converstation

* remove type from config

* remove outdated validation related to the instance api

* correct diff

* changing last bits

* add missing dot
This commit is contained in:
rchicoli 2021-11-20 08:20:24 +01:00 committed by GitHub
parent fef02c0fba
commit 58eb94fff3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 150 additions and 100 deletions

View file

@ -0,0 +1,2 @@
minor_changes:
- lxd_container - adds ``type`` option which also allows to operate on virtual machines and not just containers (https://github.com/ansible-collections/community.general/pull/3661).

View file

@ -11,29 +11,28 @@ __metaclass__ = type
DOCUMENTATION = ''' DOCUMENTATION = '''
--- ---
module: lxd_container module: lxd_container
short_description: Manage LXD Containers short_description: Manage LXD instances
description: description:
- Management of LXD containers - Management of LXD containers and virtual machines.
author: "Hiroaki Nakamura (@hnakamur)" author: "Hiroaki Nakamura (@hnakamur)"
options: options:
name: name:
description: description:
- Name of a container. - Name of an instance.
type: str type: str
required: true required: true
architecture: architecture:
description: description:
- 'The architecture for the container (for example C(x86_64) or C(i686)). - 'The architecture for the instance (for example C(x86_64) or C(i686)).
See U(https://github.com/lxc/lxd/blob/master/doc/rest-api.md#post-1).' See U(https://github.com/lxc/lxd/blob/master/doc/rest-api.md#post-1).'
type: str type: str
required: false required: false
config: config:
description: description:
- 'The config for the container (for example C({"limits.cpu": "2"})). - 'The config for the instance (for example C({"limits.cpu": "2"})).
See U(https://github.com/lxc/lxd/blob/master/doc/rest-api.md#post-1).' See U(https://github.com/lxc/lxd/blob/master/doc/rest-api.md#post-1).'
- If the container already exists and its "config" values in metadata - If the instance already exists and its "config" values in metadata
obtained from GET /1.0/containers/<name> obtained from the LXD API U(https://github.com/lxc/lxd/blob/master/doc/rest-api.md#instances-containers-and-virtual-machines)
U(https://github.com/lxc/lxd/blob/master/doc/rest-api.md#10containersname)
are different, this module tries to apply the configurations. are different, this module tries to apply the configurations.
- The keys starting with C(volatile.) are ignored for this comparison when I(ignore_volatile_options=true). - The keys starting with C(volatile.) are ignored for this comparison when I(ignore_volatile_options=true).
type: dict type: dict
@ -50,25 +49,25 @@ options:
version_added: 3.7.0 version_added: 3.7.0
profiles: profiles:
description: description:
- Profile to be used by the container. - Profile to be used by the instance.
type: list type: list
elements: str elements: str
devices: devices:
description: description:
- 'The devices for the container - 'The devices for the instance
(for example C({ "rootfs": { "path": "/dev/kvm", "type": "unix-char" }})). (for example C({ "rootfs": { "path": "/dev/kvm", "type": "unix-char" }})).
See U(https://github.com/lxc/lxd/blob/master/doc/rest-api.md#post-1).' See U(https://github.com/lxc/lxd/blob/master/doc/rest-api.md#post-1).'
type: dict type: dict
required: false required: false
ephemeral: ephemeral:
description: description:
- Whether or not the container is ephemeral (for example C(true) or C(false)). - Whether or not the instance is ephemeral (for example C(true) or C(false)).
See U(https://github.com/lxc/lxd/blob/master/doc/rest-api.md#post-1). See U(https://github.com/lxc/lxd/blob/master/doc/rest-api.md#post-1).
required: false required: false
type: bool type: bool
source: source:
description: description:
- 'The source for the container - 'The source for the instance
(e.g. { "type": "image", (e.g. { "type": "image",
"mode": "pull", "mode": "pull",
"server": "https://images.linuxcontainers.org", "server": "https://images.linuxcontainers.org",
@ -86,39 +85,49 @@ options:
- absent - absent
- frozen - frozen
description: description:
- Define the state of a container. - Define the state of an instance.
required: false required: false
default: started default: started
type: str type: str
target: target:
description: description:
- For cluster deployments. Will attempt to create a container on a target node. - For cluster deployments. Will attempt to create an instance on a target node.
If container exists elsewhere in a cluster, then container will not be replaced or moved. If the instance exists elsewhere in a cluster, then it will not be replaced or moved.
The name should respond to same name of the node you see in C(lxc cluster list). The name should respond to same name of the node you see in C(lxc cluster list).
type: str type: str
required: false required: false
version_added: 1.0.0 version_added: 1.0.0
timeout: timeout:
description: description:
- A timeout for changing the state of the container. - A timeout for changing the state of the instance.
- This is also used as a timeout for waiting until IPv4 addresses - This is also used as a timeout for waiting until IPv4 addresses
are set to the all network interfaces in the container after are set to the all network interfaces in the instance after
starting or restarting. starting or restarting.
required: false required: false
default: 30 default: 30
type: int type: int
type:
description:
- Instance type can be either C(virtual-machine) or C(container).
required: false
default: container
choices:
- container
- virtual-machine
type: str
version_added: 4.1.0
wait_for_ipv4_addresses: wait_for_ipv4_addresses:
description: description:
- If this is true, the C(lxd_container) waits until IPv4 addresses - If this is true, the C(lxd_container) waits until IPv4 addresses
are set to the all network interfaces in the container after are set to the all network interfaces in the instance after
starting or restarting. starting or restarting.
required: false required: false
default: false default: false
type: bool type: bool
force_stop: force_stop:
description: description:
- If this is true, the C(lxd_container) forces to stop the container - If this is true, the C(lxd_container) forces to stop the instance
when it stops or restarts the container. when it stops or restarts the instance.
required: false required: false
default: false default: false
type: bool type: bool
@ -160,18 +169,18 @@ options:
required: false required: false
type: str type: str
notes: notes:
- Containers must have a unique name. If you attempt to create a container - Instances can be a container or a virtual machine, both of them must have unique name. If you attempt to create an instance
with a name that already existed in the users namespace the module will with a name that already existed in the users namespace the module will
simply return as "unchanged". simply return as "unchanged".
- There are two ways to run commands in containers, using the command - There are two ways to run commands inside a container or virtual machine, using the command
module or using the ansible lxd connection plugin bundled in Ansible >= module or using the ansible lxd connection plugin bundled in Ansible >=
2.1, the later requires python to be installed in the container which can 2.1, the later requires python to be installed in the instance which can
be done with the command module. be done with the command module.
- You can copy a file from the host to the container - You can copy a file from the host to the instance
with the Ansible M(ansible.builtin.copy) and M(ansible.builtin.template) module and the `lxd` connection plugin. with the Ansible M(ansible.builtin.copy) and M(ansible.builtin.template) module and the `lxd` connection plugin.
See the example below. See the example below.
- You can copy a file in the created container to the localhost - You can copy a file in the created instance to the localhost
with `command=lxc file pull container_name/dir/filename filename`. with `command=lxc file pull instance_name/dir/filename filename`.
See the first example below. See the first example below.
''' '''
@ -240,6 +249,7 @@ EXAMPLES = '''
community.general.lxd_container: community.general.lxd_container:
name: mycontainer name: mycontainer
state: absent state: absent
type: container
# An example for restarting a container # An example for restarting a container
- hosts: localhost - hosts: localhost
@ -249,6 +259,7 @@ EXAMPLES = '''
community.general.lxd_container: community.general.lxd_container:
name: mycontainer name: mycontainer
state: restarted state: restarted
type: container
# An example for restarting a container using https to connect to the LXD server # An example for restarting a container using https to connect to the LXD server
- hosts: localhost - hosts: localhost
@ -306,16 +317,36 @@ EXAMPLES = '''
mode: pull mode: pull
alias: ubuntu/xenial/amd64 alias: ubuntu/xenial/amd64
target: node02 target: node02
# An example for creating a virtual machine
- hosts: localhost
connection: local
tasks:
- name: Create container on another node
community.general.lxd_container:
name: new-vm-1
type: virtual-machine
state: started
ignore_volatile_options: true
wait_for_ipv4_addresses: true
profiles: ["default"]
source:
protocol: simplestreams
type: image
mode: pull
server: https://images.linuxcontainers.org
alias: debian/11
timeout: 600
''' '''
RETURN = ''' RETURN = '''
addresses: addresses:
description: Mapping from the network device name to a list of IPv4 addresses in the container description: Mapping from the network device name to a list of IPv4 addresses in the instance.
returned: when state is started or restarted returned: when state is started or restarted
type: dict type: dict
sample: {"eth0": ["10.155.92.191"]} sample: {"eth0": ["10.155.92.191"]}
old_state: old_state:
description: The old state of the container description: The old state of the instance.
returned: when state is started or restarted returned: when state is started or restarted
type: str type: str
sample: "stopped" sample: "stopped"
@ -325,7 +356,7 @@ logs:
type: list type: list
sample: "(too long to be placed here)" sample: "(too long to be placed here)"
actions: actions:
description: List of actions performed for the container. description: List of actions performed for the instance.
returned: success returned: success
type: list type: list
sample: '["create", "start"]' sample: '["create", "start"]'
@ -384,6 +415,15 @@ class LXDContainerManagement(object):
self.addresses = None self.addresses = None
self.target = self.module.params['target'] self.target = self.module.params['target']
self.type = self.module.params['type']
# LXD Rest API provides additional endpoints for creating containers and virtual-machines.
self.api_endpoint = None
if self.type == 'container':
self.api_endpoint = '/1.0/containers'
elif self.type == 'virtual-machine':
self.api_endpoint = '/1.0/virtual-machines'
self.key_file = self.module.params.get('client_key') self.key_file = self.module.params.get('client_key')
if self.key_file is None: if self.key_file is None:
self.key_file = '{0}/.config/lxc/client.key'.format(os.environ['HOME']) self.key_file = '{0}/.config/lxc/client.key'.format(os.environ['HOME'])
@ -419,20 +459,20 @@ class LXDContainerManagement(object):
if param_val is not None: if param_val is not None:
self.config[attr] = param_val self.config[attr] = param_val
def _get_container_json(self): def _get_instance_json(self):
return self.client.do( return self.client.do(
'GET', '/1.0/containers/{0}'.format(self.name), 'GET', '{0}/{1}'.format(self.api_endpoint, self.name),
ok_error_codes=[404] ok_error_codes=[404]
) )
def _get_container_state_json(self): def _get_instance_state_json(self):
return self.client.do( return self.client.do(
'GET', '/1.0/containers/{0}/state'.format(self.name), 'GET', '{0}/{1}/state'.format(self.api_endpoint, self.name),
ok_error_codes=[404] ok_error_codes=[404]
) )
@staticmethod @staticmethod
def _container_json_to_module_state(resp_json): def _instance_json_to_module_state(resp_json):
if resp_json['type'] == 'error': if resp_json['type'] == 'error':
return 'absent' return 'absent'
return ANSIBLE_LXD_STATES[resp_json['metadata']['status']] return ANSIBLE_LXD_STATES[resp_json['metadata']['status']]
@ -441,45 +481,45 @@ 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
return self.client.do('PUT', '/1.0/containers/{0}/state'.format(self.name), body_json=body_json) return self.client.do('PUT', '{0}/{1}/state'.format(self.api_endpoint, self.name), body_json=body_json)
def _create_container(self): def _create_instance(self):
config = self.config.copy() config = self.config.copy()
config['name'] = self.name config['name'] = self.name
if self.target: if self.target:
self.client.do('POST', '/1.0/containers?' + urlencode(dict(target=self.target)), config) self.client.do('POST', '{0}?{1}'.format(self.api_endpoint, urlencode(dict(target=self.target))), config)
else: else:
self.client.do('POST', '/1.0/containers', config) self.client.do('POST', self.api_endpoint, config)
self.actions.append('create') self.actions.append('create')
def _start_container(self): def _start_instance(self):
self._change_state('start') self._change_state('start')
self.actions.append('start') self.actions.append('start')
def _stop_container(self): def _stop_instance(self):
self._change_state('stop', self.force_stop) self._change_state('stop', self.force_stop)
self.actions.append('stop') self.actions.append('stop')
def _restart_container(self): def _restart_instance(self):
self._change_state('restart', self.force_stop) self._change_state('restart', self.force_stop)
self.actions.append('restart') self.actions.append('restart')
def _delete_container(self): def _delete_instance(self):
self.client.do('DELETE', '/1.0/containers/{0}'.format(self.name)) self.client.do('DELETE', '{0}/{1}'.format(self.api_endpoint, self.name))
self.actions.append('delete') self.actions.append('delete')
def _freeze_container(self): def _freeze_instance(self):
self._change_state('freeze') self._change_state('freeze')
self.actions.append('freeze') self.actions.append('freeze')
def _unfreeze_container(self): def _unfreeze_instance(self):
self._change_state('unfreeze') self._change_state('unfreeze')
self.actions.append('unfreez') self.actions.append('unfreez')
def _container_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
resp_json = self._get_container_state_json() resp_json = self._get_instance_state_json()
network = resp_json['metadata']['network'] or {} network = resp_json['metadata']['network'] or {}
network = dict((k, v) for k, v in network.items() if k not in ignore_devices) 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 {} addresses = dict((k, [a['address'] for a in v['addresses'] if a['family'] == 'inet']) for k, v in network.items()) or {}
@ -494,7 +534,7 @@ class LXDContainerManagement(object):
due = datetime.datetime.now() + datetime.timedelta(seconds=self.timeout) due = datetime.datetime.now() + datetime.timedelta(seconds=self.timeout)
while datetime.datetime.now() < due: while datetime.datetime.now() < due:
time.sleep(1) time.sleep(1)
addresses = self._container_ipv4_addresses() addresses = self._instance_ipv4_addresses()
if self._has_all_ipv4_addresses(addresses): if self._has_all_ipv4_addresses(addresses):
self.addresses = addresses self.addresses = addresses
return return
@ -504,72 +544,72 @@ class LXDContainerManagement(object):
def _started(self): def _started(self):
if self.old_state == 'absent': if self.old_state == 'absent':
self._create_container() self._create_instance()
self._start_container() self._start_instance()
else: else:
if self.old_state == 'frozen': if self.old_state == 'frozen':
self._unfreeze_container() self._unfreeze_instance()
elif self.old_state == 'stopped': elif self.old_state == 'stopped':
self._start_container() self._start_instance()
if self._needs_to_apply_container_configs(): if self._needs_to_apply_instance_configs():
self._apply_container_configs() self._apply_instance_configs()
if self.wait_for_ipv4_addresses: if self.wait_for_ipv4_addresses:
self._get_addresses() self._get_addresses()
def _stopped(self): def _stopped(self):
if self.old_state == 'absent': if self.old_state == 'absent':
self._create_container() self._create_instance()
else: else:
if self.old_state == 'stopped': if self.old_state == 'stopped':
if self._needs_to_apply_container_configs(): if self._needs_to_apply_instance_configs():
self._start_container() self._start_instance()
self._apply_container_configs() self._apply_instance_configs()
self._stop_container() self._stop_instance()
else: else:
if self.old_state == 'frozen': if self.old_state == 'frozen':
self._unfreeze_container() self._unfreeze_instance()
if self._needs_to_apply_container_configs(): if self._needs_to_apply_instance_configs():
self._apply_container_configs() self._apply_instance_configs()
self._stop_container() self._stop_instance()
def _restarted(self): def _restarted(self):
if self.old_state == 'absent': if self.old_state == 'absent':
self._create_container() self._create_instance()
self._start_container() self._start_instance()
else: else:
if self.old_state == 'frozen': if self.old_state == 'frozen':
self._unfreeze_container() self._unfreeze_instance()
if self._needs_to_apply_container_configs(): if self._needs_to_apply_instance_configs():
self._apply_container_configs() self._apply_instance_configs()
self._restart_container() self._restart_instance()
if self.wait_for_ipv4_addresses: if self.wait_for_ipv4_addresses:
self._get_addresses() self._get_addresses()
def _destroyed(self): def _destroyed(self):
if self.old_state != 'absent': if self.old_state != 'absent':
if self.old_state == 'frozen': if self.old_state == 'frozen':
self._unfreeze_container() self._unfreeze_instance()
if self.old_state != 'stopped': if self.old_state != 'stopped':
self._stop_container() self._stop_instance()
self._delete_container() self._delete_instance()
def _frozen(self): def _frozen(self):
if self.old_state == 'absent': if self.old_state == 'absent':
self._create_container() self._create_instance()
self._start_container() self._start_instance()
self._freeze_container() self._freeze_instance()
else: else:
if self.old_state == 'stopped': if self.old_state == 'stopped':
self._start_container() self._start_instance()
if self._needs_to_apply_container_configs(): if self._needs_to_apply_instance_configs():
self._apply_container_configs() self._apply_instance_configs()
self._freeze_container() self._freeze_instance()
def _needs_to_change_container_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" 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_container_json['metadata'][key].items() if not k.startswith('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(): for k, v in self.config['config'].items():
if k not in old_configs: if k not in old_configs:
return True return True
@ -577,7 +617,7 @@ class LXDContainerManagement(object):
return True return True
return False return False
elif key == 'config': # next default behavior elif key == 'config': # next default behavior
old_configs = dict((k, v) for k, v in self.old_container_json['metadata'][key].items()) 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
@ -585,39 +625,41 @@ class LXDContainerManagement(object):
return True return True
return False return False
else: else:
old_configs = self.old_container_json['metadata'][key] old_configs = self.old_instance_json['metadata'][key]
return self.config[key] != old_configs return self.config[key] != old_configs
def _needs_to_apply_container_configs(self): def _needs_to_apply_instance_configs(self):
return ( return (
self._needs_to_change_container_config('architecture') or self._needs_to_change_instance_config('architecture') or
self._needs_to_change_container_config('config') or self._needs_to_change_instance_config('config') or
self._needs_to_change_container_config('ephemeral') or self._needs_to_change_instance_config('ephemeral') or
self._needs_to_change_container_config('devices') or self._needs_to_change_instance_config('devices') or
self._needs_to_change_container_config('profiles') self._needs_to_change_instance_config('profiles')
) )
def _apply_container_configs(self): def _apply_instance_configs(self):
old_metadata = self.old_container_json['metadata'] old_metadata = self.old_instance_json['metadata']
body_json = { body_json = {
'architecture': old_metadata['architecture'], 'architecture': old_metadata['architecture'],
'config': old_metadata['config'], 'config': old_metadata['config'],
'devices': old_metadata['devices'], 'devices': old_metadata['devices'],
'profiles': old_metadata['profiles'] 'profiles': old_metadata['profiles']
} }
if self._needs_to_change_container_config('architecture'):
if self._needs_to_change_instance_config('architecture'):
body_json['architecture'] = self.config['architecture'] body_json['architecture'] = self.config['architecture']
if self._needs_to_change_container_config('config'): if self._needs_to_change_instance_config('config'):
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_container_config('ephemeral'): if self._needs_to_change_instance_config('ephemeral'):
body_json['ephemeral'] = self.config['ephemeral'] body_json['ephemeral'] = self.config['ephemeral']
if self._needs_to_change_container_config('devices'): if self._needs_to_change_instance_config('devices'):
body_json['devices'] = self.config['devices'] body_json['devices'] = self.config['devices']
if self._needs_to_change_container_config('profiles'): if self._needs_to_change_instance_config('profiles'):
body_json['profiles'] = self.config['profiles'] body_json['profiles'] = self.config['profiles']
self.client.do('PUT', '/1.0/containers/{0}'.format(self.name), body_json=body_json)
self.actions.append('apply_container_configs') self.client.do('PUT', '{0}/{1}'.format(self.api_endpoint, self.name), body_json=body_json)
self.actions.append('apply_instance_configs')
def run(self): def run(self):
"""Run the main method.""" """Run the main method."""
@ -627,8 +669,8 @@ class LXDContainerManagement(object):
self.client.authenticate(self.trust_password) self.client.authenticate(self.trust_password)
self.ignore_volatile_options = self.module.params.get('ignore_volatile_options') self.ignore_volatile_options = self.module.params.get('ignore_volatile_options')
self.old_container_json = self._get_container_json() self.old_instance_json = self._get_instance_json()
self.old_state = self._container_json_to_module_state(self.old_container_json) self.old_state = self._instance_json_to_module_state(self.old_instance_json)
action = getattr(self, LXD_ANSIBLE_STATES[self.state]) action = getattr(self, LXD_ANSIBLE_STATES[self.state])
action() action()
@ -698,6 +740,11 @@ def main():
type='int', type='int',
default=30 default=30
), ),
type=dict(
type='str',
default='container',
choices=['container', 'virtual-machine'],
),
wait_for_ipv4_addresses=dict( wait_for_ipv4_addresses=dict(
type='bool', type='bool',
default=False default=False
@ -736,6 +783,7 @@ def main():
'This will change in the future. Please test your scripts' 'This will change in the future. Please test your scripts'
'by "ignore_volatile_options: false". To keep the old behavior, set that option explicitly to "true"', 'by "ignore_volatile_options: false". To keep the old behavior, set that option explicitly to "true"',
version='6.0.0', collection_name='community.general') version='6.0.0', collection_name='community.general')
lxd_manage = LXDContainerManagement(module=module) lxd_manage = LXDContainerManagement(module=module)
lxd_manage.run() lxd_manage.run()