saltstack/salt

View on GitHub
salt/states/lxd_container.py

Summary

Maintainability
F
1 wk
Test Coverage
# -*- coding: utf-8 -*-
'''
Manage LXD containers.

.. versionadded:: 2019.2.0

.. note:

    - :ref:`pylxd` version 2 is required to let this work,
      currently only available via pip.

        To install on Ubuntu:

        $ apt-get install libssl-dev python-pip
        $ pip install -U pylxd

    - you need lxd installed on the minion
      for the init() and version() methods.

    - for the config_get() and config_get() methods
      you need to have lxd-client installed.

.. _pylxd: https://github.com/lxc/pylxd/blob/master/doc/source/installation.rst

:maintainer: René Jochum <rene@jochums.at>
:maturity: new
:depends: python-pylxd
:platform: Linux
'''

# Import python libs
from __future__ import absolute_import, print_function, unicode_literals

# Import salt libs
from salt.exceptions import CommandExecutionError
from salt.exceptions import SaltInvocationError
import salt.ext.six as six
from salt.ext.six.moves import map

__docformat__ = 'restructuredtext en'

__virtualname__ = 'lxd_container'

# Keep in sync with: https://github.com/lxc/lxd/blob/master/shared/status.go
CONTAINER_STATUS_RUNNING = 103
CONTAINER_STATUS_FROZEN = 110
CONTAINER_STATUS_STOPPED = 102


def __virtual__():
    '''
    Only load if the lxd module is available in __salt__
    '''
    return __virtualname__ if 'lxd.version' in __salt__ else False


