saltstack/salt

View on GitHub
salt/cloud/clouds/proxmox.py

Summary

Maintainability
F
3 wks
Test Coverage
# -*- coding: utf-8 -*-
'''
Proxmox Cloud Module
======================

.. versionadded:: 2014.7.0

The Proxmox cloud module is used to control access to cloud providers using
the Proxmox system (KVM / OpenVZ / LXC).

Set up the cloud configuration at ``/etc/salt/cloud.providers`` or
 ``/etc/salt/cloud.providers.d/proxmox.conf``:

.. code-block:: yaml

    my-proxmox-config:
      # Proxmox account information
      user: myuser@pam or myuser@pve
      password: mypassword
      url: hypervisor.domain.tld
      port: 8006
      driver: proxmox
      verify_ssl: True

:maintainer: Frank Klaassen <frank@cloudright.nl>
:depends: requests >= 2.2.1
:depends: IPy >= 0.81
'''

# Import python libs
from __future__ import absolute_import, print_function, unicode_literals
import time
import pprint
import logging
import re

# Import salt libs
import salt.utils.cloud
import salt.utils.json

# Import salt cloud libs
import salt.config as config
from salt.exceptions import (
    SaltCloudSystemExit,
    SaltCloudExecutionFailure,
    SaltCloudExecutionTimeout
)

# Import 3rd-party Libs
from salt.ext import six
from salt.ext.six.moves import range
try:
    import requests
    HAS_REQUESTS = True
except ImportError:
    HAS_REQUESTS = False

try:
    from IPy import IP
    HAS_IPY = True
except ImportError:
    HAS_IPY = False

# Get logging started
log = logging.getLogger(__name__)

__virtualname__ = 'proxmox'


def __virtual__():
    '''
    Check for PROXMOX configurations
    '''
    if get_configured_provider() is False:
        return False

    if get_dependencies() is False:
        return False

    return __virtualname__


def get_configured_provider():
    '''
    Return the first configured instance.
    '''
    return config.is_provider_configured(
        __opts__,
        __active_provider_name__ or __virtualname__,
        ('user',)
    )


def get_dependencies():
    '''
    Warn if dependencies aren't met.
    '''
    deps = {
        'requests': HAS_REQUESTS,
        'IPy': HAS_IPY
    }
    return config.check_driver_dependencies(
        __virtualname__,
        deps
    )


url = None
port = None
ticket = None
csrf = None
verify_ssl = None
api = None


def _authenticate():
    '''
    Retrieve CSRF and API tickets for the Proxmox API
    '''
    global url, port, ticket, csrf, verify_ssl
    url = config.get_cloud_config_value(
        'url', get_configured_provider(), __opts__, search_global=False
    )
    port = config.get_cloud_config_value(
        'port', get_configured_provider(), __opts__,
        default=8006, search_global=False
    )
    username = config.get_cloud_config_value(
        'user', get_configured_provider(), __opts__, search_global=False
    ),
    passwd = config.get_cloud_config_value(
        'password', get_configured_provider(), __opts__, search_global=False
    )
    verify_ssl = config.get_cloud_config_value(
        'verify_ssl', get_configured_provider(), __opts__,
        default=True, search_global=False
    )

    connect_data = {'username': username, 'password': passwd}
    full_url = 'https://{0}:{1}/api2/json/access/ticket'.format(url, port)

    returned_data = requests.post(
        full_url, verify=verify_ssl, data=connect_data).json()

    ticket = {'PVEAuthCookie': returned_data['data']['ticket']}
    csrf = six.text_type(returned_data['data']['CSRFPreventionToken'])


def query(conn_type, option, post_data=None):
    '''
    Execute the HTTP request to the API
    '''
    if ticket is None or csrf is None or url is None:
        log.debug('Not authenticated yet, doing that now..')
        _authenticate()

    full_url = 'https://{0}:{1}/api2/json/{2}'.format(url, port, option)

    log.debug('%s: %s (%s)', conn_type, full_url, post_data)

    httpheaders = {'Accept': 'application/json',
                   'Content-Type': 'application/x-www-form-urlencoded',
                   'User-Agent': 'salt-cloud-proxmox'}

    if conn_type == 'post':
        httpheaders['CSRFPreventionToken'] = csrf
        response = requests.post(full_url, verify=verify_ssl,
                                 data=post_data,
                                 cookies=ticket,
                                 headers=httpheaders)
    elif conn_type == 'put':
        httpheaders['CSRFPreventionToken'] = csrf
        response = requests.put(full_url, verify=verify_ssl,
                                data=post_data,
                                cookies=ticket,
                                headers=httpheaders)
    elif conn_type == 'delete':
        httpheaders['CSRFPreventionToken'] = csrf
        response = requests.delete(full_url, verify=verify_ssl,
                                   data=post_data,
                                   cookies=ticket,
                                   headers=httpheaders)
    elif conn_type == 'get':
        response = requests.get(full_url, verify=verify_ssl,
                                cookies=ticket)

    response.raise_for_status()

    try:
        returned_data = response.json()
        if 'data' not in returned_data:
            raise SaltCloudExecutionFailure
        return returned_data['data']
    except Exception:
        log.error('Error in trying to process JSON')
        log.error(response)


def _get_vm_by_name(name, allDetails=False):
    '''
    Since Proxmox works based op id's rather than names as identifiers this
    requires some filtering to retrieve the required information.
    '''
    vms = get_resources_vms(includeConfig=allDetails)
    if name in vms:
        return vms[name]

    log.info('VM with name "%s" could not be found.', name)
    return False


