saltstack/salt

View on GitHub
salt/utils/virtualbox.py

Summary

Maintainability
D
1 day
Test Coverage
# -*- coding: utf-8 -*-
'''
Utilities to help make requests to virtualbox

The virtualbox SDK reference can be found at http://download.virtualbox.org/virtualbox/SDKRef.pdf

This code assumes vboxapi.py from VirtualBox distribution
being in PYTHONPATH, or installed system-wide
'''
# Import python libs
from __future__ import absolute_import, print_function, unicode_literals
import logging
import re
import time

# Import salt libs
import salt.utils.compat
import salt.utils.data
from salt.utils.timeout import wait_for
import salt.ext.six as six


log = logging.getLogger(__name__)

# Import 3rd-party libs
from salt.ext import six
from salt.ext.six.moves import range

# Import virtualbox libs
HAS_LIBS = False
try:
    import vboxapi

    HAS_LIBS = True

except ImportError:
    VirtualBoxManager = None
    log.trace('Couldn\'t import VirtualBox API')

_virtualboxManager = None

'''
Attributes we expect to have when converting an XPCOM object to a dict
'''
XPCOM_ATTRIBUTES = {
    'IMachine': [
        'id',
        'name',
        'accessible',
        'description',
        'groups',
        'memorySize',
        'OSTypeId',
        'state',
    ],
    'INetworkAdapter': [
        'adapterType',
        'slot',
        'enabled',
        'MACAddress',
        'bridgedInterface',
        'hostOnlyInterface',
        'internalNetwork',
        'NATNetwork',
        'genericDriver',
        'cableConnected',
        'lineSpeed',
        'lineSpeed',
    ]
}

UNKNOWN_MACHINE_STATE = ('Unknown', 'This state is unknown to us. Might be new?')
MACHINE_STATE_LIST = [
    ('Null', 'Null value (never used by the API)'),
    ('PoweredOff', 'The machine is not running and has no saved execution state; '
                   'it has either never been started or been shut down successfully.'),
    ('Saved', 'The machine is not currently running, but the execution state of the machine has been '
              'saved to an external file when it was running, from where it can be resumed.'),
    ('Teleported', 'The machine was teleported to a different host (or process) and then powered off. '
                   'Take care when powering it on again may corrupt resources it shares with the teleportation '
                   'target (e.g. disk and network).'),
    ('Aborted', 'The process running the machine has terminated abnormally. This may indicate a '
                'crash of the VM process in host execution context, or the VM process has been terminated '
                'externally.'),
    ('Running', 'The machine is currently being executed.'),
    ('Paused', 'Execution of the machine has been paused.'),
    ('Stuck', 'Execution of the machine has reached the \'Guru Meditation\' condition. This indicates a '
              'severe error in the hypervisor itself.'),
    ('Teleporting', 'The machine is about to be teleported to a different host or process. It is possible '
                    'to pause a machine in this state, but it will go to the TeleportingPausedVM state and it '
                    'will not be possible to resume it again unless the teleportation fails.'),
    ('LiveSnapshotting', 'A live snapshot is being taken. The machine is running normally, but some '
                         'of the runtime configuration options are inaccessible. '
                         'Also, if paused while in this state it will transition to OnlineSnapshotting '
                         'and it will not be resume the execution until the snapshot operation has completed.'),
    ('Starting', 'Machine is being started after powering it on from a zero execution state.'),
    ('Stopping', 'Machine is being normally stopped powering it off, or after the guest OS has initiated '
                 'a shutdown sequence.'),
    ('Saving', 'Machine is saving its execution state to a file.'),
    ('Restoring', 'Execution state of the machine is being restored from a file after powering it on from '
                  'the saved execution state.'),
    ('TeleportingPausedVM', 'The machine is being teleported to another host or process, but it is not '
                            'running. This is the paused variant of the Teleporting state.'),
    ('TeleportingIn', 'Teleporting the machine state in from another host or process.'),
    ('FaultTolerantSyncing', 'The machine is being synced with a fault tolerant VM running else-where.'),
    ('DeletingSnapshotOnline', 'Like DeletingSnapshot , but the merging of media is ongoing in the '
                               'background while the machine is running.'),
    ('DeletingSnapshotPaused', 'Like DeletingSnapshotOnline , but the machine was paused when '
                               'the merging of differencing media was started.'),
    ('OnlineSnapshotting', 'Like LiveSnapshotting , but the machine was paused when the merging '
                           'of differencing media was started.'),
    ('RestoringSnapshot', 'A machine snapshot is being restored; this typically does not take long.'),
    ('DeletingSnapshot', 'A machine snapshot is being deleted; this can take a long time since this may '
                         'require merging differencing media. This value indicates that the machine is not running '
                         'while the snapshot is being deleted.'),
    ('SettingUp', 'Lengthy setup operation is in progress.'),
    ('Snapshotting', 'Taking an (offline) snapshot.'),
    ('FirstOnline', 'Pseudo-state: first online state (for use in relational expressions).'),
    ('LastOnline', 'Pseudo-state: last online state (for use in relational expressions).'),
    ('FirstTransient', 'Pseudo-state: first transient state (for use in relational expressions).'),
    ('LastTransient', 'Pseudo-state: last transient state (for use in relational expressions).'),
]
MACHINE_STATES = dict(MACHINE_STATE_LIST)

