# -*- coding: utf-8 -*-
# $OpenBSD: vmctl.py,v 1.3 2018/05/24 16:59:40 jasper Exp $
'''
Manage vms running on the OpenBSD VMM hypervisor using vmctl(8).

.. versionadded:: Fluorine

:codeauthor: :email:`Jasper Lievisse Adriaanse <jasper@openbsd.org>`

.. note::

    This module requires the `vmd` service to be running on the OpenBSD
    target machine.
'''

from __future__ import absolute_import

# Import python libs
import logging
import re

# Imoprt salt libs:
import salt.utils.path
from salt.exceptions import (CommandExecutionError, SaltInvocationError)
from salt.ext.six.moves import zip

log = logging.getLogger(__name__)


def __virtual__():
    '''
    Only works on OpenBSD with vmctl(8) present.
    '''
    if __grains__['os'] == 'OpenBSD' and salt.utils.path.which('vmctl'):
        return True

    return (False, 'The vmm execution module cannot be loaded: either the system is not OpenBSD or the vmctl binary was not found')


def _id_to_name(id):
    '''
    Lookup the name associated with a VM id.
    '''
    vm = status(id=id)
    if vm == {}:
        return None
    else:
        return vm['name']


def create_disk(name, size):
    '''
    Create a VMM disk with the specified `name` and `size`.

    size:
        Size in megabytes, or use a specifier such as M, G, T.

    CLI Example:

    .. code-block:: bash

        salt '*' vmctl.create_disk /path/to/disk.img size=10G
    '''
    ret = False
    cmd = 'vmctl create {0} -s {1}'.format(name, size)

    result = __salt__['cmd.run_all'](cmd,
                                     output_loglevel='trace',
                                     python_shell=False)

    if result['retcode'] == 0:
        ret = True
    else:
        raise CommandExecutionError(
            'Problem encountered creating disk image',
            info={'errors': [result['stderr']], 'changes': ret}
        )

    return ret


def load(path):
    '''
    Load additional configuration from the specified file.

    path
        Path to the configuration file.

    CLI Example:

    .. code-block:: bash

        salt '*' vmctl.load path=/etc/vm.switches.conf
    '''
    ret = False
    cmd = 'vmctl load {0}'.format(path)
    result = __salt__['cmd.run_all'](cmd,
                                     output_loglevel='trace',
                                     python_shell=False)
    if result['retcode'] == 0:
        ret = True
    else:
        raise CommandExecutionError(
            'Problem encountered running vmctl',
            info={'errors': [result['stderr']], 'changes': ret}
        )

    return ret


def reload():
    '''
    Remove all stopped VMs and reload configuration from the default configuration file.

    CLI Example:

    .. code-block:: bash

        salt '*' vmctl.reload
    '''
    ret = False
    cmd = 'vmctl reload'
    result = __salt__['cmd.run_all'](cmd,
                                     output_loglevel='trace',
                                     python_shell=False)
    if result['retcode'] == 0:
        ret = True
    else:
        raise CommandExecutionError(
            'Problem encountered running vmctl',
            info={'errors': [result['stderr']], 'changes': ret}
        )

    return ret


def reset(all=False, vms=False, switches=False):
    '''
    Reset the running state of VMM or a subsystem.

    all:
        Reset the running state.

    switches:
        Reset the configured switches.

    vms:
        Reset and terminate all VMs.


    CLI Example:

    .. code-block:: bash

        salt '*' vmctl.reset all=True
    '''
    ret = False
    cmd = ['vmctl', 'reset']

    if all:
        cmd.append('all')
    elif vms:
        cmd.append('vms')
    elif switches:
        cmd.append('switches')

    result = __salt__['cmd.run_all'](cmd,
                                     output_loglevel='trace',
                                     python_shell=False)
    if result['retcode'] == 0:
        ret = True
    else:
        raise CommandExecutionError(
            'Problem encountered running vmctl',
            info={'errors': [result['stderr']], 'changes': ret}
        )

    return ret