def _get_vm_by_id(vmid, allDetails=False):
    '''
    Retrieve a VM based on the ID.
    '''
    for vm_name, vm_details in six.iteritems(get_resources_vms(includeConfig=allDetails)):
        if six.text_type(vm_details['vmid']) == six.text_type(vmid):
            return vm_details

    log.info('VM with ID "%s" could not be found.', vmid)
    return False


def _get_next_vmid():
    '''
    Proxmox allows the use of alternative ids instead of autoincrementing.
    Because of that its required to query what the first available ID is.
    '''
    return int(query('get', 'cluster/nextid'))


def _check_ip_available(ip_addr):
    '''
    Proxmox VMs refuse to start when the IP is already being used.
    This function can be used to prevent VMs being created with duplicate
    IP's or to generate a warning.
    '''
    for vm_name, vm_details in six.iteritems(get_resources_vms(includeConfig=True)):
        vm_config = vm_details['config']
        if ip_addr in vm_config['ip_address'] or vm_config['ip_address'] == ip_addr:
            log.debug('IP "%s" is already defined', ip_addr)
            return False

    log.debug('IP \'%s\' is available to be defined', ip_addr)
    return True


def _parse_proxmox_upid(node, vm_=None):
    '''
    Upon requesting a task that runs for a longer period of time a UPID is given.
    This includes information about the job and can be used to lookup information in the log.
    '''
    ret = {}

    upid = node
    # Parse node response
    node = node.split(':')
    if node[0] == 'UPID':
        ret['node'] = six.text_type(node[1])
        ret['pid'] = six.text_type(node[2])
        ret['pstart'] = six.text_type(node[3])
        ret['starttime'] = six.text_type(node[4])
        ret['type'] = six.text_type(node[5])
        ret['vmid'] = six.text_type(node[6])
        ret['user'] = six.text_type(node[7])
        # include the upid again in case we'll need it again
        ret['upid'] = six.text_type(upid)

        if vm_ is not None and 'technology' in vm_:
            ret['technology'] = six.text_type(vm_['technology'])

    return ret


def _lookup_proxmox_task(upid):
    '''
    Retrieve the (latest) logs and retrieve the status for a UPID.
    This can be used to verify whether a task has completed.
    '''
    log.debug('Getting creation status for upid: %s', upid)
    tasks = query('get', 'cluster/tasks')

    if tasks:
        for task in tasks:
            if task['upid'] == upid:
                log.debug('Found upid task: %s', task)
                return task

    return False


def get_resources_nodes(call=None, resFilter=None):
    '''
    Retrieve all hypervisors (nodes) available on this environment
    CLI Example:

    .. code-block:: bash

        salt-cloud -f get_resources_nodes my-proxmox-config
    '''
    log.debug('Getting resource: nodes.. (filter: %s)', resFilter)
    resources = query('get', 'cluster/resources')

    ret = {}
    for resource in resources:
        if 'type' in resource and resource['type'] == 'node':
            name = resource['node']
            ret[name] = resource

    if resFilter is not None:
        log.debug('Filter given: %s, returning requested '
                  'resource: nodes', resFilter)
        return ret[resFilter]

    log.debug('Filter not given: %s, returning all resource: nodes', ret)
    return ret


def get_resources_vms(call=None, resFilter=None, includeConfig=True):
    '''
    Retrieve all VMs available on this environment

    CLI Example:

    .. code-block:: bash

        salt-cloud -f get_resources_vms my-proxmox-config
    '''

    timeoutTime = time.time() + 60
    while True:
        log.debug('Getting resource: vms.. (filter: %s)', resFilter)
        resources = query('get', 'cluster/resources')
        ret = {}
        badResource = False
        for resource in resources:
            if 'type' in resource and resource['type'] in ['openvz', 'qemu',
                                                           'lxc']:
                try:
                    name = resource['name']
                except KeyError:
                    badResource = True
                    log.debug('No name in VM resource %s', repr(resource))
                    break

                ret[name] = resource

                if includeConfig:
                    # Requested to include the detailed configuration of a VM
                    ret[name]['config'] = get_vmconfig(
                        ret[name]['vmid'],
                        ret[name]['node'],
                        ret[name]['type']
                    )

        if time.time() > timeoutTime:
            raise SaltCloudExecutionTimeout('FAILED to get the proxmox '
                                            'resources vms')

        # Carry on if there wasn't a bad resource return from Proxmox
        if not badResource:
            break

        time.sleep(0.5)

    if resFilter is not None:
        log.debug('Filter given: %s, returning requested '
                  'resource: nodes', resFilter)
        return ret[resFilter]

    log.debug('Filter not given: %s, returning all resource: nodes', ret)
    return ret


def script(vm_):
    '''
    Return the script deployment object
    '''
    script_name = config.get_cloud_config_value('script', vm_, __opts__)
    if not script_name:
        script_name = 'bootstrap-salt'

    return salt.utils.cloud.os_script(
        script_name,
        vm_,
        __opts__,
        salt.utils.cloud.salt_config_to_yaml(
            salt.utils.cloud.minion_config(__opts__, vm_)
        )
    )