'''
Dict of states {
    <number>: ( <name>, <description> )
}
'''
MACHINE_STATES_ENUM = dict(enumerate(MACHINE_STATE_LIST))


def vb_get_manager():
    '''
    Creates a 'singleton' manager to communicate with a local virtualbox hypervisor.
    @return:
    @rtype: VirtualBoxManager
    '''
    global _virtualboxManager
    if _virtualboxManager is None and HAS_LIBS:
        salt.utils.compat.reload(vboxapi)
        _virtualboxManager = vboxapi.VirtualBoxManager(None, None)

    return _virtualboxManager


def vb_get_box():
    '''
    Needed for certain operations in the SDK e.g creating sessions
    @return:
    @rtype: IVirtualBox
    '''
    vb_get_manager()

    try:
        # This works in older versions of the SDK, but does not seem to work anymore.
        vbox = _virtualboxManager.vbox
    except AttributeError:
        vbox = _virtualboxManager.getVirtualBox()

    return vbox


def vb_get_max_network_slots():
    '''
    Max number of slots any machine can have
    @return:
    @rtype: number
    '''
    sysprops = vb_get_box().systemProperties
    totals = [
        sysprops.getMaxNetworkAdapters(adapter_type)
        for adapter_type in [
            1,  # PIIX3 A PIIX3 (PCI IDE ISA Xcelerator) chipset.
            2  # ICH9 A ICH9 (I/O Controller Hub) chipset
        ]
        ]
    return sum(totals)


def vb_get_network_adapters(machine_name=None, machine=None):
    '''
    A valid machine_name or a machine is needed to make this work!

    @param machine_name:
    @type machine_name: str
    @param machine:
    @type machine: IMachine
    @return: INetorkAdapter's converted to dicts
    @rtype: [dict]
    '''

    if machine_name:
        machine = vb_get_box().findMachine(machine_name)
    network_adapters = []

    for i in range(vb_get_max_network_slots()):
        try:
            inetwork_adapter = machine.getNetworkAdapter(i)
            network_adapter = vb_xpcom_to_attribute_dict(
                inetwork_adapter, 'INetworkAdapter'
            )
            network_adapter['properties'] = inetwork_adapter.getProperties('')
            network_adapters.append(network_adapter)
        except Exception:
            pass

    return network_adapters


def vb_wait_for_network_address(timeout, step=None, machine_name=None, machine=None, wait_for_pattern=None):
    '''
    Wait until a machine has a network address to return or quit after the timeout

    @param timeout: in seconds
    @type timeout: float
    @param step: How regularly we want to check for ips (in seconds)
    @type step: float
    @param machine_name:
    @type machine_name: str
    @param machine:
    @type machine: IMachine
    @type wait_for_pattern: str
    @param wait_for_pattern:
    @type machine: str
    @return:
    @rtype: list
    '''
    kwargs = {
        'machine_name': machine_name,
        'machine': machine,
        'wait_for_pattern': wait_for_pattern
    }
    return wait_for(vb_get_network_addresses, timeout=timeout, step=step, default=[], func_kwargs=kwargs)


def _check_session_state(xp_session, expected_state='Unlocked'):
    '''
    @param xp_session:
    @type xp_session: ISession from the Virtualbox API
    @param expected_state: The constant descriptor according to the docs
    @type expected_state: str
    @return:
    @rtype: bool
    '''
    state_value = getattr(_virtualboxManager.constants, 'SessionState_' + expected_state)
    return xp_session.state == state_value