def present(name,
            running=None,
            source=None,
            profiles=None,
            config=None,
            devices=None,
            architecture='x86_64',
            ephemeral=False,
            restart_on_change=False,
            remote_addr=None,
            cert=None,
            key=None,
            verify_cert=True):
    '''
    Create the named container if it does not exist

    name
        The name of the container to be created

    running : None
        * If ``True``, ensure that the container is running
        * If ``False``, ensure that the container is stopped
        * If ``None``, do nothing with regards to the running state of the
          container

    source : None
        Can be either a string containing an image alias:

        .. code-block:: none

             "xenial/amd64"

        or an dict with type "image" with alias:

        .. code-block:: python

            {"type": "image",
             "alias": "xenial/amd64"}

        or image with "fingerprint":

        .. code-block:: python

            {"type": "image",
             "fingerprint": "SHA-256"}

        or image with "properties":

        .. code-block:: python

            {"type": "image",
             "properties": {
                "os": "ubuntu",
                "release": "14.04",
                "architecture": "x86_64"
             }}

        or none:

        .. code-block:: python

            {"type": "none"}

        or copy:

        .. code-block:: python

            {"type": "copy",
             "source": "my-old-container"}

    profiles : ['default']
        List of profiles to apply on this container

    config :
        A config dict or None (None = unset).

        Can also be a list:

        .. code-block:: python

            [{'key': 'boot.autostart', 'value': 1},
             {'key': 'security.privileged', 'value': '1'}]

    devices :
        A device dict or None (None = unset).

    architecture : 'x86_64'
        Can be one of the following:

        * unknown
        * i686
        * x86_64
        * armv7l
        * aarch64
        * ppc
        * ppc64
        * ppc64le
        * s390x

    ephemeral : False
        Destroy this container after stop?

    restart_on_change : False
        Restart the container when we detect changes on the config or
        its devices?

    remote_addr :
        An URL to a remote Server, you also have to give cert and key if you
        provide remote_addr!

        Examples:
            https://myserver.lan:8443
            /var/lib/mysocket.sock

    cert :
        PEM Formatted SSL Zertifikate.

        Examples:
            ~/.config/lxc/client.crt

    key :
        PEM Formatted SSL Key.

        Examples:
            ~/.config/lxc/client.key

    verify_cert : True
        Wherever to verify the cert, this is by default True
        but in the most cases you want to set it off as LXD
        normaly uses self-signed certificates.
    '''
    if profiles is None:
        profiles = ['default']

    if source is None:
        source = {}

    ret = {
        'name': name,
        'running': running,
        'profiles': profiles,
        'source': source,
        'config': config,
        'devices': devices,
        'architecture': architecture,
        'ephemeral': ephemeral,
        'restart_on_change': restart_on_change,
        'remote_addr': remote_addr,
        'cert': cert,
        'key': key,
        'verify_cert': verify_cert,

        'changes': {}
    }

    container = None
    try:
        container = __salt__['lxd.container_get'](
            name, remote_addr, cert, key, verify_cert, _raw=True
        )
    except CommandExecutionError as e:
        return _error(ret, six.text_type(e))
    except SaltInvocationError as e:
        # Profile not found
        pass

    if container is None:
        if __opts__['test']:
            # Test is on, just return that we would create the container
            msg = 'Would create the container "{0}"'.format(name)
            ret['changes'] = {
                'created': msg
            }
            if running is True:
                msg = msg + ' and start it.'
                ret['changes']['started'] = (
                    'Would start the container "{0}"'.format(name)
                )

            ret['changes'] = {'created': msg}
            return _unchanged(ret, msg)

        # create the container
        try:
            __salt__['lxd.container_create'](
                name,
                source,
                profiles,
                config,
                devices,
                architecture,
                ephemeral,
                True,  # Wait
                remote_addr,
                cert,
                key,
                verify_cert
            )
        except CommandExecutionError as e:
            return _error(ret, six.text_type(e))

        msg = 'Created the container "{0}"'.format(name)
        ret['changes'] = {
            'created': msg
        }

        if running is True:
            try:
                __salt__['lxd.container_start'](
                    name,
                    remote_addr,
                    cert,
                    key,
                    verify_cert
                )
            except CommandExecutionError as e:
                return _error(ret, six.text_type(e))

            msg = msg + ' and started it.'
            ret['changes'] = {
                'started': 'Started the container "{0}"'.format(name)
            }

        return _success(ret, msg)

    # Container exists, lets check for differences
    new_profiles = set(map(six.text_type, profiles))
    old_profiles = set(map(six.text_type, container.profiles))

    container_changed = False

    profile_changes = []
    # Removed profiles
    for k in old_profiles.difference(new_profiles):
        if not __opts__['test']:
            profile_changes.append('Removed profile "{0}"'.format(k))
            old_profiles.discard(k)
        else:
            profile_changes.append('Would remove profile "{0}"'.format(k))

    # Added profiles
    for k in new_profiles.difference(old_profiles):
        if not __opts__['test']:
            profile_changes.append('Added profile "{0}"'.format(k))
            old_profiles.add(k)
        else:
            profile_changes.append('Would add profile "{0}"'.format(k))

    if profile_changes:
        container_changed = True
        ret['changes']['profiles'] = profile_changes
        container.profiles = list(old_profiles)

    # Config and devices changes
    config, devices = __salt__['lxd.normalize_input_values'](
        config,
        devices
    )
    changes = __salt__['lxd.sync_config_devices'](
        container, config, devices, __opts__['test']
    )
    if changes:
        container_changed = True
        ret['changes'].update(changes)

    is_running = \
        container.status_code == CONTAINER_STATUS_RUNNING

    if not __opts__['test']:
        try:
            __salt__['lxd.pylxd_save_object'](container)
        except CommandExecutionError as e:
            return _error(ret, six.text_type(e))

    if running != is_running:
        if running is True:
            if __opts__['test']:
                changes['running'] = 'Would start the container'
                return _unchanged(
                    ret,
                    ('Container "{0}" would get changed '
                     'and started.').format(name)
                )
            else:
                container.start(wait=True)
                changes['running'] = 'Started the container'

        elif running is False:
            if __opts__['test']:
                changes['stopped'] = 'Would stopped the container'
                return _unchanged(
                    ret,
                    ('Container "{0}" would get changed '
                     'and stopped.').format(name)
                )
            else:
                container.stop(wait=True)
                changes['stopped'] = 'Stopped the container'

    if ((running is True or running is None) and
            is_running and
            restart_on_change and
            container_changed):

        if __opts__['test']:
            changes['restarted'] = 'Would restart the container'
            return _unchanged(
                ret,
                'Would restart the container "{0}"'.format(name)
            )
        else:
            container.restart(wait=True)
            changes['restarted'] = (
                'Container "{0}" has been restarted'.format(name)
            )
            return _success(
                ret,
                'Container "{0}" has been restarted'.format(name)
            )

    if not container_changed:
        return _success(ret, 'No changes')

    if __opts__['test']:
        return _unchanged(
            ret,
            'Container "{0}" would get changed.'.format(name)
        )

    return _success(ret, '{0} changes'.format(len(ret['changes'].keys())))