def avail_locations(call=None):
    '''
    Return a list of the hypervisors (nodes) which this Proxmox PVE machine manages

    CLI Example:

    .. code-block:: bash

        salt-cloud --list-locations my-proxmox-config
    '''
    if call == 'action':
        raise SaltCloudSystemExit(
            'The avail_locations function must be called with '
            '-f or --function, or with the --list-locations option'
        )

    # could also use the get_resources_nodes but speed is ~the same
    nodes = query('get', 'nodes')

    ret = {}
    for node in nodes:
        name = node['node']
        ret[name] = node

    return ret


def avail_images(call=None, location='local'):
    '''
    Return a list of the images that are on the provider

    CLI Example:

    .. code-block:: bash

        salt-cloud --list-images my-proxmox-config
    '''
    if call == 'action':
        raise SaltCloudSystemExit(
            'The avail_images function must be called with '
            '-f or --function, or with the --list-images option'
        )

    ret = {}
    for host_name, host_details in six.iteritems(avail_locations()):
        for item in query('get', 'nodes/{0}/storage/{1}/content'.format(host_name, location)):
            ret[item['volid']] = item
    return ret


def list_nodes(call=None):
    '''
    Return a list of the VMs that are managed by the provider

    CLI Example:

    .. code-block:: bash

        salt-cloud -Q my-proxmox-config
    '''
    if call == 'action':
        raise SaltCloudSystemExit(
            'The list_nodes function must be called with -f or --function.'
        )

    ret = {}
    for vm_name, vm_details in six.iteritems(get_resources_vms(includeConfig=True)):
        log.debug('VM_Name: %s', vm_name)
        log.debug('vm_details: %s', vm_details)

        # Limit resultset on what Salt-cloud demands:
        ret[vm_name] = {}
        ret[vm_name]['id'] = six.text_type(vm_details['vmid'])
        ret[vm_name]['image'] = six.text_type(vm_details['vmid'])
        ret[vm_name]['size'] = six.text_type(vm_details['disk'])
        ret[vm_name]['state'] = six.text_type(vm_details['status'])

        # Figure out which is which to put it in the right column
        private_ips = []
        public_ips = []

        if 'ip_address' in vm_details['config'] and vm_details['config']['ip_address'] != '-':
            ips = vm_details['config']['ip_address'].split(' ')
            for ip_ in ips:
                if IP(ip_).iptype() == 'PRIVATE':
                    private_ips.append(six.text_type(ip_))
                else:
                    public_ips.append(six.text_type(ip_))

        ret[vm_name]['private_ips'] = private_ips
        ret[vm_name]['public_ips'] = public_ips

    return ret


def list_nodes_full(call=None):
    '''
    Return a list of the VMs that are on the provider

    CLI Example:

    .. code-block:: bash

        salt-cloud -F my-proxmox-config
    '''
    if call == 'action':
        raise SaltCloudSystemExit(
            'The list_nodes_full function must be called with -f or --function.'
        )

    return get_resources_vms(includeConfig=True)


def list_nodes_select(call=None):
    '''
    Return a list of the VMs that are on the provider, with select fields

    CLI Example:

    .. code-block:: bash

        salt-cloud -S my-proxmox-config
    '''
    return salt.utils.cloud.list_nodes_select(
        list_nodes_full(), __opts__['query.selection'], call,
    )


def _stringlist_to_dictionary(input_string):
    '''
    Convert a stringlist (comma separated settings) to a dictionary

    The result of the string setting1=value1,setting2=value2 will be a python dictionary:

    {'setting1':'value1','setting2':'value2'}
    '''
    li = str(input_string).split(',')
    ret = {}
    for item in li:
        pair = str(item).replace(' ', '').split('=')
        if len(pair) != 2:
            log.warning('Cannot process stringlist item %s', item)
            continue

        ret[pair[0]] = pair[1]
    return ret


def _dictionary_to_stringlist(input_dict):
    '''
    Convert a dictionary to a stringlist (comma separated settings)

    The result of the dictionary {'setting1':'value1','setting2':'value2'} will be:

    setting1=value1,setting2=value2
    '''
    string_value = ""
    for s in input_dict:
        string_value += "{0}={1},".format(s, input_dict[s])
    string_value = string_value[:-1]
    return string_value


