Skip to content
election.py 8.51 KiB
Newer Older
#!/usr/bin/env python
#coding=utf8

'''
This module contains the hypervisor destination election stuff.
'''

from __future__ import absolute_import

from copy import copy

from ccserver.exceptions import (UnknownElectionAlgo, UnknownElectionType,
                                 ElectionError)

def tags(*args):
    '''
    Decorator used to declare tags used by a filter.
    '''

    def decorator(func):
        func.__tags__ = set(args)
        return func
    return decorator

class Elector(object):

    # Filtering function for destination hypervisors:
    FILTERS = {
        'cold': (('is_hv', 'filter r=hv'),
                 ('not_source_hv', 'filter source hv'),
                 ('is_connected', 'filter connected hv'),
                 ('vm_htype_eq_hv', 'filter bad hv types'),
                 ('has_rights', 'filter rights'),
                 ('has_alloc', 'filter allocatable hv'),
                 ('duplicate_name', 'filter vm duplicate names'),
                 ('enough_disk', 'filter hv with not enough disk'),),
    }

    # Available algos for each types:
    ALGO_BY_TYPES = {'cold': ('fair', )}

    def __init__(self, server, query_vm, query_dest, login):
        # The server instance for this election:
        self._server = server

        # The TQL query to select vm:
        self._query_vm = query_vm

        # The TQL query to select destination hypervisor:
        self._query_dest = query_dest

        # The login of the initiator of the election:
        self._login = login

    def election(self, mtype, algo):
        '''
        Generate a new migration plan for this election. You must specify the
        migration type and the distribution algoritm.

        :param mtype: the migration type
        :param algo: the distribution algoritm
        '''

        # Check the choosen election method:
        if mtype not in self.ALGO_BY_TYPES:
            raise UnknownElectionType('%r is unknown migration type' % mtype)
        else:
            if algo not in self.ALGO_BY_TYPES[mtype]:
                raise UnknownElectionAlgo('%r is unknown migration algo' % algo)
            else:
                func = '_algo_%s' % algo
                if not hasattr(self, func):
                    raise UnknownElectionAlgo('%r not found' % func)
                else:
                    distribute = getattr(self, func)

        # Get the destination hypervisor candidates:
        candidates = self._get_candidates(self.FILTERS[mtype])

        # Distributes VMs to each candidate:
        migration_plan = distribute(candidates)

        # Return the migration plan:
        return migration_plan

    def _get_candidates(self, filters):
        # Get all the tags needed for hypervisors and construct the final
        # filter list:
        filterfuncs = []
        hv_tags = set()

        for name, desc in filters:
            filterfunc = getattr(self, '_filter_%s' % name)
            filterfuncs.append((filterfunc, desc))
            hv_tags |= getattr(filterfunc, '__tags__', set())

        # Get the selected vms and hvs:
        vms = self._server.list(self._query_vm, show=('*',))
        hvs = self._server.list(self._query_dest, show=hv_tags)

        candidates = []

        # Filters the candidates:
        for vm in vms:
            if vm['r'] != 'vm':
                continue
            tql = 'id=%s' % vm['id']
            if not self._server.check(self._login, 'coldmigrate', tql):
                continue
            vm_dest = copy(hvs)
            for func, desc in filterfuncs:
                vm_dest = func(vm, vm_dest)
            candidates.append((vm, vm_dest))

        return candidates

#####
##### Distribution algorithm methods:
#####

    def _algo_fair(self, candidates):
        migration_plan = []
        
        # Sort vm by number of destination hv:
        candidates = sorted(candidates, key=lambda x: len(x[1]))
        
        hv_alloc = {}
        for vm, hvs in candidates:
            if not hvs:
                raise ElectionError('No destination found for %r vm' % vm['id'])

            else:
                # Try to take an hypervisor that is not already in the plan:
                for hv in hvs:
                    if hv['id'] not in hv_alloc:
                        break
                else:
                    # If all candidates for this VM are already in the
                    # migration plan, we take the less allocated:
                    hv = min(hvs, key=lambda x: hv_alloc[x['id']])

                migration_plan.append({'sid': vm['id'], 'did': hv['id'],
                                       'type': 'cold'})
                hv_alloc[hv['id']] = hv_alloc.get(hv['id'], 0) + 1

        return migration_plan