def vb_wait_for_session_state(xp_session, state='Unlocked', timeout=10, step=None):
    '''
    Waits until a session state has been reached, checking at regular intervals.

    @param xp_session:
    @type xp_session: ISession from the Virtualbox API
    @param state: The constant descriptor according to the docs
    @type state: str
    @param timeout: in seconds
    @type timeout: int | float
    @param step: Intervals at which the value is checked
    @type step: int | float
    @return: Did we reach the state?
    @rtype: bool
    '''
    args = (xp_session, state)
    wait_for(_check_session_state, timeout=timeout, step=step, default=False, func_args=args)


def vb_get_network_addresses(machine_name=None, machine=None, wait_for_pattern=None):
    '''
    TODO distinguish between private and public addresses

    A valid machine_name or a machine is needed to make this work!

    !!!
    Guest prerequisite: GuestAddition
    !!!

    Thanks to Shrikant Havale for the StackOverflow answer http://stackoverflow.com/a/29335390

    More information on guest properties: https://www.virtualbox.org/manual/ch04.html#guestadd-guestprops

    @param machine_name:
    @type machine_name: str
    @param machine:
    @type machine: IMachine
    @return: All the IPv4 addresses we could get
    @rtype: str[]
    '''
    if machine_name:
        machine = vb_get_box().findMachine(machine_name)

    ip_addresses = []
    log.debug("checking for power on:")
    if machine.state == _virtualboxManager.constants.MachineState_Running:

        log.debug("got power on:")

        #wait on an arbitrary named property
        #for instance use a dhcp client script to set a property via VBoxControl guestproperty set dhcp_done 1
        if wait_for_pattern and not machine.getGuestPropertyValue(wait_for_pattern):
            log.debug("waiting for pattern:%s:", wait_for_pattern)
            return None

        _total_slots = machine.getGuestPropertyValue('/VirtualBox/GuestInfo/Net/Count')

        #upon dhcp the net count drops to 0 and it takes some seconds for it to be set again
        if not _total_slots:
            log.debug("waiting for net count:%s:", wait_for_pattern)
            return None

        try:
            total_slots = int(_total_slots)
            for i in range(total_slots):
                try:
                    address = machine.getGuestPropertyValue('/VirtualBox/GuestInfo/Net/{0}/V4/IP'.format(i))
                    if address:
                        ip_addresses.append(address)
                except Exception as e:
                    log.debug(e.message)
        except ValueError as e:
            log.debug(e.message)
            return None

    log.debug("returning ip_addresses:%s:", ip_addresses)
    return ip_addresses


def vb_list_machines(**kwargs):
    '''
    Which machines does the hypervisor have
    @param kwargs: Passed to vb_xpcom_to_attribute_dict to filter the attributes
    @type kwargs: dict
    @return: Untreated dicts of the machines known to the hypervisor
    @rtype: [{}]
    '''
    manager = vb_get_manager()
    machines = manager.getArray(vb_get_box(), 'machines')
    return [
        vb_xpcom_to_attribute_dict(machine, 'IMachine', **kwargs)
        for machine in machines
        ]


def vb_create_machine(name=None):
    '''
    Creates a machine on the virtualbox hypervisor

    TODO pass more params to customize machine creation
    @param name:
    @type name: str
    @return: Representation of the created machine
    @rtype: dict
    '''
    vbox = vb_get_box()
    log.info('Create virtualbox machine %s ', name)
    groups = None
    os_type_id = 'Other'
    new_machine = vbox.createMachine(
        None,  # Settings file
        name,
        groups,
        os_type_id,
        None  # flags
    )
    vbox.registerMachine(new_machine)
    log.info('Finished creating %s', name)
    return vb_xpcom_to_attribute_dict(new_machine, 'IMachine')


def vb_clone_vm(
    name=None,
    clone_from=None,
    clone_mode=0,
    timeout=10000,
    **kwargs
):
    '''
    Tells virtualbox to create a VM by cloning from an existing one

    @param name: Name for the new VM
    @type name: str
    @param clone_from:
    @type clone_from: str
    @param timeout: maximum time in milliseconds to wait or -1 to wait indefinitely
    @type timeout: int
    @return dict of resulting VM
    '''
    vbox = vb_get_box()
    log.info('Clone virtualbox machine %s from %s', name, clone_from)

    source_machine = vbox.findMachine(clone_from)

    groups = None
    os_type_id = 'Other'
    new_machine = vbox.createMachine(
        None,  # Settings file
        name,
        groups,
        os_type_id,
        None  # flags
    )

    progress = source_machine.cloneTo(
        new_machine,
        clone_mode,  # CloneMode
        None  # CloneOptions : None = Full?
    )

    progress.waitForCompletion(timeout)
    log.info('Finished cloning %s from %s', name, clone_from)

    vbox.registerMachine(new_machine)

    return vb_xpcom_to_attribute_dict(new_machine, 'IMachine')