def create(vm_):
    '''
    Create a single VM from a data dict

    CLI Example:

    .. code-block:: bash

        salt-cloud -p proxmox-ubuntu vmhostname
    '''
    try:
        # Check for required profile parameters before sending any API calls.
        if vm_['profile'] and config.is_profile_configured(__opts__,
                                                           __active_provider_name__ or 'proxmox',
                                                           vm_['profile'],
                                                           vm_=vm_) is False:
            return False
    except AttributeError:
        pass

    ret = {}

    __utils__['cloud.fire_event'](
        'event',
        'starting create',
        'salt/cloud/{0}/creating'.format(vm_['name']),
        args=__utils__['cloud.filter_event'](
            'creating', vm_, ['name', 'profile', 'provider', 'driver']),
        sock_dir=__opts__['sock_dir'],
        transport=__opts__['transport']
    )

    log.info('Creating Cloud VM %s', vm_['name'])

    if 'use_dns' in vm_ and 'ip_address' not in vm_:
        use_dns = vm_['use_dns']
        if use_dns:
            from socket import gethostbyname, gaierror
            try:
                ip_address = gethostbyname(six.text_type(vm_['name']))
            except gaierror:
                log.debug('Resolving of %s failed', vm_['name'])
            else:
                vm_['ip_address'] = six.text_type(ip_address)

    try:
        newid = _get_next_vmid()
        data = create_node(vm_, newid)
    except Exception as exc:
        log.error(
            'Error creating %s on PROXMOX\n\n'
            'The following exception was thrown when trying to '
            'run the initial deployment: \n%s',
            vm_['name'], exc,
            # Show the traceback if the debug logging level is enabled
            exc_info_on_loglevel=logging.DEBUG
        )
        return False

    ret['creation_data'] = data
    name = vm_['name']        # hostname which we know
    if 'clone' in vm_ and vm_['clone'] is True:
        vmid = newid
    else:
        vmid = data['vmid']       # vmid which we have received
    host = data['node']       # host which we have received
    nodeType = data['technology']  # VM tech (Qemu / OpenVZ)

    if 'agent_get_ip' not in vm_ or vm_['agent_get_ip'] == 0:
        # Determine which IP to use in order of preference:
        if 'ip_address' in vm_:
            ip_address = six.text_type(vm_['ip_address'])
        elif 'public_ips' in data:
            ip_address = six.text_type(data['public_ips'][0])  # first IP
        elif 'private_ips' in data:
            ip_address = six.text_type(data['private_ips'][0])  # first IP
        else:
            raise SaltCloudExecutionFailure("Could not determine an IP address to use")

    # wait until the vm has been created so we can start it
    if not wait_for_created(data['upid'], timeout=300):
        return {'Error': 'Unable to create {0}, command timed out'.format(name)}

    if 'clone' in vm_ and vm_['clone'] is True and vm_['technology'] == 'qemu':
        # If we cloned a machine, see if we need to reconfigure any of the options such as net0,
        # ide2, etc. This enables us to have a different cloud-init ISO mounted for each VM that's
        # brought up

        log.info('Configuring cloned VM')

        # Modify the settings for the VM one at a time so we can see any problems with the values
        # as quickly as possible
        for setting in 'sockets', 'cores', 'cpulimit', 'memory', 'onboot', 'agent':
            if setting in vm_:  # if the property is set, use it for the VM request
                postParams = {}
                postParams[setting] = vm_[setting]
                query('post', 'nodes/{0}/qemu/{1}/config'.format(vm_['host'], vmid), postParams)

        # cloud-init settings
        for setting in 'ciuser', 'cipassword', 'sshkeys', 'nameserver', 'searchdomain':
            if setting in vm_:  # if the property is set, use it for the VM request
                postParams = {}
                postParams[setting] = vm_[setting]
                query('post', 'nodes/{0}/qemu/{1}/config'.format(vm_['host'], vmid), postParams)

        for setting_number in range(3):
            setting = 'ide{0}'.format(setting_number)
            if setting in vm_:
                postParams = {}
                postParams[setting] = vm_[setting]
                query('post', 'nodes/{0}/qemu/{1}/config'.format(vm_['host'], vmid), postParams)

        for setting_number in range(5):
            setting = 'sata{0}'.format(setting_number)
            if setting in vm_:
                vm_config = query('get', 'nodes/{0}/qemu/{1}/config'.format(vm_['host'], vmid))
                if setting in vm_config:
                    setting_params = vm_[setting]
                    setting_storage = setting_params.split(':')[0]
                    setting_size = _stringlist_to_dictionary(setting_params)['size']
                    vm_disk_params = vm_config[setting]
                    vm_disk_storage = vm_disk_params.split(':')[0]
                    vm_disk_size = _stringlist_to_dictionary(vm_disk_params)['size']
                    # if storage is different, move the disk
                    if setting_storage != vm_disk_storage:
                        postParams = {}
                        postParams['disk'] = setting
                        postParams['storage'] = setting_storage
                        postParams['delete'] = 1
                        node = query('post', 'nodes/{0}/qemu/{1}/move_disk'.format(
                            vm_['host'], vmid), postParams)
                        data = _parse_proxmox_upid(node, vm_)
                        # wait until the disk has been moved
                        if not wait_for_task(data['upid'], timeout=300):
                            return {'Error': 'Unable to move disk {0}, command timed out'.format(
                                setting)}
                    # if storage is different, move the disk
                    if setting_size != vm_disk_size:
                        postParams = {}
                        postParams['disk'] = setting
                        postParams['size'] = setting_size
                        query('put', 'nodes/{0}/qemu/{1}/resize'.format(
                            vm_['host'], vmid), postParams)
                else:
                    postParams = {}
                    postParams[setting] = vm_[setting]
                    query('post', 'nodes/{0}/qemu/{1}/config'.format(vm_['host'], vmid), postParams)

        for setting_number in range(13):
            setting = 'scsi{0}'.format(setting_number)
            if setting in vm_:
                vm_config = query('get', 'nodes/{0}/qemu/{1}/config'.format(vm_['host'], vmid))
                if setting in vm_config:
                    setting_params = vm_[setting]
                    setting_storage = setting_params.split(':')[0]
                    setting_size = _stringlist_to_dictionary(setting_params)['size']
                    vm_disk_params = vm_config[setting]
                    vm_disk_storage = vm_disk_params.split(':')[0]
                    vm_disk_size = _stringlist_to_dictionary(vm_disk_params)['size']
                    # if storage is different, move the disk
                    if setting_storage != vm_disk_storage:
                        postParams = {}
                        postParams['disk'] = setting
                        postParams['storage'] = setting_storage
                        postParams['delete'] = 1
                        node = query('post', 'nodes/{0}/qemu/{1}/move_disk'.format(
                            vm_['host'], vmid), postParams)
                        data = _parse_proxmox_upid(node, vm_)
                        # wait until the disk has been moved
                        if not wait_for_task(data['upid'], timeout=300):
                            return {'Error': 'Unable to move disk {0}, command timed out'.format(
                                setting)}
                    # if storage is different, move the disk
                    if setting_size != vm_disk_size:
                        postParams = {}
                        postParams['disk'] = setting
                        postParams['size'] = setting_size
                        query('put', 'nodes/{0}/qemu/{1}/resize'.format(
                            vm_['host'], vmid), postParams)
                else:
                    postParams = {}
                    postParams[setting] = vm_[setting]
                    query('post', 'nodes/{0}/qemu/{1}/config'.format(vm_['host'], vmid), postParams)

        # net strings are a list of comma seperated settings. We need to merge the settings so that
        # the setting in the profile only changes the settings it touches and the other settings
        # are left alone. An example of why this is necessary is because the MAC address is set
        # in here and generally you don't want to alter or have to know the MAC address of the new
        # instance, but you may want to set the VLAN bridge for example
        for setting_number in range(20):
            setting = 'net{0}'.format(setting_number)
            if setting in vm_:
                data = query('get', 'nodes/{0}/qemu/{1}/config'.format(vm_['host'], vmid))

                # Generate a dictionary of settings from the existing string
                new_setting = {}
                if setting in data:
                    new_setting.update(_stringlist_to_dictionary(data[setting]))

                # Merge the new settings (as a dictionary) into the existing dictionary to get the
                # new merged settings
                new_setting.update(_stringlist_to_dictionary(vm_[setting]))

                # Convert the dictionary back into a string list
                postParams = {setting: _dictionary_to_stringlist(new_setting)}
                query('post', 'nodes/{0}/qemu/{1}/config'.format(vm_['host'], vmid), postParams)

        for setting_number in range(20):
            setting = 'ipconfig{0}'.format(setting_number)
            if setting in vm_:
                data = query('get', 'nodes/{0}/qemu/{1}/config'.format(vm_['host'], vmid))

                # Generate a dictionary of settings from the existing string
                new_setting = {}
                if setting in data:
                    new_setting.update(_stringlist_to_dictionary(data[setting]))

                # Merge the new settings (as a dictionary) into the existing dictionary to get the
                # new merged settings
                if setting_number == 0 and 'ip_address' in vm_:
                    if 'gw' in _stringlist_to_dictionary(vm_[setting]):
                        new_setting.update(_stringlist_to_dictionary(
                            'ip={0}/24,gw={1}'.format(
                                vm_['ip_address'], _stringlist_to_dictionary(vm_[setting])['gw'])))
                    else:
                        new_setting.update(
                            _stringlist_to_dictionary('ip={0}/24'.format(vm_['ip_address'])))
                else:
                    new_setting.update(_stringlist_to_dictionary(vm_[setting]))

                # Convert the dictionary back into a string list
                postParams = {setting: _dictionary_to_stringlist(new_setting)}
                query('post', 'nodes/{0}/qemu/{1}/config'.format(vm_['host'], vmid), postParams)

    # VM has been created. Starting..
    if not start(name, vmid, call='action'):
        log.error('Node %s (%s) failed to start!', name, vmid)
        raise SaltCloudExecutionFailure

    # Wait until the VM has fully started
    log.debug('Waiting for state "running" for vm %s on %s', vmid, host)
    if not wait_for_state(vmid, 'running'):
        return {'Error': 'Unable to start {0}, command timed out'.format(name)}

    # For QEMU VMs, we can get the IP Address from qemu-agent
    if 'agent_get_ip' in vm_ and vm_['agent_get_ip'] == 1:
        def __find_agent_ip(vm_):
            log.debug("Waiting for qemu-agent to start...")
            endpoint = 'nodes/{0}/qemu/{1}/agent/network-get-interfaces'.format(vm_['host'], vmid)
            interfaces = query('get', endpoint)
            # If we get a result from the agent, parse it
            if 'result' in interfaces:
                for interface in interfaces['result']:
                    if_name = interface['name']
                    # Only check ethernet type interfaces, as they are not returned in any order
                    if if_name.startswith('eth') or if_name.startswith('ens'):
                        for if_addr in interface['ip-addresses']:
                            ip_addr = if_addr['ip-address']
                            # Ensure interface has a valid IPv4 address
                            if if_addr['ip-address-type'] == 'ipv4' and ip_addr is not None:
                                return six.text_type(ip_addr)
            raise SaltCloudExecutionFailure

        # We have to wait for a bit for qemu-agent to start
        try:
            ip_address = __utils__['cloud.wait_for_fun'](
                __find_agent_ip,
                vm_=vm_
            )
        except (SaltCloudExecutionTimeout, SaltCloudExecutionFailure) as exc:
            try:
                # If VM was created but we can't connect, destroy it.
                destroy(vm_['name'])
            except SaltCloudSystemExit:
                pass
            finally:
                raise SaltCloudSystemExit(six.text_type(exc))

    log.debug('Using IP address %s', ip_address)

    ssh_username = config.get_cloud_config_value(
        'ssh_username', vm_, __opts__, default='root'
    )
    ssh_password = config.get_cloud_config_value(
        'password', vm_, __opts__,
    )

    ret['ip_address'] = ip_address
    ret['username'] = ssh_username
    ret['password'] = ssh_password

    vm_['ssh_host'] = ip_address
    vm_['password'] = ssh_password
    ret = __utils__['cloud.bootstrap'](vm_, __opts__)

    # Report success!
    log.info('Created Cloud VM \'%s\'', vm_['name'])
    log.debug(
        '\'%s\' VM creation details:\n%s',
        vm_['name'], pprint.pformat(data)
    )

    __utils__['cloud.fire_event'](
        'event',
        'created instance',
        'salt/cloud/{0}/created'.format(vm_['name']),
        args=__utils__['cloud.filter_event'](
            'created', vm_, ['name', 'profile', 'provider', 'driver']),
        sock_dir=__opts__['sock_dir'],
    )

    return ret