def absent(name,
           stop=False,
           remote_addr=None,
           cert=None,
           key=None,
           verify_cert=True):
    '''
    Ensure a LXD container is not present, destroying it if present

    name :
        The name of the container to destroy

    stop :
        stop before destroying
        default: false

    remote_addr :
        An URL to a remote Server, you also have to give cert and key if you
        provide remote_addr!

        Examples:
            https://myserver.lan:8443
            /var/lib/mysocket.sock

    cert :
        PEM Formatted SSL Zertifikate.

        Examples:
            ~/.config/lxc/client.crt

    key :
        PEM Formatted SSL Key.

        Examples:
            ~/.config/lxc/client.key

    verify_cert : True
        Wherever to verify the cert, this is by default True
        but in the most cases you want to set it off as LXD
        normaly uses self-signed certificates.
    '''
    ret = {
        'name': name,
        'stop': stop,

        'remote_addr': remote_addr,
        'cert': cert,
        'key': key,
        'verify_cert': verify_cert,

        'changes': {}
    }

    try:
        container = __salt__['lxd.container_get'](
            name, remote_addr, cert, key, verify_cert, _raw=True
        )
    except CommandExecutionError as e:
        return _error(ret, six.text_type(e))
    except SaltInvocationError as e:
        # Container not found
        return _success(ret, 'Container "{0}" not found.'.format(name))

    if __opts__['test']:
        ret['changes'] = {
            'removed':
            'Container "{0}" would get deleted.'.format(name)
        }
        return _unchanged(ret, ret['changes']['removed'])

    if stop and container.status_code == CONTAINER_STATUS_RUNNING:
        container.stop(wait=True)

    container.delete(wait=True)

    ret['changes']['deleted'] = \
        'Container "{0}" has been deleted.'.format(name)
    return _success(ret, ret['changes']['deleted'])


def running(name,
            restart=False,
            remote_addr=None,
            cert=None,
            key=None,
            verify_cert=True):
    '''
    Ensure a LXD container is running and restart it if restart is True

    name :
        The name of the container to start/restart.

    restart :
        restart the container if it is already started.

    remote_addr :
        An URL to a remote Server, you also have to give cert and key if you
        provide remote_addr!

        Examples:
            https://myserver.lan:8443
            /var/lib/mysocket.sock

    cert :
        PEM Formatted SSL Zertifikate.

        Examples:
            ~/.config/lxc/client.crt

    key :
        PEM Formatted SSL Key.

        Examples:
            ~/.config/lxc/client.key

    verify_cert : True
        Wherever to verify the cert, this is by default True
        but in the most cases you want to set it off as LXD
        normaly uses self-signed certificates.
    '''
    ret = {
        'name': name,
        'restart': restart,

        'remote_addr': remote_addr,
        'cert': cert,
        'key': key,
        'verify_cert': verify_cert,

        'changes': {}
    }

    try:
        container = __salt__['lxd.container_get'](
            name, remote_addr, cert, key, verify_cert, _raw=True
        )
    except CommandExecutionError as e:
        return _error(ret, six.text_type(e))
    except SaltInvocationError as e:
        # Container not found
        return _error(ret, 'Container "{0}" not found'.format(name))

    is_running = container.status_code == CONTAINER_STATUS_RUNNING

    if is_running:
        if not restart:
            return _success(
                ret,
                'The container "{0}" is already running'.format(name)
            )
        else:
            if __opts__['test']:
                ret['changes']['restarted'] = (
                    'Would restart the container "{0}"'.format(name)
                )
                return _unchanged(ret, ret['changes']['restarted'])
            else:
                container.restart(wait=True)
                ret['changes']['restarted'] = (
                    'Restarted the container "{0}"'.format(name)
                )
                return _success(ret, ret['changes']['restarted'])

    if __opts__['test']:
        ret['changes']['started'] = (
            'Would start the container "{0}"'.format(name)
        )
        return _unchanged(ret, ret['changes']['started'])

    container.start(wait=True)
    ret['changes']['started'] = (
        'Started the container "{0}"'.format(name)
    )
    return _success(ret, ret['changes']['started'])