#####
##### Filtering methods:
#####

    @tags('r')
    def _filter_is_hv(self, vm, hvs):
        returned = []
        for hv in hvs:
            if hv.get('r') == 'hv':
                returned.append(hv)
        return returned

    @tags('con')
    def _filter_is_connected(self, vm, hvs):
        returned = []
        for hv in hvs:
            if hv.get('con'):
                returned.append(hv)
        return returned

    @tags('htype')
    def _filter_vm_htype_eq_hv(self, vm, hvs):
        returned = []
        vm_htype = vm.get('htype')
        for hv in hvs:
            if hv.get('htype') == vm_htype:
                returned.append(hv)
        return returned

    @tags('id')
    def _filter_not_source_hv(self, vm, hvs):
        returned = []
        hv_id, _, _ = vm['id'].partition('.')
        for hv in hvs:
            if hv['id'] != hv_id:
                returned.append(hv)
        return returned

    def _filter_has_rights(self, vm, hvs):
        returned = []
        for hv in hvs:
            tql = 'id=%s' % hv['id']
            if self._server.check(self._login, 'coldmigrate_dest', tql):
                returned.append(hv)
        return returned

    @tags('alloc')
    def _filter_has_alloc(self, vm, hvs):
        returned = []
        for hv in hvs:
            if hv.get('alloc', False):
                returned.append(hv)
        return returned

    def _filter_duplicate_name(self, vm, hvs):
        returned = []
        hv_id, _, vm_name = vm['id'].partition('.')
        for hv in hvs:
            vms = self._server.list('id:%s.*$h' % hv['id'])
            duplicate = False
            for vm in vms:
                if vm.get('h') == vm_name:
                    duplicate = True
                    break
            if not duplicate:
                returned.append(hv)
        return returned

    @tags('memfree')
    def _filter_enough_ram(self, vm, hvs):
        returned = []
        for hv in hvs:
            if int(hv.get('memfree', 0)) >= int(vm.get('mem', 0)):
                returned.append(hv)
        return returned

    @tags('cpu')
    def _filter_enough_core(self, vm, hvs):
        returned = []
        for hv in hvs:
            # Calculate the total number of vcpu used by VMs:
            vms = self.manager.server.list('id:%s.*$cpu' % hv['id'])
            count = 0
            for vm in vms:
                count += int(vm.get('cpu', 0))
            if int(hv.get('cpu', 0)) >= count + int(self.vm.get('cpu', 0)):
                returned.append(hv)
        return returned

    @tags('*')
    def _filter_enough_disk(self, vm, hvs):
        returned = []

        return hvs

        # Calculate the size needed for each pools:
        pools = {}
        for disk in vm['disk'].split():
            size = vm['disk%s_size' % disk]
            pool = vm['disk%s_pool' % disk]
            vol = vm['disk%s_vol' % disk]
            dpool = pools.get(pool, {'size': 0, 'vols': set()})
            dpool['size'] += int(size)
            dpool['vols'].add(vol)
            pools[pool] = dpool

        # Check for each HV if it match:
        for hv in hvs:
            good = True
            for pool, prop in pools.iteritems():
                free = int(hv.get('sto%s_free' % pool, 0))
                vols = set(hv.get('sto%s_vol' % pool, '').split())

                if free < prop['size'] or prop['vols'] & vols:
                    good = False

            if good:
                returned.append(hv)
        return returned

    def _filter_not_locked(self, vm, hvs):
        if len(hvs) == 1:
            return hvs
        
        returned = []
        for hv in hvs:
            lock = self.manager.server.get_connection(hv['id']).lock
            if not lock.locked():
                returned.append(hv)

        if len(returned) == 0:
            return hvs

        return returned

    def _filter_biggest_id(self, hvs):
        return sorted(hvs, key=lambda x: x['id'])[-1]