def _import_api():
    '''
    Download https://<url>/pve-docs/api-viewer/apidoc.js
    Extract content of pveapi var (json formated)
    Load this json content into global variable "api"
    '''
    global api
    full_url = 'https://{0}:{1}/pve-docs/api-viewer/apidoc.js'.format(url, port)
    returned_data = requests.get(full_url, verify=verify_ssl)

    re_filter = re.compile('(?<=pveapi =)(.*)(?=^;)', re.DOTALL | re.MULTILINE)
    api_json = re_filter.findall(returned_data.text)[0]
    api = salt.utils.json.loads(api_json)


def _get_properties(path="", method="GET", forced_params=None):
    '''
    Return the parameter list from api for defined path and HTTP method
    '''
    if api is None:
        _import_api()

    sub = api
    path_levels = [level for level in path.split('/') if level != '']
    search_path = ''
    props = []
    parameters = set([] if forced_params is None else forced_params)
    # Browse all path elements but last
    for elem in path_levels[:-1]:
        search_path += '/' + elem
        # Lookup for a dictionary with path = "requested path" in list" and return its children
        sub = (item for item in sub if item["path"] == search_path).next()['children']
    # Get leaf element in path
    search_path += '/' + path_levels[-1]
    sub = next((item for item in sub if item["path"] == search_path))
    try:
        # get list of properties for requested method
        props = sub['info'][method]['parameters']['properties'].keys()
    except KeyError as exc:
        log.error('method not found: "%s"', exc)
    for prop in props:
        numerical = re.match(r'(\w+)\[n\]', prop)
        # generate (arbitrarily) 10 properties for duplicatable properties identified by:
        # "prop[n]"
        if numerical:
            for i in range(10):
                parameters.add(numerical.group(1) + six.text_type(i))
        else:
            parameters.add(prop)
    return parameters


