1
0
Fork 0
mirror of https://github.com/ansible-collections/community.general.git synced 2024-09-14 20:13:21 +02:00
community.general/plugins/modules/cloud/lxd/lxd_container.py
rchicoli 58eb94fff3
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
2021-11-20 08:20:24 +01:00

792 lines
28 KiB
Python

#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: (c) 2016, Hiroaki Nakamura <hnakamur@gmail.com>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = '''
---
module: lxd_container
short_description: Manage LXD instances
description:
- Management of LXD containers and virtual machines.
author: "Hiroaki Nakamura (@hnakamur)"
options:
name:
description:
- Name of an instance.
type: str
required: true
architecture:
description:
- '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 instance (for example C({"limits.cpu": "2"})).
See U(https://github.com/lxc/lxd/blob/master/doc/rest-api.md#post-1).'
- 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
required: false
ignore_volatile_options:
description:
- If set to C(true), options starting with C(volatile.) are ignored. As a result,
they are reapplied for each execution.
- This default behavior can be changed by setting this option to C(false).
- The current default value C(true) is deprecated since community.general 4.0.0,
and will change to C(false) in community.general 6.0.0.
type: bool
required: false
version_added: 3.7.0
profiles:
description:
- Profile to be used by the instance.
type: list
elements: str
devices:
description:
- '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 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 instance
(e.g. { "type": "image",
"mode": "pull",
"server": "https://images.linuxcontainers.org",
"protocol": "lxd",
"alias": "ubuntu/xenial/amd64" }).'
- 'See U(https://github.com/lxc/lxd/blob/master/doc/rest-api.md#post-1) for complete API documentation.'
- 'Note that C(protocol) accepts two choices: C(lxd) or C(simplestreams).'
required: false
type: dict
state:
choices:
- started
- stopped
- restarted
- absent
- frozen
description:
- Define the state of an instance.
required: false
default: started
type: str
target:
description:
- 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 instance.
- This is also used as a timeout for waiting until IPv4 addresses
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 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 instance
when it stops or restarts the instance.
required: false
default: false
type: bool
url:
description:
- The unix domain socket path or the https URL for the LXD server.
required: false
default: unix:/var/lib/lxd/unix.socket
type: str
snap_url:
description:
- The unix domain socket path when LXD is installed by snap package manager.
required: false
default: unix:/var/snap/lxd/common/lxd/unix.socket
type: str
client_key:
description:
- The client certificate key file path.
- If not specified, it defaults to C(${HOME}/.config/lxc/client.key).
required: false
aliases: [ key_file ]
type: path
client_cert:
description:
- The client certificate file path.
- If not specified, it defaults to C(${HOME}/.config/lxc/client.crt).
required: false
aliases: [ cert_file ]
type: path
trust_password:
description:
- The client trusted password.
- 'You need to set this password on the LXD server before
running this module using the following command:
C(lxc config set core.trust_password <some random password>).
See U(https://www.stgraber.org/2016/04/18/lxd-api-direct-interaction/).'
- If trust_password is set, this module send a request for
authentication before sending any requests.
required: false
type: str
notes:
- 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 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 instance which can
be done with the command module.
- 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 instance to the localhost
with `command=lxc file pull instance_name/dir/filename filename`.
See the first example below.
'''
EXAMPLES = '''
# An example for creating a Ubuntu container and install python
- hosts: localhost
connection: local
tasks:
- name: Create a started container
community.general.lxd_container:
name: mycontainer
ignore_volatile_options: true
state: started
source:
type: image
mode: pull
server: https://images.linuxcontainers.org
protocol: lxd # if you get a 404, try setting protocol: simplestreams
alias: ubuntu/xenial/amd64
profiles: ["default"]
wait_for_ipv4_addresses: true
timeout: 600
- name: Check python is installed in container
delegate_to: mycontainer
ansible.builtin.raw: dpkg -s python
register: python_install_check
failed_when: python_install_check.rc not in [0, 1]
changed_when: false
- name: Install python in container
delegate_to: mycontainer
ansible.builtin.raw: apt-get install -y python
when: python_install_check.rc == 1
# An example for creating an Ubuntu 14.04 container using an image fingerprint.
# This requires changing 'server' and 'protocol' key values, replacing the
# 'alias' key with with 'fingerprint' and supplying an appropriate value that
# matches the container image you wish to use.
- hosts: localhost
connection: local
tasks:
- name: Create a started container
community.general.lxd_container:
name: mycontainer
ignore_volatile_options: true
state: started
source:
type: image
mode: pull
# Provides current (and older) Ubuntu images with listed fingerprints
server: https://cloud-images.ubuntu.com/releases
# Protocol used by 'ubuntu' remote (as shown by 'lxc remote list')
protocol: simplestreams
# This provides an Ubuntu 14.04 LTS amd64 image from 20150814.
fingerprint: e9a8bdfab6dc
profiles: ["default"]
wait_for_ipv4_addresses: true
timeout: 600
# An example for deleting a container
- hosts: localhost
connection: local
tasks:
- name: Delete a container
community.general.lxd_container:
name: mycontainer
state: absent
type: container
# An example for restarting a container
- hosts: localhost
connection: local
tasks:
- name: Restart a container
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
connection: local
tasks:
- name: Restart a container
community.general.lxd_container:
url: https://127.0.0.1:8443
# These client_cert and client_key values are equal to the default values.
#client_cert: "{{ lookup('env', 'HOME') }}/.config/lxc/client.crt"
#client_key: "{{ lookup('env', 'HOME') }}/.config/lxc/client.key"
trust_password: mypassword
name: mycontainer
state: restarted
# Note your container must be in the inventory for the below example.
#
# [containers]
# mycontainer ansible_connection=lxd
#
- hosts:
- mycontainer
tasks:
- name: Copy /etc/hosts in the created container to localhost with name "mycontainer-hosts"
ansible.builtin.fetch:
src: /etc/hosts
dest: /tmp/mycontainer-hosts
flat: true
# An example for LXD cluster deployments. This example will create two new container on specific
# nodes - 'node01' and 'node02'. In 'target:', 'node01' and 'node02' are names of LXD cluster
# members that LXD cluster recognizes, not ansible inventory names, see: 'lxc cluster list'.
# LXD API calls can be made to any LXD member, in this example, we send API requests to
#'node01.example.com', which matches ansible inventory name.
- hosts: node01.example.com
tasks:
- name: Create LXD container
community.general.lxd_container:
name: new-container-1
ignore_volatile_options: true
state: started
source:
type: image
mode: pull
alias: ubuntu/xenial/amd64
target: node01
- name: Create container on another node
community.general.lxd_container:
name: new-container-2
ignore_volatile_options: true
state: started
source:
type: image
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 instance.
returned: when state is started or restarted
type: dict
sample: {"eth0": ["10.155.92.191"]}
old_state:
description: The old state of the instance.
returned: when state is started or restarted
type: str
sample: "stopped"
logs:
description: The logs of requests and responses.
returned: when ansible-playbook is invoked with -vvvv.
type: list
sample: "(too long to be placed here)"
actions:
description: List of actions performed for the instance.
returned: success
type: list
sample: '["create", "start"]'
'''
import datetime
import os
import time
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.lxd import LXDClient, LXDClientException
from ansible.module_utils.six.moves.urllib.parse import urlencode
# LXD_ANSIBLE_STATES is a map of states that contain values of methods used
# when a particular state is evoked.
LXD_ANSIBLE_STATES = {
'started': '_started',
'stopped': '_stopped',
'restarted': '_restarted',
'absent': '_destroyed',
'frozen': '_frozen'
}
# ANSIBLE_LXD_STATES is a map of states of lxd containers to the Ansible
# lxc_container module state parameter value.
ANSIBLE_LXD_STATES = {
'Running': 'started',
'Stopped': 'stopped',
'Frozen': 'frozen',
}
# ANSIBLE_LXD_DEFAULT_URL is a default value of the lxd endpoint
ANSIBLE_LXD_DEFAULT_URL = 'unix:/var/lib/lxd/unix.socket'
# CONFIG_PARAMS is a list of config attribute names.
CONFIG_PARAMS = [
'architecture', 'config', 'devices', 'ephemeral', 'profiles', 'source'
]
class LXDContainerManagement(object):
def __init__(self, module):
"""Management of LXC containers via Ansible.
:param module: Processed Ansible Module.
:type module: ``object``
"""
self.module = module
self.name = self.module.params['name']
self._build_config()
self.state = self.module.params['state']
self.timeout = self.module.params['timeout']
self.wait_for_ipv4_addresses = self.module.params['wait_for_ipv4_addresses']
self.force_stop = self.module.params['force_stop']
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'])
self.cert_file = self.module.params.get('client_cert')
if self.cert_file is None:
self.cert_file = '{0}/.config/lxc/client.crt'.format(os.environ['HOME'])
self.debug = self.module._verbosity >= 4
try:
if self.module.params['url'] != ANSIBLE_LXD_DEFAULT_URL:
self.url = self.module.params['url']
elif os.path.exists(self.module.params['snap_url'].replace('unix:', '')):
self.url = self.module.params['snap_url']
else:
self.url = self.module.params['url']
except Exception as e:
self.module.fail_json(msg=e.msg)
try:
self.client = LXDClient(
self.url, key_file=self.key_file, cert_file=self.cert_file,
debug=self.debug
)
except LXDClientException as e:
self.module.fail_json(msg=e.msg)
self.trust_password = self.module.params.get('trust_password', None)
self.actions = []
def _build_config(self):
self.config = {}
for attr in CONFIG_PARAMS:
param_val = self.module.params.get(attr, None)
if param_val is not None:
self.config[attr] = param_val
def _get_instance_json(self):
return self.client.do(
'GET', '{0}/{1}'.format(self.api_endpoint, self.name),
ok_error_codes=[404]
)
def _get_instance_state_json(self):
return self.client.do(
'GET', '{0}/{1}/state'.format(self.api_endpoint, self.name),
ok_error_codes=[404]
)
@staticmethod
def _instance_json_to_module_state(resp_json):
if resp_json['type'] == 'error':
return 'absent'
return ANSIBLE_LXD_STATES[resp_json['metadata']['status']]
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', '{0}/{1}/state'.format(self.api_endpoint, self.name), body_json=body_json)
def _create_instance(self):
config = self.config.copy()
config['name'] = self.name
if self.target:
self.client.do('POST', '{0}?{1}'.format(self.api_endpoint, urlencode(dict(target=self.target))), config)
else:
self.client.do('POST', self.api_endpoint, config)
self.actions.append('create')
def _start_instance(self):
self._change_state('start')
self.actions.append('start')
def _stop_instance(self):
self._change_state('stop', self.force_stop)
self.actions.append('stop')
def _restart_instance(self):
self._change_state('restart', self.force_stop)
self.actions.append('restart')
def _delete_instance(self):
self.client.do('DELETE', '{0}/{1}'.format(self.api_endpoint, self.name))
self.actions.append('delete')
def _freeze_instance(self):
self._change_state('freeze')
self.actions.append('freeze')
def _unfreeze_instance(self):
self._change_state('unfreeze')
self.actions.append('unfreez')
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 {}
return addresses
@staticmethod
def _has_all_ipv4_addresses(addresses):
return len(addresses) > 0 and all(len(v) > 0 for v in addresses.values())
def _get_addresses(self):
try:
due = datetime.datetime.now() + datetime.timedelta(seconds=self.timeout)
while datetime.datetime.now() < due:
time.sleep(1)
addresses = self._instance_ipv4_addresses()
if self._has_all_ipv4_addresses(addresses):
self.addresses = addresses
return
except LXDClientException as e:
e.msg = 'timeout for getting IPv4 addresses'
raise
def _started(self):
if self.old_state == 'absent':
self._create_instance()
self._start_instance()
else:
if self.old_state == 'frozen':
self._unfreeze_instance()
elif self.old_state == 'stopped':
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_instance()
else:
if self.old_state == 'stopped':
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_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_instance()
self._start_instance()
else:
if self.old_state == 'frozen':
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_instance()
if self.old_state != 'stopped':
self._stop_instance()
self._delete_instance()
def _frozen(self):
if self.old_state == 'absent':
self._create_instance()
self._start_instance()
self._freeze_instance()
else:
if self.old_state == 'stopped':
self._start_instance()
if self._needs_to_apply_instance_configs():
self._apply_instance_configs()
self._freeze_instance()
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())
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]
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')
)
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']
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."""
try:
if self.trust_password is not None:
self.client.authenticate(self.trust_password)
self.ignore_volatile_options = self.module.params.get('ignore_volatile_options')
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()
state_changed = len(self.actions) > 0
result_json = {
'log_verbosity': self.module._verbosity,
'changed': state_changed,
'old_state': self.old_state,
'actions': self.actions
}
if self.client.debug:
result_json['logs'] = self.client.logs
if self.addresses is not None:
result_json['addresses'] = self.addresses
self.module.exit_json(**result_json)
except LXDClientException as e:
state_changed = len(self.actions) > 0
fail_params = {
'msg': e.msg,
'changed': state_changed,
'actions': self.actions
}
if self.client.debug:
fail_params['logs'] = e.kwargs['logs']
self.module.fail_json(**fail_params)
def main():
"""Ansible Main module."""
module = AnsibleModule(
argument_spec=dict(
name=dict(
type='str',
required=True
),
architecture=dict(
type='str',
),
config=dict(
type='dict',
),
ignore_volatile_options=dict(
type='bool',
),
devices=dict(
type='dict',
),
ephemeral=dict(
type='bool',
),
profiles=dict(
type='list',
elements='str',
),
source=dict(
type='dict',
),
state=dict(
choices=list(LXD_ANSIBLE_STATES.keys()),
default='started'
),
target=dict(
type='str',
),
timeout=dict(
type='int',
default=30
),
type=dict(
type='str',
default='container',
choices=['container', 'virtual-machine'],
),
wait_for_ipv4_addresses=dict(
type='bool',
default=False
),
force_stop=dict(
type='bool',
default=False
),
url=dict(
type='str',
default=ANSIBLE_LXD_DEFAULT_URL
),
snap_url=dict(
type='str',
default='unix:/var/snap/lxd/common/lxd/unix.socket'
),
client_key=dict(
type='path',
aliases=['key_file']
),
client_cert=dict(
type='path',
aliases=['cert_file']
),
trust_password=dict(type='str', no_log=True)
),
supports_check_mode=False,
)
if module.params['ignore_volatile_options'] is None:
module.params['ignore_volatile_options'] = True
module.deprecate(
'If the keyword "volatile" is used in a playbook in the config'
'section, a "changed" message will appear with every run, even without a change'
'to the playbook.'
'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()
if __name__ == '__main__':
main()