From 58eb94fff38b7eba8d5116bd4b3c35c9ced08bbe Mon Sep 17 00:00:00 2001 From: rchicoli Date: Sat, 20 Nov 2021 08:20:24 +0100 Subject: [PATCH] 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 --- .../3661-lxd_container-add-vm-support.yml | 2 + plugins/modules/cloud/lxd/lxd_container.py | 248 +++++++++++------- 2 files changed, 150 insertions(+), 100 deletions(-) create mode 100644 changelogs/fragments/3661-lxd_container-add-vm-support.yml diff --git a/changelogs/fragments/3661-lxd_container-add-vm-support.yml b/changelogs/fragments/3661-lxd_container-add-vm-support.yml new file mode 100644 index 0000000000..6dd3105733 --- /dev/null +++ b/changelogs/fragments/3661-lxd_container-add-vm-support.yml @@ -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). diff --git a/plugins/modules/cloud/lxd/lxd_container.py b/plugins/modules/cloud/lxd/lxd_container.py index e06b72b244..28ce8a3f73 100644 --- a/plugins/modules/cloud/lxd/lxd_container.py +++ b/plugins/modules/cloud/lxd/lxd_container.py @@ -11,29 +11,28 @@ __metaclass__ = type DOCUMENTATION = ''' --- module: lxd_container -short_description: Manage LXD Containers +short_description: Manage LXD instances description: - - Management of LXD containers + - Management of LXD containers and virtual machines. author: "Hiroaki Nakamura (@hnakamur)" options: name: description: - - Name of a container. + - Name of an instance. type: str required: true architecture: 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).' type: str required: false config: 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).' - - If the container already exists and its "config" values in metadata - obtained from GET /1.0/containers/ - U(https://github.com/lxc/lxd/blob/master/doc/rest-api.md#10containersname) + - If the instance already exists and its "config" values in metadata + obtained from the LXD API U(https://github.com/lxc/lxd/blob/master/doc/rest-api.md#instances-containers-and-virtual-machines) 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). type: dict @@ -50,25 +49,25 @@ options: version_added: 3.7.0 profiles: description: - - Profile to be used by the container. + - Profile to be used by the instance. type: list elements: str devices: description: - - 'The devices for the container + - 'The devices for the instance (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).' type: dict required: false ephemeral: 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). required: false type: bool source: description: - - 'The source for the container + - 'The source for the instance (e.g. { "type": "image", "mode": "pull", "server": "https://images.linuxcontainers.org", @@ -86,39 +85,49 @@ options: - absent - frozen description: - - Define the state of a container. + - Define the state of an instance. required: false default: started type: str target: description: - - For cluster deployments. Will attempt to create a container on a target node. - If container exists elsewhere in a cluster, then container will not be replaced or moved. + - For cluster deployments. Will attempt to create an instance on a target node. + 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). type: str required: false version_added: 1.0.0 timeout: 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 - 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. required: false default: 30 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: description: - 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. required: false default: false type: bool force_stop: description: - - If this is true, the C(lxd_container) forces to stop the container - when it stops or restarts the container. + - If this is true, the C(lxd_container) forces to stop the instance + when it stops or restarts the instance. required: false default: false type: bool @@ -160,18 +169,18 @@ options: required: false type: str 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 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 >= - 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. - - 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. See the example below. - - You can copy a file in the created container to the localhost - with `command=lxc file pull container_name/dir/filename filename`. + - You can copy a file in the created instance to the localhost + with `command=lxc file pull instance_name/dir/filename filename`. See the first example below. ''' @@ -240,6 +249,7 @@ EXAMPLES = ''' community.general.lxd_container: name: mycontainer state: absent + type: container # An example for restarting a container - hosts: localhost @@ -249,6 +259,7 @@ EXAMPLES = ''' community.general.lxd_container: name: mycontainer state: restarted + type: container # An example for restarting a container using https to connect to the LXD server - hosts: localhost @@ -306,16 +317,36 @@ EXAMPLES = ''' mode: pull alias: ubuntu/xenial/amd64 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 = ''' 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 type: dict sample: {"eth0": ["10.155.92.191"]} old_state: - description: The old state of the container + description: The old state of the instance. returned: when state is started or restarted type: str sample: "stopped" @@ -325,7 +356,7 @@ logs: type: list sample: "(too long to be placed here)" actions: - description: List of actions performed for the container. + description: List of actions performed for the instance. returned: success type: list sample: '["create", "start"]' @@ -384,6 +415,15 @@ class LXDContainerManagement(object): self.addresses = None 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') if self.key_file is None: 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: self.config[attr] = param_val - def _get_container_json(self): + def _get_instance_json(self): 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] ) - def _get_container_state_json(self): + def _get_instance_state_json(self): 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] ) @staticmethod - def _container_json_to_module_state(resp_json): + def _instance_json_to_module_state(resp_json): if resp_json['type'] == 'error': return 'absent' return ANSIBLE_LXD_STATES[resp_json['metadata']['status']] @@ -441,45 +481,45 @@ class LXDContainerManagement(object): body_json = {'action': action, 'timeout': self.timeout} if force_stop: 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['name'] = self.name 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: - self.client.do('POST', '/1.0/containers', config) + self.client.do('POST', self.api_endpoint, config) self.actions.append('create') - def _start_container(self): + def _start_instance(self): self._change_state('start') self.actions.append('start') - def _stop_container(self): + def _stop_instance(self): self._change_state('stop', self.force_stop) self.actions.append('stop') - def _restart_container(self): + def _restart_instance(self): self._change_state('restart', self.force_stop) self.actions.append('restart') - def _delete_container(self): - self.client.do('DELETE', '/1.0/containers/{0}'.format(self.name)) + def _delete_instance(self): + self.client.do('DELETE', '{0}/{1}'.format(self.api_endpoint, self.name)) self.actions.append('delete') - def _freeze_container(self): + def _freeze_instance(self): self._change_state('freeze') self.actions.append('freeze') - def _unfreeze_container(self): + def _unfreeze_instance(self): self._change_state('unfreeze') 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 - resp_json = self._get_container_state_json() + 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 {} @@ -494,7 +534,7 @@ class LXDContainerManagement(object): due = datetime.datetime.now() + datetime.timedelta(seconds=self.timeout) while datetime.datetime.now() < due: time.sleep(1) - addresses = self._container_ipv4_addresses() + addresses = self._instance_ipv4_addresses() if self._has_all_ipv4_addresses(addresses): self.addresses = addresses return @@ -504,72 +544,72 @@ class LXDContainerManagement(object): def _started(self): if self.old_state == 'absent': - self._create_container() - self._start_container() + self._create_instance() + self._start_instance() else: if self.old_state == 'frozen': - self._unfreeze_container() + self._unfreeze_instance() elif self.old_state == 'stopped': - self._start_container() - if self._needs_to_apply_container_configs(): - self._apply_container_configs() + self._start_instance() + if self._needs_to_apply_instance_configs(): + self._apply_instance_configs() if self.wait_for_ipv4_addresses: self._get_addresses() def _stopped(self): if self.old_state == 'absent': - self._create_container() + self._create_instance() else: if self.old_state == 'stopped': - if self._needs_to_apply_container_configs(): - self._start_container() - self._apply_container_configs() - self._stop_container() + if self._needs_to_apply_instance_configs(): + self._start_instance() + self._apply_instance_configs() + self._stop_instance() else: if self.old_state == 'frozen': - self._unfreeze_container() - if self._needs_to_apply_container_configs(): - self._apply_container_configs() - self._stop_container() + self._unfreeze_instance() + if self._needs_to_apply_instance_configs(): + self._apply_instance_configs() + self._stop_instance() def _restarted(self): if self.old_state == 'absent': - self._create_container() - self._start_container() + self._create_instance() + self._start_instance() else: if self.old_state == 'frozen': - self._unfreeze_container() - if self._needs_to_apply_container_configs(): - self._apply_container_configs() - self._restart_container() + self._unfreeze_instance() + if self._needs_to_apply_instance_configs(): + self._apply_instance_configs() + self._restart_instance() if self.wait_for_ipv4_addresses: self._get_addresses() def _destroyed(self): if self.old_state != 'absent': if self.old_state == 'frozen': - self._unfreeze_container() + self._unfreeze_instance() if self.old_state != 'stopped': - self._stop_container() - self._delete_container() + self._stop_instance() + self._delete_instance() def _frozen(self): if self.old_state == 'absent': - self._create_container() - self._start_container() - self._freeze_container() + self._create_instance() + self._start_instance() + self._freeze_instance() else: if self.old_state == 'stopped': - self._start_container() - if self._needs_to_apply_container_configs(): - self._apply_container_configs() - self._freeze_container() + self._start_instance() + if self._needs_to_apply_instance_configs(): + self._apply_instance_configs() + 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: 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_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(): if k not in old_configs: return True @@ -577,7 +617,7 @@ class LXDContainerManagement(object): return True return False 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(): if k not in old_configs: return True @@ -585,39 +625,41 @@ class LXDContainerManagement(object): return True return False else: - old_configs = self.old_container_json['metadata'][key] + old_configs = self.old_instance_json['metadata'][key] return self.config[key] != old_configs - def _needs_to_apply_container_configs(self): + def _needs_to_apply_instance_configs(self): return ( - self._needs_to_change_container_config('architecture') or - self._needs_to_change_container_config('config') or - self._needs_to_change_container_config('ephemeral') or - self._needs_to_change_container_config('devices') or - self._needs_to_change_container_config('profiles') + 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') ) - def _apply_container_configs(self): - old_metadata = self.old_container_json['metadata'] + 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_container_config('architecture'): + + if self._needs_to_change_instance_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(): 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'] - if self._needs_to_change_container_config('devices'): + if self._needs_to_change_instance_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'] - 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): """Run the main method.""" @@ -627,8 +669,8 @@ class LXDContainerManagement(object): self.client.authenticate(self.trust_password) self.ignore_volatile_options = self.module.params.get('ignore_volatile_options') - self.old_container_json = self._get_container_json() - self.old_state = self._container_json_to_module_state(self.old_container_json) + self.old_instance_json = self._get_instance_json() + self.old_state = self._instance_json_to_module_state(self.old_instance_json) action = getattr(self, LXD_ANSIBLE_STATES[self.state]) action() @@ -698,6 +740,11 @@ def main(): type='int', default=30 ), + type=dict( + type='str', + default='container', + choices=['container', 'virtual-machine'], + ), wait_for_ipv4_addresses=dict( type='bool', default=False @@ -736,6 +783,7 @@ def main(): '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"', version='6.0.0', collection_name='community.general') + lxd_manage = LXDContainerManagement(module=module) lxd_manage.run()