def create_node(vm_, newid):
    '''
    Build and submit the requestdata to create a new node
    '''
    newnode = {}

    if 'technology' not in vm_:
        vm_['technology'] = 'openvz'  # default virt tech if none is given

    if vm_['technology'] not in ['qemu', 'openvz', 'lxc']:
        # Wrong VM type given
        log.error('Wrong VM type. Valid options are: qemu, openvz (proxmox3) or lxc (proxmox4)')
        raise SaltCloudExecutionFailure

    if 'host' not in vm_:
        # Use globally configured/default location
        vm_['host'] = config.get_cloud_config_value(
            'default_host', get_configured_provider(), __opts__, search_global=False
        )

    if vm_['host'] is None:
        # No location given for the profile
        log.error('No host given to create this VM on')
        raise SaltCloudExecutionFailure

    # Required by both OpenVZ and Qemu (KVM)
    vmhost = vm_['host']
    newnode['vmid'] = newid

    for prop in 'cpuunits', 'description', 'memory', 'onboot':
        if prop in vm_:  # if the property is set, use it for the VM request
            newnode[prop] = vm_[prop]

    if vm_['technology'] == 'openvz':
        # OpenVZ related settings, using non-default names:
        newnode['hostname'] = vm_['name']
        newnode['ostemplate'] = vm_['image']

        # optional VZ settings
        for prop in ['cpus', 'disk', 'ip_address', 'nameserver',
                     'password', 'swap', 'poolid', 'storage']:
            if prop in vm_:  # if the property is set, use it for the VM request
                newnode[prop] = vm_[prop]

    elif vm_['technology'] == 'lxc':
        # LXC related settings, using non-default names:
        newnode['hostname'] = vm_['name']
        newnode['ostemplate'] = vm_['image']

        static_props = ('cpuunits', 'cpulimit', 'rootfs', 'cores', 'description', 'memory',
                        'onboot', 'net0', 'password', 'nameserver', 'swap', 'storage', 'rootfs')
        for prop in _get_properties('/nodes/{node}/lxc',
                                    'POST',
                                    static_props):
            if prop in vm_:  # if the property is set, use it for the VM request
                newnode[prop] = vm_[prop]

        if 'pubkey' in vm_:
            newnode['ssh-public-keys'] = vm_['pubkey']

        # inform user the "disk" option is not supported for LXC hosts
        if 'disk' in vm_:
            log.warning('The "disk" option is not supported for LXC hosts and was ignored')

        # LXC specific network config
        # OpenVZ allowed specifying IP and gateway. To ease migration from
        # Proxmox 3, I've mapped the ip_address and gw to a generic net0 config.
        # If you need more control, please use the net0 option directly.
        # This also assumes a /24 subnet.
        if 'ip_address' in vm_ and 'net0' not in vm_:
            newnode['net0'] = 'bridge=vmbr0,ip=' + vm_['ip_address'] + '/24,name=eth0,type=veth'

            # gateway is optional and does not assume a default
            if 'gw' in vm_:
                newnode['net0'] = newnode['net0'] + ',gw=' + vm_['gw']

    elif vm_['technology'] == 'qemu':
        # optional Qemu settings
        static_props = (
            'acpi', 'cores', 'cpu', 'pool', 'storage', 'sata0', 'ostype', 'ide2', 'net0')
        for prop in _get_properties('/nodes/{node}/qemu',
                                    'POST',
                                    static_props):
            if prop in vm_:  # if the property is set, use it for the VM request
                newnode[prop] = vm_[prop]

    # The node is ready. Lets request it to be added
    __utils__['cloud.fire_event'](
        'event',
        'requesting instance',
        'salt/cloud/{0}/requesting'.format(vm_['name']),
        args={
            'kwargs': __utils__['cloud.filter_event']('requesting', newnode, list(newnode)),
        },
        sock_dir=__opts__['sock_dir'],
    )

    log.debug('Preparing to generate a node using these parameters: %s ', newnode)
    if 'clone' in vm_ and vm_['clone'] is True and vm_['technology'] == 'qemu':
        postParams = {}
        postParams['newid'] = newnode['vmid']

        for prop in 'description', 'format', 'full', 'name':
            if 'clone_' + prop in vm_:  # if the property is set, use it for the VM request
                postParams[prop] = vm_['clone_' + prop]

        if 'host' in vm_:
            postParams['target'] = vm_['host']

        try:
            int(vm_['clone_from'])
        except ValueError:
            if ':' in vm_['clone_from']:
                vmhost = vm_['clone_from'].split(':')[0]
                vm_['clone_from'] = vm_['clone_from'].split(':')[1]

        node = query('post', 'nodes/{0}/qemu/{1}/clone'.format(
            vmhost, vm_['clone_from']), postParams)
    else:
        node = query('post', 'nodes/{0}/{1}'.format(vmhost, vm_['technology']), newnode)
    return _parse_proxmox_upid(node, vm_)