def start(name=None, id=None, bootpath=None, disk=None, disks=None, local_iface=False,
          memory=None, nics=0, switch=None):
    '''
    Starts a VM defined by the specified parameters.
    When both a name and id are provided, the id is ignored.

    name:
        Name of the defined VM.

    id:
        VM id.

    bootpath:
        Path to a kernel or BIOS image to load.

    disk:
        Path to a single disk to use.

    disks:
        List of multiple disks to use.

    local_iface:
        Whether to add a local network interface. See "LOCAL INTERFACES"
        in the vmctl(8) manual page for more information.

    memory:
        Memory size of the VM specified in megabytes.

    switch:
        Add a network interface that is attached to the specified
        virtual switch on the host.

    CLI Example:

    .. code-block:: bash

        salt '*' vmctl.start 2   # start VM with id 2
        salt '*' vmctl.start name=web1 bootpath='/bsd.rd' nics=2 memory=512M disk='/disk.img'
    '''
    ret = {'changes': False, 'console': None}
    cmd = ['vmctl', 'start']

    if not (name or id):
        raise SaltInvocationError('Must provide either "name" or "id"')
    elif name:
        cmd.append(name)
    else:
        cmd.append(id)
        name = _id_to_name(id)

    if nics > 0:
        cmd.append('-i {0}'.format(nics))

    # Paths cannot be appended as otherwise the inserted whitespace is treated by
    # vmctl as being part of the path.
    if bootpath:
        cmd.extend(['-b', bootpath])

    if memory:
        cmd.append('-m {0}'.format(memory))

    if switch:
        cmd.append('-n {0}'.format(switch))

    if local_iface:
        cmd.append('-L')

    if disk and (disks and len(disks) > 0):
        raise SaltInvocationError('Must provide either "disks" or "disk"')

    if disk:
        cmd.extend(['-d', disk])

    if disks and len(disks) > 0:
        cmd.extend(['-d', x] for x in disks)

    # Before attempting to define a new VM, make sure it doesn't already exist.
    # Otherwise return to indicate nothing was changed.
    if len(cmd) > 3:
        vmstate = status(name)
        if vmstate:
            ret['comment'] = 'VM already exists and cannot be redefined'
            return ret

    result = __salt__['cmd.run_all'](cmd,
                                     output_loglevel='trace',
                                     python_shell=False)

    if result['retcode'] == 0:
        ret['changes'] = True
        m = re.match(r'.*successfully, tty (\/dev.*)', result['stderr'])
        if m:
            ret['console'] = m.groups()[0]
        else:
            m = re.match(r'.*Operation already in progress$', result['stderr'])
            if m:
                ret['changes'] = False
    else:
        raise CommandExecutionError(
            'Problem encountered running vmctl',
            info={'errors': [result['stderr']], 'changes': ret}
        )

    return ret


def status(name=None, id=None):
    '''
    List VMs running on the host, or only the VM specified by ``id''.
    When both a name and id are provided, the id is ignored.

    name:
        Name of the defined VM.

    id:
        VM id.

    CLI Example:

    .. code-block:: bash

        salt '*' vmctl.status           # to list all VMs
        salt '*' vmctl.status name=web1 # to get a single VM
    '''
    ret = {}
    cmd = ['vmctl', 'status']

    result = __salt__['cmd.run_all'](cmd,
                                     output_loglevel='trace',
                                     python_shell=False)

    if result['retcode'] != 0:
        raise CommandExecutionError(
            'Problem encountered running vmctl',
            info={'error': [result['stderr']], 'changes': ret}
        )

    # Grab the header and save it with the lowercase names.
    header = result['stdout'].splitlines()[0].split()
    header = list([x.lower() for x in header])

    # A VM can be in one of the following states (from vmm.c:vcpu_state_decode())
    # - stopped
    # - running
    # - requesting termination
    # - terminated
    # - unknown

    for line in result['stdout'].splitlines()[1:]:
        data = line.split()
        vm = dict(list(zip(header, data)))
        vmname = vm.pop('name')
        if vm['pid'] == '-':
            # If the VM has no PID it's not running.
            vm['state'] = 'stopped'
        elif vmname and data[-2] == '-':
            # When a VM does have a PID and the second to last field is a '-', it's
            # transitioning to another state. A VM name itself cannot contain a
            # '-' so it's safe to split on '-'.
            vm['state'] = data[-1]
        else:
            vm['state'] = 'running'

        # When the status is requested of a single VM (by name) which is stopping,
        # vmctl doesn't print the status line. So we'll parse the full list and
        # return when we've found the requested VM.
        if id and int(vm['id']) == id:
            return {vmname: vm}
        elif name and vmname == name:
            return {vmname: vm}
        else:
            ret[vmname] = vm

    # Assert we've not come this far when an id or name have been provided. That
    # means the requested VM does not exist.
    if id or name:
        return {}

    return ret


def stop(name=None, id=None):
    '''
    Stop (terminate) the VM identified by the given id or name.
    When both a name and id are provided, the id is ignored.

    name:
        Name of the defined VM.

    id:
        VM id.

    CLI Example:

    .. code-block:: bash

        salt '*' vmctl.stop name=alpine
    '''
    ret = {}
    cmd = ['vmctl', 'stop']

    if not (name or id):
        raise SaltInvocationError('Must provide either "name" or "id"')
    elif name:
        cmd.append(name)
    else:
        cmd.append(id)

    result = __salt__['cmd.run_all'](cmd,
                                     output_loglevel='trace',
                                     python_shell=False)

    if result['retcode'] == 0:
        if re.match('^vmctl: sent request to terminate vm.*', result['stderr']):
            ret['changes'] = True
        else:
            ret['changes'] = False
    else:
        raise CommandExecutionError(
            'Problem encountered running vmctl',
            info={'errors': [result['stderr']], 'changes': ret}
        )

    return ret