def _start_machine(machine, session):
    '''
    Helper to try and start machines

    @param machine:
    @type machine: IMachine
    @param session:
    @type session: ISession
    @return:
    @rtype: IProgress or None
    '''
    try:
        return machine.launchVMProcess(session, '', '')
    except Exception as e:
        log.debug(e.message, exc_info=True)
        return None


def vb_start_vm(name=None, timeout=10000, **kwargs):
    '''
    Tells Virtualbox to start up a VM.
    Blocking function!

    @param name:
    @type name: str
    @param timeout: Maximum time in milliseconds to wait or -1 to wait indefinitely
    @type timeout: int
    @return untreated dict of started VM
    '''
    # Time tracking
    start_time = time.time()
    timeout_in_seconds = timeout / 1000
    max_time = start_time + timeout_in_seconds

    vbox = vb_get_box()
    machine = vbox.findMachine(name)
    session = _virtualboxManager.getSessionObject(vbox)

    log.info('Starting machine %s in state %s', name, vb_machinestate_to_str(machine.state))
    try:
        # Keep trying to start a machine
        args = (machine, session)
        progress = wait_for(_start_machine, timeout=timeout_in_seconds, func_args=args)
        if not progress:
            progress = machine.launchVMProcess(session, '', '')

        # We already waited for stuff, don't push it
        time_left = max_time - time.time()
        progress.waitForCompletion(time_left * 1000)
    finally:
        _virtualboxManager.closeMachineSession(session)

    # The session state should best be unlocked otherwise subsequent calls might cause problems
    time_left = max_time - time.time()
    vb_wait_for_session_state(session, timeout=time_left)
    log.info('Started machine %s', name)

    return vb_xpcom_to_attribute_dict(machine, 'IMachine')


def vb_stop_vm(name=None, timeout=10000, **kwargs):
    '''
    Tells Virtualbox to stop a VM.
    This is a blocking function!

    @param name:
    @type name: str
    @param timeout: Maximum time in milliseconds to wait or -1 to wait indefinitely
    @type timeout: int
    @return untreated dict of stopped VM
    '''
    vbox = vb_get_box()
    machine = vbox.findMachine(name)
    log.info('Stopping machine %s', name)
    session = _virtualboxManager.openMachineSession(machine)
    try:
        console = session.console
        progress = console.powerDown()
        progress.waitForCompletion(timeout)
    finally:
        _virtualboxManager.closeMachineSession(session)
        vb_wait_for_session_state(session)
    log.info('Stopped machine %s is now %s', name, vb_machinestate_to_str(machine.state))
    return vb_xpcom_to_attribute_dict(machine, 'IMachine')


def vb_destroy_machine(name=None, timeout=10000):
    '''
    Attempts to get rid of a machine and all its files from the hypervisor
    @param name:
    @type name: str
    @param timeout int timeout in milliseconds
    '''
    vbox = vb_get_box()
    log.info('Destroying machine %s', name)
    machine = vbox.findMachine(name)
    files = machine.unregister(2)
    progress = machine.deleteConfig(files)
    progress.waitForCompletion(timeout)
    log.info('Finished destroying machine %s', name)