def show_instance(name, call=None):
    '''
    Show the details from Proxmox concerning an instance
    '''
    if call != 'action':
        raise SaltCloudSystemExit(
            'The show_instance action must be called with -a or --action.'
        )

    nodes = list_nodes_full()
    __utils__['cloud.cache_node'](nodes[name], __active_provider_name__, __opts__)
    return nodes[name]


def get_vmconfig(vmid, node=None, node_type='openvz'):
    '''
    Get VM configuration
    '''
    if node is None:
        # We need to figure out which node this VM is on.
        for host_name, host_details in six.iteritems(avail_locations()):
            for item in query('get', 'nodes/{0}/{1}'.format(host_name, node_type)):
                if item['vmid'] == vmid:
                    node = host_name

    # If we reached this point, we have all the information we need
    data = query('get', 'nodes/{0}/{1}/{2}/config'.format(node, node_type, vmid))

    return data


def wait_for_created(upid, timeout=300):
    '''
    Wait until a the vm has been created successfully
    '''
    start_time = time.time()
    info = _lookup_proxmox_task(upid)
    if not info:
        log.error('wait_for_created: No task information '
                  'retrieved based on given criteria.')
        raise SaltCloudExecutionFailure

    while True:
        if 'status' in info and info['status'] == 'OK':
            log.debug('Host has been created!')
            return True
        time.sleep(3)  # Little more patience, we're not in a hurry
        if time.time() - start_time > timeout:
            log.debug('Timeout reached while waiting for host to be created')
            return False
        info = _lookup_proxmox_task(upid)


def wait_for_state(vmid, state, timeout=300):
    '''
    Wait until a specific state has been reached on a node
    '''
    start_time = time.time()
    node = get_vm_status(vmid=vmid)
    if not node:
        log.error('wait_for_state: No VM retrieved based on given criteria.')
        raise SaltCloudExecutionFailure

    while True:
        if node['status'] == state:
            log.debug('Host %s is now in "%s" state!', node['name'], state)
            return True
        time.sleep(1)
        if time.time() - start_time > timeout:
            log.debug('Timeout reached while waiting for %s to become %s',
                      node['name'], state)
            return False
        node = get_vm_status(vmid=vmid)
        log.debug('State for %s is: "%s" instead of "%s"',
                  node['name'], node['status'], state)


def wait_for_task(upid, timeout=300):
    '''
    Wait until a the task has been finished successfully
    '''
    start_time = time.time()
    info = _lookup_proxmox_task(upid)
    if not info:
        log.error('wait_for_task: No task information '
                  'retrieved based on given criteria.')
        raise SaltCloudExecutionFailure

    while True:
        if 'status' in info and info['status'] == 'OK':
            log.debug('Task has been finished!')
            return True
        time.sleep(3)  # Little more patience, we're not in a hurry
        if time.time() - start_time > timeout:
            log.debug('Timeout reached while waiting for task to be finished')
            return False
        info = _lookup_proxmox_task(upid)