def frozen(name,
           start=True,
           remote_addr=None,
           cert=None,
           key=None,
           verify_cert=True):
    '''
    Ensure a LXD container is frozen, start and freeze it if start is true

    name :
        The name of the container to freeze

    start :
        start and freeze it

    remote_addr :
        An URL to a remote Server, you also have to give cert and key if you
        provide remote_addr!

        Examples:
            https://myserver.lan:8443
            /var/lib/mysocket.sock

    cert :
        PEM Formatted SSL Zertifikate.

        Examples:
            ~/.config/lxc/client.crt

    key :
        PEM Formatted SSL Key.

        Examples:
            ~/.config/lxc/client.key

    verify_cert : True
        Wherever to verify the cert, this is by default True
        but in the most cases you want to set it off as LXD
        normaly uses self-signed certificates.
    '''
    ret = {
        'name': name,
        'start': start,

        'remote_addr': remote_addr,
        'cert': cert,
        'key': key,
        'verify_cert': verify_cert,

        'changes': {}
    }

    try:
        container = __salt__['lxd.container_get'](
            name, remote_addr, cert, key, verify_cert, _raw=True
        )
    except CommandExecutionError as e:
        return _error(ret, six.text_type(e))
    except SaltInvocationError as e:
        # Container not found
        return _error(ret, 'Container "{0}" not found'.format(name))

    if container.status_code == CONTAINER_STATUS_FROZEN:
        return _success(ret, 'Container "{0}" is alredy frozen'.format(name))

    is_running = container.status_code == CONTAINER_STATUS_RUNNING

    if not is_running and not start:
        return _error(ret, (
            'Container "{0}" is not running and start is False, '
            'cannot freeze it').format(name)
        )

    elif not is_running and start:
        if __opts__['test']:
            ret['changes']['started'] = (
                'Would start the container "{0}" and freeze it after'
                .format(name)
            )
            return _unchanged(ret, ret['changes']['started'])
        else:
            container.start(wait=True)
            ret['changes']['started'] = (
                'Start the container "{0}"'
                .format(name)
            )

    if __opts__['test']:
        ret['changes']['frozen'] = (
            'Would freeze the container "{0}"'.format(name)
        )
        return _unchanged(ret, ret['changes']['frozen'])

    container.freeze(wait=True)
    ret['changes']['frozen'] = (
        'Froze the container "{0}"'.format(name)
    )

    return _success(ret, ret['changes']['frozen'])


def stopped(name,
            kill=False,
            remote_addr=None,
            cert=None,
            key=None,
            verify_cert=True):
    '''
    Ensure a LXD container is stopped, kill it if kill is true else stop it

    name :
        The name of the container to stop

    kill :
        kill if true

    remote_addr :
        An URL to a remote Server, you also have to give cert and key if you
        provide remote_addr!

        Examples:
            https://myserver.lan:8443
            /var/lib/mysocket.sock

    cert :
        PEM Formatted SSL Zertifikate.

        Examples:
            ~/.config/lxc/client.crt

    key :
        PEM Formatted SSL Key.

        Examples:
            ~/.config/lxc/client.key

    verify_cert : True
        Wherever to verify the cert, this is by default True
        but in the most cases you want to set it off as LXD
        normaly uses self-signed certificates.
    '''
    ret = {
        'name': name,
        'kill': kill,

        'remote_addr': remote_addr,
        'cert': cert,
        'key': key,
        'verify_cert': verify_cert,

        'changes': {}
    }

    try:
        container = __salt__['lxd.container_get'](
            name, remote_addr, cert, key, verify_cert, _raw=True
        )
    except CommandExecutionError as e:
        return _error(ret, six.text_type(e))
    except SaltInvocationError as e:
        # Container not found
        return _error(ret, 'Container "{0}" not found'.format(name))

    if container.status_code == CONTAINER_STATUS_STOPPED:
        return _success(ret, 'Container "{0}" is already stopped'.format(name))

    if __opts__['test']:
        ret['changes']['stopped'] = \
            'Would stop the container "{0}"'.format(name)
        return _unchanged(ret, ret['changes']['stopped'])

    container.stop(force=kill, wait=True)
    ret['changes']['stopped'] = \
        'Stopped the container "{0}"'.format(name)
    return _success(ret, ret['changes']['stopped'])