def vb_xpcom_to_attribute_dict(xpcom,
                               interface_name=None,
                               attributes=None,
                               excluded_attributes=None,
                               extra_attributes=None
                               ):
    '''
    Attempts to build a dict from an XPCOM object.
    Attributes that don't exist in the object return an empty string.

    attribute_list = list of str or tuple(str,<a class>)

    e.g attributes=[('bad_attribute', list)] --> { 'bad_attribute': [] }

    @param xpcom:
    @type xpcom:
    @param interface_name: Which interface we will be converting from.
                           Without this it's best to specify the list of attributes you want
    @type interface_name: str
    @param attributes: Overrides the attributes used from XPCOM_ATTRIBUTES
    @type attributes: attribute_list
    @param excluded_attributes: Which should be excluded in the returned dict.
                                !!These take precedence over extra_attributes!!
    @type excluded_attributes: attribute_list
    @param extra_attributes: Which should be retrieved in addition those already being retrieved
    @type extra_attributes: attribute_list
    @return:
    @rtype: dict
    '''
    # Check the interface
    if interface_name:
        m = re.search(r'XPCOM.+implementing {0}'.format(interface_name), six.text_type(xpcom))
        if not m:
            # TODO maybe raise error here?
            log.warning('Interface %s is unknown and cannot be converted to dict', interface_name)
            return dict()

    interface_attributes = set(attributes or XPCOM_ATTRIBUTES.get(interface_name, []))
    if extra_attributes:
        interface_attributes = interface_attributes.union(extra_attributes)
    if excluded_attributes:
        interface_attributes = interface_attributes.difference(excluded_attributes)

    attribute_tuples = []
    for attribute in interface_attributes:
        if isinstance(attribute, tuple):
            attribute_name = attribute[0]
            attribute_class = attribute[1]
            value = (attribute_name, getattr(xpcom, attribute_name, attribute_class()))
        else:
            value = (attribute, getattr(xpcom, attribute, ''))
        attribute_tuples.append(value)

    return dict(attribute_tuples)


def treat_machine_dict(machine):
    '''
    Make machine presentable for outside world.

    !!!Modifies the input machine!!!

    @param machine:
    @type machine: dict
    @return: the modified input machine
    @rtype: dict
    '''
    machine.update({
        'id': machine.get('id', ''),
        'image': machine.get('image', ''),
        'size': '{0} MB'.format(machine.get('memorySize', 0)),
        'state': machine_get_machinestate_str(machine),
        'private_ips': [],
        'public_ips': [],
    })

    # Replaced keys
    if 'memorySize' in machine:
        del machine['memorySize']
    return machine


def vb_machinestate_to_str(machinestate):
    '''
    Put a name to the state

    @param machinestate: from the machine state enum from XPCOM
    @type machinestate: int
    @return:
    @rtype: str
    '''

    return vb_machinestate_to_tuple(machinestate)[0]


def vb_machinestate_to_description(machinestate):
    '''
    Describe the given state

    @param machinestate: from the machine state enum from XPCOM
    @type machinestate: int | str
    @return:
    @rtype: str
    '''
    return vb_machinestate_to_tuple(machinestate)[1]


def vb_machinestate_to_tuple(machinestate):
    '''

    @param machinestate:
    @type machinestate: int | str
    @return:
    @rtype: tuple(<name>, <description>)
    '''
    if isinstance(machinestate, int):
        ret = MACHINE_STATES_ENUM.get(machinestate, UNKNOWN_MACHINE_STATE)
    elif isinstance(machinestate, six.string_types):
        ret = MACHINE_STATES.get(machinestate, UNKNOWN_MACHINE_STATE)
    else:
        ret = UNKNOWN_MACHINE_STATE
    return salt.utils.data.decode(ret, preserve_tuples=True)


def machine_get_machinestate_tuple(machinedict):
    return vb_machinestate_to_tuple(machinedict.get('state'))


def machine_get_machinestate_str(machinedict):
    return vb_machinestate_to_str(machinedict.get('state'))


def vb_machine_exists(name):
    '''
    Checks in with the hypervisor to see if the machine with the given name is known
    @param name:
    @type name:
    @return:
    @rtype:
    '''
    try:
        vbox = vb_get_box()
        vbox.findMachine(name)
        return True
    except Exception as e:
        if isinstance(e.message, six.string_types):
            message = e.message
        elif hasattr(e, 'msg') and isinstance(getattr(e, 'msg'), six.string_types):
            message = getattr(e, 'msg')
        else:
            message = ''
        if 0 > message.find('Could not find a registered machine named'):
            log.error(message)

        return False


def vb_get_machine(name, **kwargs):
    '''
    Attempts to fetch a machine from Virtualbox and convert it to a dict

    @param name: The unique name of the machine
    @type name:
    @param kwargs: To be passed to vb_xpcom_to_attribute_dict
    @type kwargs:
    @return:
    @rtype: dict
    '''
    vbox = vb_get_box()
    machine = vbox.findMachine(name)
    return vb_xpcom_to_attribute_dict(machine, 'IMachine', **kwargs)