def destroy(name, call=None):
    '''
    Destroy a node.

    CLI Example:

    .. code-block:: bash

        salt-cloud --destroy mymachine
    '''
    if call == 'function':
        raise SaltCloudSystemExit(
            'The destroy action must be called with -d, --destroy, '
            '-a or --action.'
        )

    __utils__['cloud.fire_event'](
        'event',
        'destroying instance',
        'salt/cloud/{0}/destroying'.format(name),
        args={'name': name},
        sock_dir=__opts__['sock_dir'],
        transport=__opts__['transport']
    )

    vmobj = _get_vm_by_name(name)
    if vmobj is not None:
        # stop the vm
        if get_vm_status(vmid=vmobj['vmid'])['status'] != 'stopped':
            stop(name, vmobj['vmid'], 'action')

        # wait until stopped
        if not wait_for_state(vmobj['vmid'], 'stopped'):
            return {'Error': 'Unable to stop {0}, command timed out'.format(name)}

        # required to wait a bit here, otherwise the VM is sometimes
        # still locked and destroy fails.
        time.sleep(3)

        query('delete', 'nodes/{0}/{1}'.format(
            vmobj['node'], vmobj['id']
        ))
        __utils__['cloud.fire_event'](
            'event',
            'destroyed instance',
            'salt/cloud/{0}/destroyed'.format(name),
            args={'name': name},
            sock_dir=__opts__['sock_dir'],
            transport=__opts__['transport']
        )
        if __opts__.get('update_cachedir', False) is True:
            __utils__['cloud.delete_minion_cachedir'](
                name, __active_provider_name__.split(':')[0], __opts__)

        return {'Destroyed': '{0} was destroyed.'.format(name)}


def set_vm_status(status, name=None, vmid=None):
    '''
    Convenience function for setting VM status
    '''
    log.debug('Set status to %s for %s (%s)', status, name, vmid)

    if vmid is not None:
        log.debug('set_vm_status: via ID - VMID %s (%s): %s',
                  vmid, name, status)
        vmobj = _get_vm_by_id(vmid)
    else:
        log.debug('set_vm_status: via name - VMID %s (%s): %s',
                  vmid, name, status)
        vmobj = _get_vm_by_name(name)

    if not vmobj or 'node' not in vmobj or 'type' not in vmobj or 'vmid' not in vmobj:
        log.error('Unable to set status %s for %s (%s)',
                  status, name, vmid)
        raise SaltCloudExecutionTimeout

    log.debug("VM_STATUS: Has desired info (%s). Setting status..", vmobj)
    data = query('post', 'nodes/{0}/{1}/{2}/status/{3}'.format(
        vmobj['node'], vmobj['type'], vmobj['vmid'], status))

    result = _parse_proxmox_upid(data, vmobj)

    if result is not False and result is not None:
        log.debug('Set_vm_status action result: %s', result)
        return True

    return False


def get_vm_status(vmid=None, name=None):
    '''
    Get the status for a VM, either via the ID or the hostname
    '''
    if vmid is not None:
        log.debug('get_vm_status: VMID %s', vmid)
        vmobj = _get_vm_by_id(vmid)
    elif name is not None:
        log.debug('get_vm_status: name %s', name)
        vmobj = _get_vm_by_name(name)
    else:
        log.debug("get_vm_status: No ID or NAME given")
        raise SaltCloudExecutionFailure

    log.debug('VM found: %s', vmobj)

    if vmobj is not None and 'node' in vmobj:
        log.debug("VM_STATUS: Has desired info. Retrieving.. (%s)",
                  vmobj['name'])
        data = query('get', 'nodes/{0}/{1}/{2}/status/current'.format(
            vmobj['node'], vmobj['type'], vmobj['vmid']))
        return data

    log.error('VM or requested status not found..')
    return False


def start(name, vmid=None, call=None):
    '''
    Start a node.

    CLI Example:

    .. code-block:: bash

        salt-cloud -a start mymachine
    '''
    if call != 'action':
        raise SaltCloudSystemExit(
            'The start action must be called with -a or --action.'
        )

    log.debug('Start: %s (%s) = Start', name, vmid)
    if not set_vm_status('start', name, vmid=vmid):
        log.error('Unable to bring VM %s (%s) up..', name, vmid)
        raise SaltCloudExecutionFailure

    # xxx: TBD: Check here whether the status was actually changed to 'started'

    return {'Started': '{0} was started.'.format(name)}


def stop(name, vmid=None, call=None):
    '''
    Stop a node ("pulling the plug").

    CLI Example:

    .. code-block:: bash

        salt-cloud -a stop mymachine
    '''
    if call != 'action':
        raise SaltCloudSystemExit(
            'The stop action must be called with -a or --action.'
        )

    if not set_vm_status('stop', name, vmid=vmid):
        log.error('Unable to bring VM %s (%s) down..', name, vmid)
        raise SaltCloudExecutionFailure

    # xxx: TBD: Check here whether the status was actually changed to 'stopped'

    return {'Stopped': '{0} was stopped.'.format(name)}


def shutdown(name=None, vmid=None, call=None):
    '''
    Shutdown a node via ACPI.

    CLI Example:

    .. code-block:: bash

        salt-cloud -a shutdown mymachine
    '''
    if call != 'action':
        raise SaltCloudSystemExit(
            'The shutdown action must be called with -a or --action.'
        )

    if not set_vm_status('shutdown', name, vmid=vmid):
        log.error('Unable to shut VM %s (%s) down..', name, vmid)
        raise SaltCloudExecutionFailure

    # xxx: TBD: Check here whether the status was actually changed to 'stopped'

    return {'Shutdown': '{0} was shutdown.'.format(name)}