def migrated(name,
             remote_addr,
             cert,
             key,
             verify_cert,
             src_remote_addr,
             stop_and_start=False,
             src_cert=None,
             src_key=None,
             src_verify_cert=None):
    ''' Ensure a container is migrated to another host

    If the container is running, it either must be shut down
    first (use stop_and_start=True) or criu must be installed
    on the source and destination machines.

    For this operation both certs need to be authenticated,
    use :mod:`lxd.authenticate <salt.states.lxd.authenticate`
    to authenticate your cert(s).

    name :
        The container to migrate

    remote_addr :
        An URL to the destination remote Server

        Examples:
            https://myserver.lan:8443
            /var/lib/mysocket.sock

    cert :
        PEM Formatted SSL Zertifikate.

        Examples:
            ~/.config/lxc/client.crt

    key :
        PEM Formatted SSL Key.

        Examples:
            ~/.config/lxc/client.key

    verify_cert : True
        Wherever to verify the cert, this is by default True
        but in the most cases you want to set it off as LXD
        normaly uses self-signed certificates.

    src_remote_addr :
        An URL to the source remote Server

        Examples:
            https://myserver.lan:8443
            /var/lib/mysocket.sock

    stop_and_start:
        Stop before migrating and start after

    src_cert :
        PEM Formatted SSL Zertifikate, if None we copy "cert"

        Examples:
            ~/.config/lxc/client.crt

    src_key :
        PEM Formatted SSL Key, if None we copy "key"

        Examples:
            ~/.config/lxc/client.key

    src_verify_cert :
        Wherever to verify the cert, if None we copy "verify_cert"
    '''
    ret = {
        'name': name,

        'remote_addr': remote_addr,
        'cert': cert,
        'key': key,
        'verify_cert': verify_cert,

        'src_remote_addr': src_remote_addr,
        'src_and_start': stop_and_start,
        'src_cert': src_cert,
        'src_key': src_key,

        'changes': {}
    }

    dest_container = None
    try:
        dest_container = __salt__['lxd.container_get'](
            name, remote_addr, cert, key,
            verify_cert, _raw=True
        )
    except CommandExecutionError as e:
        return _error(ret, six.text_type(e))
    except SaltInvocationError as e:
        # Destination container not found
        pass

    if dest_container is not None:
        return _success(
            ret,
            'Container "{0}" exists on the destination'.format(name)
        )

    if src_verify_cert is None:
        src_verify_cert = verify_cert

    try:
        __salt__['lxd.container_get'](
            name, src_remote_addr, src_cert, src_key, src_verify_cert, _raw=True
        )
    except CommandExecutionError as e:
        return _error(ret, six.text_type(e))
    except SaltInvocationError as e:
        # Container not found
        return _error(ret, 'Source Container "{0}" not found'.format(name))

    if __opts__['test']:
        ret['changes']['migrated'] = (
            'Would migrate the container "{0}" from "{1}" to "{2}"'
        ).format(name, src_remote_addr, remote_addr)
        return _unchanged(ret, ret['changes']['migrated'])

    try:
        __salt__['lxd.container_migrate'](
            name, stop_and_start, remote_addr, cert, key,
            verify_cert, src_remote_addr, src_cert, src_key, src_verify_cert
        )
    except CommandExecutionError as e:
        return _error(ret, six.text_type(e))

    ret['changes']['migrated'] = (
        'Migrated the container "{0}" from "{1}" to "{2}"'
    ).format(name, src_remote_addr, remote_addr)
    return _success(ret, ret['changes']['migrated'])


def _success(ret, success_msg):
    ret['result'] = True
    ret['comment'] = success_msg
    if 'changes' not in ret:
        ret['changes'] = {}
    return ret


def _unchanged(ret, msg):
    ret['result'] = None
    ret['comment'] = msg
    if 'changes' not in ret:
        ret['changes'] = {}
    return ret


def _error(ret, err_msg):
    ret['result'] = False
    ret['comment'] = err_msg
    if 'changes' not in ret:
        ret['changes'] = {}
    return ret