Skip to content
lib.py 14.4 KiB
Newer Older
# This file is part of CloudControl.
#
# CloudControl is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# CloudControl is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with CloudControl.  If not, see <http://www.gnu.org/licenses/>.


"""Helpers for libvirt."""

Anael Beutot's avatar
Anael Beutot committed
import logging
from itertools import chain, imap
from StringIO import StringIO
from xml.etree import cElementTree as et
Anael Beutot's avatar
Anael Beutot committed
import pyev
import libvirt
Anael Beutot's avatar
Anael Beutot committed
from cloudcontrol.common.client.tags import Tag
from cloudcontrol.node.utils import Singleton
from cloudcontrol.node.exc import PoolStorageError
Anael Beutot's avatar
Anael Beutot committed
logger = logging.getLogger(__name__)


#: corresponding name for enum state of libvirt domains
# see http://libvirt.org/html/libvirt-libvirt.html#virDomainState
DOMAIN_STATES = (
    'stopped',  # 0 no state
    'running',  # 1 running
    'blocked',  # 2 blocked
    'paused',  # 3 paused
    'running',  # 4 shutdown
    'stopped',  # 5 shuttoff
    'crashed',  # 6 crashed
    'suspended',  # 7 suspended
STORAGE_STATES = (
    'inactive',
    'building',
    'running',
    'degraded',
    'inaccessible',
    '???',  # 5
)


Anael Beutot's avatar
Anael Beutot committed
#: libvirt events
EVENTS = (
    'Added',
    'Removed',
    'Started',
    'Suspended',
    'Resumed',
    'Stopped',
    'Saved',
    'Restored',
)


Anael Beutot's avatar
Anael Beutot committed
# following event loop implementation was inspired by libvirt python example
Anael Beutot's avatar
Anael Beutot committed
# but updated to work with libev
Anael Beutot's avatar
Anael Beutot committed
class LoopHandler(object):
    """This class contains the data we need to track for a single file handle.

    """
Anael Beutot's avatar
Anael Beutot committed
    def __init__(self, loop, handle, fd, events, cb, opaque):
        self.handle = handle
        self.fd = fd
        self._events = self.virt_to_ev(events)
Anael Beutot's avatar
Anael Beutot committed
        self._cb = cb
        self.opaque = opaque
        self.watcher = loop.io(self.fd, self._events, self.ev_cb,
                               None, pyev.EV_MAXPRI)
    def ev_to_virt(self, events):
        """Convert libev events into libvirt one."""
        result = 0
        if events & pyev.EV_READ:
            result |= libvirt.VIR_EVENT_HANDLE_READABLE
        if events & pyev.EV_WRITE:
            result |= libvirt.VIR_EVENT_HANDLE_WRITABLE

        return result

    def virt_to_ev(self, events):
        """Convert libvirt event to libev one."""
        result = 0
        if events & (libvirt.VIR_EVENT_HANDLE_READABLE |
                     libvirt.VIR_EVENT_HANDLE_ERROR |
                     libvirt.VIR_EVENT_HANDLE_HANGUP):
            result |= pyev.EV_READ
        if events & libvirt.VIR_EVENT_HANDLE_WRITABLE:
            result |= pyev.EV_WRITE

        return result

Anael Beutot's avatar
Anael Beutot committed
    def _set(self):
        self.watcher.stop()
        if self._events != 0:
            self.watcher.set(self.fd, self._events)
            self.watcher.start()
Anael Beutot's avatar
Anael Beutot committed
    @property
    def events(self):
        return self._events
Anael Beutot's avatar
Anael Beutot committed
    @events.setter
    def events(self, events):
        self._events = self.virt_to_ev(events)
Anael Beutot's avatar
Anael Beutot committed
        self._set()
Anael Beutot's avatar
Anael Beutot committed
    def start(self):
        self.watcher.start()
Anael Beutot's avatar
Anael Beutot committed
    def stop(self):
        self.watcher.stop()
Anael Beutot's avatar
Anael Beutot committed
    def ev_cb(self, watcher, revents):
        # convert events
        events = self.ev_to_virt(revents)
Anael Beutot's avatar
Anael Beutot committed
        self._cb(self.handle, self.watcher.fd, events, self.opaque[0],
                 self.opaque[1])
Anael Beutot's avatar
Anael Beutot committed
class LoopTimer(object):
    """This class contains the data we need to track for a single periodic
    timer.

    """
Anael Beutot's avatar
Anael Beutot committed
    def __init__(self, loop, timer, interval, cb, opaque):
        self.timer = timer
Anael Beutot's avatar
Anael Beutot committed
        self._interval = float(interval)
        self._cb = cb
        self.opaque = opaque
Anael Beutot's avatar
Anael Beutot committed
        self.watcher = None
        self.loop = loop
        self._set()
Anael Beutot's avatar
Anael Beutot committed
    def _set(self):
        self.stop()
        if self._interval >= 0.:  # libvirt sends us interval == -1
            self.watcher = self.loop.timer(self._interval, self._interval,
                                           self.ev_cb, None, pyev.EV_MAXPRI)
Anael Beutot's avatar
Anael Beutot committed
        else:
            self.watcher = None
        self.start()
Anael Beutot's avatar
Anael Beutot committed
    @property
    def interval(self):
        return self._interval
Anael Beutot's avatar
Anael Beutot committed
    @interval.setter
    def interval(self, value):
        self._interval = float(value)
        self._set()
Anael Beutot's avatar
Anael Beutot committed
    def start(self):
        if self.watcher is not None:
            self.watcher.start()
Anael Beutot's avatar
Anael Beutot committed
    def stop(self):
        if self.watcher is not None:
            self.watcher.stop()
Anael Beutot's avatar
Anael Beutot committed
    def ev_cb(self, *args):
        self._cb(self.timer, self.opaque[0], self.opaque[1])


class EventLoop(object):
Anael Beutot's avatar
Anael Beutot committed
    """This class is used as an interface between the libvirt event handling and
    the main pyev loop.
Anael Beutot's avatar
Anael Beutot committed
    It cannot be used from other threads.
Anael Beutot's avatar
Anael Beutot committed
    # singletion usage is very important because libvirt.virEventRegisterImpl
    # would segfaults on libvirt 0.8.X
    __metaclass__ = Singleton

Anael Beutot's avatar
Anael Beutot committed
    def __init__(self, loop):
Anael Beutot's avatar
Anael Beutot committed
        :param loop: pyev loop instance
Anael Beutot's avatar
Anael Beutot committed
        self.loop = loop
Anael Beutot's avatar
Anael Beutot committed
        def counter():
            count = 0
            while True:
                yield count
                count += 1
Anael Beutot's avatar
Anael Beutot committed
        self.handle_id = counter()
        self.timer_id = counter()
        self.handles = dict()
        self.timers = dict()
        # This tells libvirt what event loop implementation it
        # should use
        libvirt.virEventRegisterImpl(
            self.add_handle,
            self.update_handle,
            self.remove_handle,
            self.add_timer,
            self.update_timer,
            self.remove_timer,
        )

    def add_handle(self, fd, events, cb, opaque):
        """Registers a new file handle 'fd', monitoring  for 'events' (libvirt
        event constants), firing the callback  cb() when an event occurs.
        Returns a unique integer identier for this handle, that should be
        used to later update/remove it.

Anael Beutot's avatar
Anael Beutot committed
        Note: unlike in the libvirt example, we don't use an interrupt trick as
        we run everything in the same thread, furthermore, calling start watcher
        method from a different thread could be dangerous.
Anael Beutot's avatar
Anael Beutot committed
        handle_id = self.handle_id.next()
Anael Beutot's avatar
Anael Beutot committed
        h = LoopHandler(self.loop, handle_id, fd, events, cb, opaque)
        h.start()
        self.handles[handle_id] = h
        # logger.debug('Add handle %d fd %d events %d', handle_id, fd, events)
Anael Beutot's avatar
Anael Beutot committed
        return handle_id

    def add_timer(self, interval, cb, opaque):
        """Registers a new timer with periodic expiry at 'interval' ms,
        firing cb() each time the timer expires. If 'interval' is -1,
        then the timer is registered, but not enabled.
        Returns a unique integer identier for this handle, that should be
        used to later update/remove it.

Anael Beutot's avatar
Anael Beutot committed
        Note: same note as for :py:meth:`add_handle` applies here
Anael Beutot's avatar
Anael Beutot committed
        timer_id = self.timer_id.next()
Anael Beutot's avatar
Anael Beutot committed
        h = LoopTimer(self.loop, timer_id, interval, cb, opaque)
        h.start()
        self.timers[timer_id] = h
        # logger.debug('Add timer %d interval %d', timer_id, interval)
Anael Beutot's avatar
Anael Beutot committed
        return timer_id
Anael Beutot's avatar
Anael Beutot committed
    def update_handle(self, handle_id, events):
        """Change the set of events to be monitored on the file handle.

        """
Anael Beutot's avatar
Anael Beutot committed
        h = self.handles.get(handle_id)
        if h:
Anael Beutot's avatar
Anael Beutot committed
            h.events = events
            # logger.debug('Update handle %d fd %d events %d', handle_id, h.fd, events)
Anael Beutot's avatar
Anael Beutot committed
    def update_timer(self, timer_id, interval):
        """Change the periodic frequency of the timer.

        """
Anael Beutot's avatar
Anael Beutot committed
        t = self.timers.get(timer_id)
        if t:
            t.interval = interval
Anael Beutot's avatar
Anael Beutot committed
    def remove_handle(self, handle_id):
        """Stop monitoring for events on the file handle.

        """
Anael Beutot's avatar
Anael Beutot committed
        h = self.handles.pop(handle_id, None)
        if h:
            h.stop()
            # logger.debug('Remove handle %d', handle_id)
Anael Beutot's avatar
Anael Beutot committed
    def remove_timer(self, timer_id):
        """Stop firing the periodic timer.
Anael Beutot's avatar
Anael Beutot committed
        t = self.timers.pop(timer_id, None)
        if t:
            t.stop()
            # logger.debug('Remove timer %d', timer_id)
Anael Beutot's avatar
Anael Beutot committed
    def stop(self):
        for handl in chain(self.handles.itervalues(), self.timers.itervalues()):
            handl.stop()

        self.handles = dict()
        self.timers = dict()


class StorageIndex(object):
    """Keep an index of all storage volume paths."""
    def __init__(self, handler, lv_con):
        """
        :param handler: Hypervisor handler instance
        :param lv_con: Libvirt connection
        """
        self.handler = handler
        self.lv_con = lv_con
        self.storages = dict(
            (s.name, s) for s in imap(
                Storage,
                imap(
                    lv_con.storagePoolLookupByName,
                    chain(
                        lv_con.listDefinedStoragePools(),
                        lv_con.listStoragePools(),
                    ),
                ),
            ),
        )

        self.paths = None
        self.update_path_index()


    def update(self):
        """Update storage pools and volumes."""
        # go through all storage pools and check if it is already in the index
        for lv_storage in imap(
            self.lv_con.storagePoolLookupByName,
            chain(
                self.lv_con.listDefinedStoragePools(),
                self.lv_con.listStoragePools(),
            ),
        ):
            if lv_storage.name() in self.storages:
                # update
                self.storages[lv_storage.name()].update()
            else:
                # add storage pool
                s = Storage(lv_storage)
                self.storages[s.name] = s
                # add tags
                self.handler.tag_db.add_tags((
                    Tag('sto%s_state' % s.name, lambda: s.state, 5, 5),
                    Tag('sto%s_size' % s.name, lambda: s.capacity, 5, 5),
                    Tag('sto%s_free' % s.name, lambda: s.available, 5, 5),
                    Tag('sto%s_used' % s.name,
                        lambda: s.capacity - s.available, 5, 5),
                    Tag('sto%s_type' % s.name, lambda: s.type, 5, 5),
                ))

        self.update_path_index()

    def update_path_index(self):
        self.paths = dict(
            (v.path, v) for v in chain.from_iterable(imap(
                lambda s: s.volumes.itervalues(),
                self.storages.itervalues(),
            )),
        )

    def get_volume(self, path):
        return self.paths.get(path)

    def get_storage(self, name):
        return self.storages.get(name)

    def create_volume(self, pool_name, volume_name, capacity):
        """Create a new volume in the storage pool.

        :param str name: name for the volume
        :param int capacity: size for the volume
        """
        # get volume
        logger.debug('asked pool %s', pool_name)
        logger.debug('Pool state %s', self.storages)
        try:
            pool = self.storages[pool_name]
        except KeyError:
            raise PoolStorageError('Invalid pool name')
        if pool is None:
            raise Exception('Storage pool not found')
        try:
            new_volume = pool.lv_storage.createXML("""<volume>
                                        <name>%s</name>
                                        <capacity>%d</capacity>
                                      </volume>""" % (volume_name, capacity), 0)
        except libvirt.libvirtError:
            logger.exception('Error while creating volume')
            raise

        new_volume = Volume(new_volume)
        # if success add the volume to the index
        self.paths[new_volume.path] = new_volume
        # and also to its storage pool
        self.storages[new_volume.storage].volumes[new_volume.name] = new_volume

        return new_volume

    def delete_volume(self, pool_name, volume_name):
        """Delete a volume in the givent storage pool.

        :param str pool_name: name for the storage pool
        :param str volume_name: name for the volume
        """
        # get volume
        try:
            pool = self.storages[pool_name]
        except KeyError:
            raise PoolStorageError('Invalid pool name')
        try:
            volume = pool.volumes[volume_name]
        except KeyError:
            raise PoolStorageError('Invalid volume name')
        # delete from index
        del self.paths[volume.path]
        del self.storages[pool_name].volumes[volume_name]

        # delete volume
        try:
            volume.lv_volume.delete(0)
        except libvirt.libvirtError:
            logger.exception('Error while deleting volume')
            raise


class Storage(object):
    """Storage abstraction."""
    def __init__(self, lv_storage):
        """
        :param lv_storage: Libvirt pool storage instance
        """
        self.uuid = lv_storage.UUID()
        self.name = lv_storage.name()
        self.lv_storage = lv_storage

        self.state, self.capacity = None, None
        self.allocation, self.available = None, None

        self.type = et.ElementTree().parse(
            StringIO(lv_storage.XMLDesc(0))).get('type')

        self.volumes = dict((v.name, v) for v in imap(
            Volume,
            (lv_storage.storageVolLookupByName(n) for n in
             lv_storage.listVolumes()),
        ))

        self.update_attr()

    def update(self):
        self.update_attr()

        # update volumes
        for vol_name in self.lv_storage.listVolumes():
            if vol_name in self.volumes:
                # update volume
                self.volumes[vol_name].update()
            else:
                # add volume
                v = Volume(self.lv_storage.storageVolLookupByName(vol_name))
                self.volumes[v.name] = v

    def update_attr(self):
        self.state, self.capacity, self.allocation, self.available = self.lv_storage.info()
        self.state = STORAGE_STATES[self.state]

        self.type = et.ElementTree().parse(
            StringIO(self.lv_storage.XMLDesc(0))).get('type')



class Volume(object):
    """Volume abstraction."""
    def __init__(self, lv_volume):
        """
        :param lv_volume: Libvirt volume instance
        """
        self.storage = lv_volume.storagePoolLookupByVolume().name()
        self.path = lv_volume.path()
        self.name = lv_volume.name()
        self.capacity, self.allocation = None, None
        self.lv_volume = lv_volume
        self.update()

    def update(self):
        self.capacity, self.allocation = self.lv_volume.info()[1:]