From cb3632a80afc149775d917c098d81ee743904fc9 Mon Sep 17 00:00:00 2001 From: Masahito Muroi Date: Mon, 22 May 2017 19:54:03 +0900 Subject: [PATCH] Move the ReservationPool class to utils/openstack/nova.py The new instance reservation feature also uses reservation pools to handle compute hosts in the freepool. However, the reservation class is located under oshosts plugin directory. This commit makes the ReservationPool class more general by moving it to utils/openstack/nova.py. Partially implements: blueprint new-instance-reservation Change-Id: I0887574544ff0456022c90c4dea2c48b588dc3b8 --- blazar/opts.py | 7 +- blazar/plugins/oshosts/host_plugin.py | 28 +- blazar/plugins/oshosts/reservation_pool.py | 286 --------------- .../plugins/oshosts/test_reservation_pool.py | 336 ------------------ .../plugins/test_physical_host_plugin.py | 45 ++- blazar/tests/utils/openstack/test_nova.py | 315 ++++++++++++++++ blazar/utils/openstack/nova.py | 244 ++++++++++++- 7 files changed, 599 insertions(+), 662 deletions(-) delete mode 100644 blazar/plugins/oshosts/reservation_pool.py delete mode 100644 blazar/tests/plugins/oshosts/test_reservation_pool.py diff --git a/blazar/opts.py b/blazar/opts.py index df54a497..ce1c3084 100644 --- a/blazar/opts.py +++ b/blazar/opts.py @@ -24,7 +24,6 @@ import blazar.manager.service import blazar.notification.notifier import blazar.plugins.instances.vm_plugin import blazar.plugins.oshosts.host_plugin -import blazar.plugins.oshosts.reservation_pool import blazar.utils.openstack.keystone import blazar.utils.openstack.nova @@ -49,8 +48,6 @@ def list_opts(): ('nova', blazar.utils.openstack.nova.nova_opts), (blazar.plugins.instances.RESOURCE_TYPE, blazar.plugins.instances.vm_plugin.plugin_opts), - (blazar.plugins.oshosts.RESOURCE_TYPE, itertools.chain( - blazar.plugins.oshosts.host_plugin.plugin_opts, - blazar.plugins.oshosts.reservation_pool.OPTS)), - + (blazar.plugins.oshosts.RESOURCE_TYPE, + blazar.plugins.oshosts.host_plugin.plugin_opts), ] diff --git a/blazar/plugins/oshosts/host_plugin.py b/blazar/plugins/oshosts/host_plugin.py index 37ca4f50..0566b8fd 100644 --- a/blazar/plugins/oshosts/host_plugin.py +++ b/blazar/plugins/oshosts/host_plugin.py @@ -27,7 +27,6 @@ from blazar.manager import exceptions as manager_ex from blazar.plugins import base from blazar.plugins import oshosts as plugin from blazar.plugins.oshosts import nova_inventory -from blazar.plugins.oshosts import reservation_pool as rp from blazar.utils.openstack import nova from blazar.utils import trusts @@ -41,7 +40,12 @@ plugin_opts = [ default='on_start', deprecated_for_removal=True, deprecated_since='0.3.0', - help='Actions which we will use at the start of the lease') + help='Actions which we will use at the start of the lease'), + cfg.StrOpt('blazar_az_prefix', + default='blazar:', + deprecated_name='climate_az_prefix', + help='Prefix for Availability Zones created by Blazar'), + ] CONF = cfg.CONF @@ -53,7 +57,7 @@ class PhysicalHostPlugin(base.BasePlugin, nova.NovaClientWrapper): resource_type = plugin.RESOURCE_TYPE title = 'Physical Host Plugin' description = 'This plugin starts and shutdowns the hosts.' - freepool_name = CONF[resource_type].aggregate_freepool_name + freepool_name = CONF.nova.aggregate_freepool_name pool = None def __init__(self): @@ -66,8 +70,11 @@ class PhysicalHostPlugin(base.BasePlugin, nova.NovaClientWrapper): def reserve_resource(self, reservation_id, values): """Create reservation.""" - pool = rp.ReservationPool() - pool_instance = pool.create(name=reservation_id) + pool = nova.ReservationPool() + pool_name = reservation_id + az_name = "%s%s" % (CONF[self.resource_type].blazar_az_prefix, + pool_name) + pool_instance = pool.create(name=pool_name, az=az_name) min_hosts = values.get('min') max_hosts = values.get('max') if 0 <= min_hosts and min_hosts <= max_hosts: @@ -102,7 +109,6 @@ class PhysicalHostPlugin(base.BasePlugin, nova.NovaClientWrapper): """Update reservation.""" reservation = db_api.reservation_get(reservation_id) lease = db_api.lease_get(reservation['lease_id']) - host_reservation = None if (values['start_date'] < lease['start_date'] or values['end_date'] > lease['end_date']): allocations = [] @@ -130,7 +136,7 @@ class PhysicalHostPlugin(base.BasePlugin, nova.NovaClientWrapper): if allocations: host_reservation = db_api.host_reservation_get( reservation['resource_id']) - pool = rp.ReservationPool() + pool = nova.ReservationPool() hosts_in_pool.extend(pool.get_computehosts( host_reservation['aggregate_id'])) host_ids = self._matching_hosts( @@ -170,7 +176,7 @@ class PhysicalHostPlugin(base.BasePlugin, nova.NovaClientWrapper): def on_start(self, resource_id): """Add the hosts in the pool.""" host_reservation = db_api.host_reservation_get(resource_id) - pool = rp.ReservationPool() + pool = nova.ReservationPool() for allocation in db_api.host_allocation_get_all_by_values( reservation_id=host_reservation['reservation_id']): host = db_api.host_get(allocation['compute_host_id']) @@ -186,7 +192,7 @@ class PhysicalHostPlugin(base.BasePlugin, nova.NovaClientWrapper): reservation_id=host_reservation['reservation_id']) for allocation in allocations: db_api.host_allocation_destroy(allocation['id']) - pool = rp.ReservationPool() + pool = nova.ReservationPool() for host in pool.get_computehosts(host_reservation['aggregate_id']): for server in self.nova.servers.list( search_opts={"host": host}): @@ -250,7 +256,7 @@ class PhysicalHostPlugin(base.BasePlugin, nova.NovaClientWrapper): extra_capabilities = dict( (key, host_values[key]) for key in extra_capabilities_keys ) - pool = rp.ReservationPool() + pool = nova.ReservationPool() pool.add_computehost(self.freepool_name, host_details['service_name']) @@ -328,7 +334,7 @@ class PhysicalHostPlugin(base.BasePlugin, nova.NovaClientWrapper): host=host['hypervisor_hostname'], servers=servers) try: - pool = rp.ReservationPool() + pool = nova.ReservationPool() pool.remove_computehost(self.freepool_name, host['service_name']) # NOTE(sbauza): Extracapabilities will be destroyed thanks to diff --git a/blazar/plugins/oshosts/reservation_pool.py b/blazar/plugins/oshosts/reservation_pool.py deleted file mode 100644 index d347fb34..00000000 --- a/blazar/plugins/oshosts/reservation_pool.py +++ /dev/null @@ -1,286 +0,0 @@ -# Copyright (c) 2013 OpenStack Foundation -# -# Author: Swann Croiset -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import uuid as uuidgen - -from novaclient import exceptions as nova_exceptions -from oslo_config import cfg -from oslo_log import log as logging - -from blazar import context -from blazar.manager import exceptions as manager_exceptions -from blazar.plugins import oshosts as plugin -from blazar.utils.openstack import nova - - -LOG = logging.getLogger(__name__) - -OPTS = [ - cfg.StrOpt('aggregate_freepool_name', - default='freepool', - help='Name of the special aggregate where all hosts ' - 'are candidate for physical host reservation'), - cfg.StrOpt('project_id_key', - default='blazar:project', - help='Aggregate metadata value for key matching project_id'), - cfg.StrOpt('blazar_owner', - default='blazar:owner', - deprecated_name='climate_owner', - help='Aggregate metadata key for knowing owner project_id'), - cfg.StrOpt('blazar_az_prefix', - default='blazar:', - deprecated_name='climate_az_prefix', - help='Prefix for Availability Zones created by Blazar'), -] - -CONF = cfg.CONF -CONF.register_opts(OPTS, group=plugin.RESOURCE_TYPE) - - -class ReservationPool(nova.NovaClientWrapper): - def __init__(self): - super(ReservationPool, self).__init__( - username=CONF.os_admin_username, - password=CONF.os_admin_password, - user_domain_name=CONF.os_admin_user_domain_name, - project_name=CONF.os_admin_project_name, - project_domain_name=CONF.os_admin_user_domain_name) - self.config = CONF[plugin.RESOURCE_TYPE] - self.freepool_name = self.config.aggregate_freepool_name - - def get_aggregate_from_name_or_id(self, aggregate_obj): - """Return an aggregate by name or an id.""" - - aggregate = None - agg_id = None - try: - agg_id = int(aggregate_obj) - except (ValueError, TypeError): - if hasattr(aggregate_obj, 'id') and aggregate_obj.id: - # pool is an aggregate - agg_id = aggregate_obj.id - - if agg_id is not None: - try: - aggregate = self.nova.aggregates.get(agg_id) - except nova_exceptions.NotFound: - aggregate = None - else: - # FIXME(scroiset): can't get an aggregate by name - # so iter over all aggregate and check for the good one - all_aggregates = self.nova.aggregates.list() - for agg in all_aggregates: - if aggregate_obj == agg.name: - aggregate = agg - if aggregate: - return aggregate - else: - raise manager_exceptions.AggregateNotFound(pool=aggregate_obj) - - @staticmethod - def _generate_aggregate_name(): - return str(uuidgen.uuid4()) - - def create(self, name=None, az=True): - """Create a Pool (an Aggregate) with or without Availability Zone. - - By default expose to user the aggregate with an Availability Zone. - Return an aggregate or raise a nova exception. - - """ - - name = name or self._generate_aggregate_name() - - if az: - az_name = "%s%s" % (self.config.blazar_az_prefix, - name) - LOG.debug('Creating pool aggregate: %s ' - 'with Availability Zone %s' % (name, az_name)) - agg = self.nova.aggregates.create(name, az_name) - else: - LOG.debug('Creating pool aggregate: %s ' - 'without Availability Zone' % name) - agg = self.nova.aggregates.create(name, None) - - project_id = None - try: - ctx = context.current() - project_id = ctx.project_id - except RuntimeError: - e = manager_exceptions.ProjectIdNotFound() - LOG.error(e.message) - raise e - - meta = {self.config.blazar_owner: project_id} - self.nova.aggregates.set_metadata(agg, meta) - - return agg - - def delete(self, pool, force=True): - """Delete an aggregate. - - pool can be an aggregate name or an aggregate id. - Remove all hosts before delete aggregate (default). - If force is False, raise exception if at least one - host is attached to. - - """ - - agg = self.get_aggregate_from_name_or_id(pool) - - hosts = agg.hosts - if len(hosts) > 0 and not force: - raise manager_exceptions.AggregateHaveHost(name=agg.name, - hosts=agg.hosts) - try: - freepool_agg = self.get(self.freepool_name) - except manager_exceptions.AggregateNotFound: - raise manager_exceptions.NoFreePool() - for host in hosts: - LOG.debug("Removing host '%s' from aggregate " - "'%s')" % (host, agg.id)) - self.nova.aggregates.remove_host(agg.id, host) - - if freepool_agg.id != agg.id: - self.nova.aggregates.add_host(freepool_agg.id, host) - - self.nova.aggregates.delete(agg.id) - - def get_all(self): - """Return all aggregate.""" - - return self.nova.aggregates.list() - - def get(self, pool): - """return details for aggregate pool or raise AggregateNotFound.""" - - return self.get_aggregate_from_name_or_id(pool) - - def get_computehosts(self, pool): - """Return a list of compute host names for an aggregate.""" - - try: - agg = self.get_aggregate_from_name_or_id(pool) - return agg.hosts - except manager_exceptions.AggregateNotFound: - return [] - - def add_computehost(self, pool, host): - """Add a compute host to an aggregate. - - The `host` must exist otherwise raise an error - and the `host` must be in the freepool. - - :param pool: Name or UUID of the pool to rattach the host - :param host: Name (not UUID) of the host to associate - :type host: str - - Return the related aggregate. - Raise an aggregate exception if something wrong. - """ - - agg = self.get_aggregate_from_name_or_id(pool) - - try: - freepool_agg = self.get(self.freepool_name) - except manager_exceptions.AggregateNotFound: - raise manager_exceptions.NoFreePool() - - if freepool_agg.id != agg.id: - if host not in freepool_agg.hosts: - raise manager_exceptions.HostNotInFreePool( - host=host, freepool_name=freepool_agg.name) - LOG.info("removing host '%s' " - "from aggregate freepool %s" % (host, freepool_agg.name)) - try: - self.remove_computehost(freepool_agg.id, host) - except nova_exceptions.NotFound: - raise manager_exceptions.HostNotFound(host=host) - - LOG.info("adding host '%s' to aggregate %s" % (host, agg.id)) - try: - return self.nova.aggregates.add_host(agg.id, host) - except nova_exceptions.NotFound: - raise manager_exceptions.HostNotFound(host=host) - except nova_exceptions.Conflict: - raise manager_exceptions.AggregateAlreadyHasHost(pool=pool, - host=host) - - def remove_all_computehosts(self, pool): - """Remove all compute hosts attached to an aggregate.""" - - hosts = self.get_computehosts(pool) - self.remove_computehost(pool, hosts) - - def remove_computehost(self, pool, hosts): - """Remove compute host(s) from an aggregate.""" - - if not isinstance(hosts, list): - hosts = [hosts] - - agg = self.get_aggregate_from_name_or_id(pool) - - try: - freepool_agg = self.get(self.freepool_name) - except manager_exceptions.AggregateNotFound: - raise manager_exceptions.NoFreePool() - - hosts_failing_to_remove = [] - hosts_failing_to_add = [] - hosts_not_in_freepool = [] - for host in hosts: - if freepool_agg.id == agg.id: - if host not in freepool_agg.hosts: - hosts_not_in_freepool.append(host) - continue - try: - self.nova.aggregates.remove_host(agg.id, host) - except nova_exceptions.ClientException: - hosts_failing_to_remove.append(host) - if freepool_agg.id != agg.id: - # NOTE(sbauza) : We don't want to put again the host in - # freepool if the requested pool is the freepool... - try: - self.nova.aggregates.add_host(freepool_agg.id, host) - except nova_exceptions.ClientException: - hosts_failing_to_add.append(host) - - if hosts_failing_to_remove: - raise manager_exceptions.CantRemoveHost( - host=hosts_failing_to_remove, pool=agg) - if hosts_failing_to_add: - raise manager_exceptions.CantAddHost(host=hosts_failing_to_add, - pool=freepool_agg) - if hosts_not_in_freepool: - raise manager_exceptions.HostNotInFreePool( - host=hosts_not_in_freepool, freepool_name=freepool_agg.name) - - def add_project(self, pool, project_id): - """Add a project to an aggregate.""" - - metadata = {project_id: self.config.project_id_key} - - agg = self.get_aggregate_from_name_or_id(pool) - - return self.nova.aggregates.set_metadata(agg.id, metadata) - - def remove_project(self, pool, project_id): - """Remove a project from an aggregate.""" - - agg = self.get_aggregate_from_name_or_id(pool) - - metadata = {project_id: None} - return self.nova.aggregates.set_metadata(agg.id, metadata) diff --git a/blazar/tests/plugins/oshosts/test_reservation_pool.py b/blazar/tests/plugins/oshosts/test_reservation_pool.py deleted file mode 100644 index 2e6761f2..00000000 --- a/blazar/tests/plugins/oshosts/test_reservation_pool.py +++ /dev/null @@ -1,336 +0,0 @@ -# Copyright (c) 2013 Openstack Fondation -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import uuid as uuidgen - -from novaclient import client as nova_client -from novaclient import exceptions as nova_exceptions -from oslo_config import cfg - -from blazar import context -from blazar.manager import exceptions as manager_exceptions -from blazar.plugins import oshosts as host_plugin -from blazar.plugins.oshosts import reservation_pool as rp -from blazar import tests -from blazar.utils.openstack import base -from blazar.utils.openstack import nova - - -class AggregateFake(object): - - def __init__(self, i, name, hosts): - self.id = i - self.name = name - self.hosts = hosts - - -class ReservationPoolTestCase(tests.TestCase): - - def setUp(self): - super(ReservationPoolTestCase, self).setUp() - self.pool_name = 'pool-name-xxx' - self.project_id = 'project-uuid' - self.fake_aggregate = AggregateFake(i=123, - name='fooname', - hosts=['host1', 'host2']) - conf = cfg.CONF[host_plugin.RESOURCE_TYPE] - self.freepool_name = conf.aggregate_freepool_name - self.project_id_key = conf.project_id_key - self.blazar_owner = conf.blazar_owner - self.blazar_az_prefix = conf.blazar_az_prefix - - self.fake_freepool = AggregateFake(i=456, - name=self.freepool_name, - hosts=['host3']) - - self.set_context(context.BlazarContext(project_id=self.project_id)) - - self.nova_client = nova_client - self.nova = self.patch(self.nova_client, 'Client').return_value - - self.patch(self.nova.aggregates, 'set_metadata') - self.patch(self.nova.aggregates, 'remove_host') - - self.patch(base, 'url_for').return_value = 'http://foo.bar' - self.pool = rp.ReservationPool() - - self.p_name = self.patch(self.pool, '_generate_aggregate_name') - self.p_name.return_value = self.pool_name - - def _patch_get_aggregate_from_name_or_id(self): - def get_fake_aggregate(*args): - if self.freepool_name in args: - return self.fake_freepool - else: - return self.fake_aggregate - - patched_pool = self.patch(self.pool, 'get_aggregate_from_name_or_id') - patched_pool.side_effect = get_fake_aggregate - - def test_get_aggregate_from_name_or_id(self): - def fake_aggregate_get(id): - if id == self.fake_aggregate.id: - return self.fake_aggregate - else: - raise nova_exceptions.NotFound(id) - - self.nova.aggregates.list.return_value = [self.fake_aggregate] - self.nova.aggregates.get.side_effect = fake_aggregate_get - - self.assertRaises(manager_exceptions.AggregateNotFound, - self.pool.get_aggregate_from_name_or_id, 'none') - self.assertRaises(manager_exceptions.AggregateNotFound, - self.pool.get_aggregate_from_name_or_id, '3000') - self.assertEqual(self.pool.get_aggregate_from_name_or_id('fooname'), - self.fake_aggregate) - self.assertEqual( - self.pool.get_aggregate_from_name_or_id(self.fake_aggregate), - self.fake_aggregate) - - def test_generate_aggregate_name(self): - self.uuidgen = uuidgen - self.patch(uuidgen, 'uuid4').return_value = 'foo' - self.assertEqual('foo', rp.ReservationPool._generate_aggregate_name()) - - def test_create(self): - self.patch(self.nova.aggregates, 'create').return_value = ( - self.fake_aggregate) - - agg = self.pool.create() - - self.assertEqual(agg, self.fake_aggregate) - - az_name = self.blazar_az_prefix + self.pool_name - check0 = self.nova.aggregates.create - check0.assert_called_once_with(self.pool_name, az_name) - - meta = {self.blazar_owner: self.project_id} - check1 = self.nova.aggregates.set_metadata - check1.assert_called_once_with(self.fake_aggregate, meta) - - def test_create_no_az(self): - self.patch(self.nova.aggregates, 'create').return_value = ( - self.fake_aggregate) - - self.pool.create(az=False) - - self.nova.aggregates.create.assert_called_once_with(self.pool_name, - None) - - def test_create_no_project_id(self): - self.patch(self.nova.aggregates, 'create').return_value = ( - self.fake_aggregate) - - self.nova_wrapper = self.patch(nova.NovaClientWrapper, 'nova') - - def raiseRuntimeError(): - raise RuntimeError() - - self.context_mock.side_effect = raiseRuntimeError - - self.assertRaises(manager_exceptions.ProjectIdNotFound, - self.pool.create) - - def test_delete_with_host(self): - self._patch_get_aggregate_from_name_or_id() - agg = self.pool.get('foo') - - self.pool.delete(agg) - self.nova.aggregates.delete.assert_called_once_with(agg.id) - for host in agg.hosts: - self.nova.aggregates.remove_host.assert_any_call(agg.id, host) - self.nova.aggregates.add_host.assert_any_call( - self.fake_freepool.id, host - ) - - # can't delete aggregate with hosts - self.assertRaises(manager_exceptions.AggregateHaveHost, - self.pool.delete, 'bar', - force=False) - - def test_delete_with_no_host(self): - self._patch_get_aggregate_from_name_or_id() - agg = self.pool.get('foo') - agg.hosts = [] - self.pool.delete('foo', force=False) - self.nova.aggregates.delete.assert_called_once_with(agg.id) - - def test_delete_with_no_freepool(self): - def get_fake_aggregate_but_no_freepool(*args): - if self.freepool_name in args: - raise manager_exceptions.AggregateNotFound - else: - return self.fake_aggregate - fake_pool = self.patch(self.pool, 'get_aggregate_from_name_or_id') - fake_pool.side_effect = get_fake_aggregate_but_no_freepool - agg = self.pool.get('foo') - agg.hosts = [] - self.assertRaises(manager_exceptions.NoFreePool, - self.pool.delete, 'bar', - force=False) - - def test_get_all(self): - self.pool.get_all() - self.nova.aggregates.list.assert_called_once_with() - - def test_get(self): - self._patch_get_aggregate_from_name_or_id() - agg = self.pool.get('foo') - self.assertEqual(self.fake_aggregate, agg) - - def test_add_computehost(self): - self._patch_get_aggregate_from_name_or_id() - self.pool.add_computehost('pool', 'host3') - - check0 = self.nova.aggregates.add_host - check0.assert_any_call(self.fake_aggregate.id, 'host3') - check1 = self.nova.aggregates.remove_host - check1.assert_any_call(self.fake_aggregate.id, 'host3') - - def test_add_computehost_with_host_id(self): - # NOTE(sbauza): Freepool.hosts only contains names of hosts, not UUIDs - self._patch_get_aggregate_from_name_or_id() - self.assertRaises(manager_exceptions.HostNotInFreePool, - self.pool.add_computehost, 'pool', '3') - - def test_add_computehost_not_in_freepool(self): - self._patch_get_aggregate_from_name_or_id() - self.assertRaises(manager_exceptions.HostNotInFreePool, - self.pool.add_computehost, - 'foopool', - 'ghost-host') - - def test_add_computehost_with_no_freepool(self): - def get_fake_aggregate_but_no_freepool(*args): - if self.freepool_name in args: - raise manager_exceptions.AggregateNotFound - else: - return self.fake_aggregate - - fake_pool = self.patch(self.pool, 'get_aggregate_from_name_or_id') - fake_pool.side_effect = get_fake_aggregate_but_no_freepool - - self.assertRaises(manager_exceptions.NoFreePool, - self.pool.add_computehost, - 'pool', - 'host3') - - def test_add_computehost_with_incorrect_pool(self): - def get_no_aggregate_but_freepool(*args): - if self.freepool_name in args: - return self.freepool_name - else: - raise manager_exceptions.AggregateNotFound - fake_pool = self.patch(self.pool, 'get_aggregate_from_name_or_id') - fake_pool.side_effect = get_no_aggregate_but_freepool - self.assertRaises(manager_exceptions.AggregateNotFound, - self.pool.add_computehost, - 'wrong_pool', - 'host3') - - def test_add_computehost_to_freepool(self): - self._patch_get_aggregate_from_name_or_id() - self.pool.add_computehost(self.freepool_name, 'host2') - check = self.nova.aggregates.add_host - check.assert_called_once_with(self.fake_freepool.id, 'host2') - - def test_remove_computehost_from_freepool(self): - self._patch_get_aggregate_from_name_or_id() - self.pool.remove_computehost(self.freepool_name, 'host3') - - check = self.nova.aggregates.remove_host - check.assert_called_once_with(self.fake_freepool.id, 'host3') - - def test_remove_computehost_not_existing_from_freepool(self): - self._patch_get_aggregate_from_name_or_id() - self.assertRaises(manager_exceptions.HostNotInFreePool, - self.pool.remove_computehost, - self.freepool_name, - 'hostXX') - - def test_remove_all_computehosts(self): - self._patch_get_aggregate_from_name_or_id() - self.pool.remove_all_computehosts('pool') - for host in self.fake_aggregate.hosts: - check = self.nova.aggregates.remove_host - check.assert_any_call(self.fake_aggregate.id, host) - - def test_remove_computehost_with_no_freepool(self): - def get_fake_aggregate_but_no_freepool(*args): - if self.freepool_name in args: - raise manager_exceptions.AggregateNotFound - else: - return self.fake_aggregate - - fake_pool = self.patch(self.pool, 'get_aggregate_from_name_or_id') - fake_pool.side_effect = get_fake_aggregate_but_no_freepool - - self.assertRaises(manager_exceptions.NoFreePool, - self.pool.remove_computehost, - 'pool', - 'host3') - - def test_remove_computehost_with_incorrect_pool(self): - def get_no_aggregate_but_freepool(*args): - if self.freepool_name in args: - return self.freepool_name - else: - raise manager_exceptions.AggregateNotFound - fake_pool = self.patch(self.pool, 'get_aggregate_from_name_or_id') - fake_pool.side_effect = get_no_aggregate_but_freepool - self.assertRaises(manager_exceptions.AggregateNotFound, - self.pool.remove_computehost, - 'wrong_pool', - 'host3') - - def test_remove_computehost_with_wrong_hosts(self): - self._patch_get_aggregate_from_name_or_id() - self.nova.aggregates.remove_host.side_effect = ( - nova_exceptions.NotFound(404)) - self.assertRaises(manager_exceptions.CantRemoveHost, - self.pool.remove_computehost, - 'pool', - 'host3') - - def test_remove_computehosts_with_duplicate_host(self): - self._patch_get_aggregate_from_name_or_id() - self.nova.aggregates.add_host.side_effect = ( - nova_exceptions.Conflict(409)) - self.assertRaises(manager_exceptions.CantAddHost, - self.pool.remove_computehost, - 'pool', - 'host3') - - def test_get_computehosts_with_correct_pool(self): - self._patch_get_aggregate_from_name_or_id() - hosts = self.pool.get_computehosts('foo') - self.assertEqual(hosts, self.fake_aggregate.hosts) - - def test_get_computehosts_with_incorrect_pool(self): - self.assertEqual([], self.pool.get_computehosts('wrong_pool')) - - def test_add_project(self): - self._patch_get_aggregate_from_name_or_id() - self.pool.add_project('pool', 'projectX') - check = self.nova.aggregates.set_metadata - check.assert_called_once_with(self.fake_aggregate.id, - {'projectX': self.project_id_key}) - - def test_remove_project(self): - self._patch_get_aggregate_from_name_or_id() - self.pool.remove_project('pool', 'projectY') - check = self.nova.aggregates.set_metadata - check.assert_called_once_with(self.fake_aggregate.id, - {'projectY': None}) diff --git a/blazar/tests/plugins/test_physical_host_plugin.py b/blazar/tests/plugins/test_physical_host_plugin.py index 5731f745..a90ae216 100644 --- a/blazar/tests/plugins/test_physical_host_plugin.py +++ b/blazar/tests/plugins/test_physical_host_plugin.py @@ -28,10 +28,9 @@ from blazar.manager import service from blazar.plugins import oshosts as plugin from blazar.plugins.oshosts import host_plugin from blazar.plugins.oshosts import nova_inventory -from blazar.plugins.oshosts import reservation_pool as rp from blazar import tests from blazar.utils.openstack import base -from blazar.utils.openstack.nova import ServerManager +from blazar.utils.openstack import nova from blazar.utils import trusts @@ -52,9 +51,9 @@ class PhysicalHostPlugingSetupOnlyTestCase(tests.TestCase): self.patch(base, 'url_for').return_value = 'http://foo.bar' self.host_plugin = host_plugin self.fake_phys_plugin = self.host_plugin.PhysicalHostPlugin() - self.rp = rp + self.nova = nova self.nova_inventory = nova_inventory - self.rp_create = self.patch(self.rp.ReservationPool, 'create') + self.rp_create = self.patch(self.nova.ReservationPool, 'create') self.db_api = db_api self.db_host_extra_capability_get_all_per_host = ( self.patch(self.db_api, 'host_extra_capability_get_all_per_host')) @@ -90,7 +89,7 @@ class PhysicalHostPluginTestCase(tests.TestCase): self.patch(self.context, 'BlazarContext') self.nova_client = nova_client - self.nova = self.patch(self.nova_client, 'Client').return_value + self.nova_client = self.patch(self.nova_client, 'Client').return_value self.service = service self.manager = self.service.ManagerService() @@ -134,13 +133,13 @@ class PhysicalHostPluginTestCase(tests.TestCase): self.db_host_extra_capability_update = self.patch( self.db_api, 'host_extra_capability_update') - self.rp = rp + self.nova = nova self.nova_inventory = nova_inventory - self.rp_create = self.patch(self.rp.ReservationPool, 'create') - self.patch(self.rp.ReservationPool, 'get_aggregate_from_name_or_id') - self.add_compute_host = self.patch(self.rp.ReservationPool, + self.rp_create = self.patch(self.nova.ReservationPool, 'create') + self.patch(self.nova.ReservationPool, 'get_aggregate_from_name_or_id') + self.add_compute_host = self.patch(self.nova.ReservationPool, 'add_computehost') - self.remove_compute_host = self.patch(self.rp.ReservationPool, + self.remove_compute_host = self.patch(self.nova.ReservationPool, 'remove_computehost') self.get_host_details = self.patch(self.nova_inventory.NovaInventory, 'get_host_details') @@ -161,7 +160,7 @@ class PhysicalHostPluginTestCase(tests.TestCase): self.trust_ctx = self.patch(self.trusts, 'create_ctx_from_trust') self.trust_create = self.patch(self.trusts, 'create_trust') - self.ServerManager = ServerManager + self.ServerManager = nova.ServerManager def test_get_host(self): host = self.fake_phys_plugin.get_computehost(self.fake_host_id) @@ -316,7 +315,7 @@ class PhysicalHostPluginTestCase(tests.TestCase): 'host_reservation_create') matching_hosts = self.patch(self.fake_phys_plugin, '_matching_hosts') matching_hosts.return_value = [] - pool_delete = self.patch(self.rp.ReservationPool, 'delete') + pool_delete = self.patch(self.nova.ReservationPool, 'delete') self.assertRaises(manager_exceptions.NotEnoughHostsAvailable, self.fake_phys_plugin.reserve_resource, u'f9894fcf-e2ed-41e9-8a4c-92fac332608e', @@ -403,7 +402,7 @@ class PhysicalHostPluginTestCase(tests.TestCase): host_reservation_get.return_value = { 'aggregate_id': 1 } - get_computehosts = self.patch(self.rp.ReservationPool, + get_computehosts = self.patch(self.nova.ReservationPool, 'get_computehosts') get_computehosts.return_value = ['host1'] host_allocation_get_all = self.patch( @@ -486,13 +485,13 @@ class PhysicalHostPluginTestCase(tests.TestCase): (datetime.datetime(2013, 12, 20, 20, 30), datetime.datetime(2013, 12, 20, 21, 00)) ] - get_computehosts = self.patch(self.rp.ReservationPool, + get_computehosts = self.patch(self.nova.ReservationPool, 'get_computehosts') get_computehosts.return_value = ['host1'] matching_hosts = self.patch(self.fake_phys_plugin, '_matching_hosts') matching_hosts.return_value = ['host2'] self.patch(self.fake_phys_plugin, '_get_hypervisor_from_name_or_id') - get_hypervisors = self.patch(self.nova.hypervisors, 'get') + get_hypervisors = self.patch(self.nova_client.hypervisors, 'get') get_hypervisors.return_value = mock.MagicMock(running_vms=1) self.assertRaises( manager_exceptions.NotEnoughHostsAvailable, @@ -533,7 +532,7 @@ class PhysicalHostPluginTestCase(tests.TestCase): (datetime.datetime(2013, 12, 19, 20, 30), datetime.datetime(2013, 12, 19, 21, 00)) ] - get_computehosts = self.patch(self.rp.ReservationPool, + get_computehosts = self.patch(self.nova.ReservationPool, 'get_computehosts') get_computehosts.return_value = [] self.fake_phys_plugin.update_reservation( @@ -586,11 +585,11 @@ class PhysicalHostPluginTestCase(tests.TestCase): (datetime.datetime(2013, 12, 20, 20, 30), datetime.datetime(2013, 12, 20, 21, 00)) ] - get_computehosts = self.patch(self.rp.ReservationPool, + get_computehosts = self.patch(self.nova.ReservationPool, 'get_computehosts') get_computehosts.return_value = ['host1'] self.patch(self.fake_phys_plugin, '_get_hypervisor_from_name_or_id') - get_hypervisors = self.patch(self.nova.hypervisors, 'get') + get_hypervisors = self.patch(self.nova_client.hypervisors, 'get') get_hypervisors.return_value = mock.MagicMock(running_vms=0) matching_hosts = self.patch(self.fake_phys_plugin, '_matching_hosts') matching_hosts.return_value = ['host2'] @@ -630,7 +629,7 @@ class PhysicalHostPluginTestCase(tests.TestCase): host_get = self.patch(self.db_api, 'host_get') host_get.return_value = {'service_name': 'host1_hostname'} add_computehost = self.patch( - self.rp.ReservationPool, 'add_computehost') + self.nova.ReservationPool, 'add_computehost') self.fake_phys_plugin.on_start(u'04de74e8-193a-49d2-9ab8-cba7b49e45e8') @@ -658,13 +657,13 @@ class PhysicalHostPluginTestCase(tests.TestCase): host_allocation_destroy = self.patch( self.db_api, 'host_allocation_destroy') - get_computehosts = self.patch(self.rp.ReservationPool, + get_computehosts = self.patch(self.nova.ReservationPool, 'get_computehosts') get_computehosts.return_value = ['host'] list_servers = self.patch(self.ServerManager, 'list') list_servers.return_value = ['server1', 'server2'] delete_server = self.patch(self.ServerManager, 'delete') - delete_pool = self.patch(self.rp.ReservationPool, 'delete') + delete_pool = self.patch(self.nova.ReservationPool, 'delete') self.fake_phys_plugin.on_end(u'04de74e8-193a-49d2-9ab8-cba7b49e45e8') host_reservation_update.assert_called_with( u'04de74e8-193a-49d2-9ab8-cba7b49e45e8', {'status': 'completed'}) @@ -695,13 +694,13 @@ class PhysicalHostPluginTestCase(tests.TestCase): host_allocation_destroy = self.patch( self.db_api, 'host_allocation_destroy') - get_computehosts = self.patch(self.rp.ReservationPool, + get_computehosts = self.patch(self.nova.ReservationPool, 'get_computehosts') get_computehosts.return_value = ['host'] list_servers = self.patch(self.ServerManager, 'list') list_servers.return_value = [] delete_server = self.patch(self.ServerManager, 'delete') - delete_pool = self.patch(self.rp.ReservationPool, 'delete') + delete_pool = self.patch(self.nova.ReservationPool, 'delete') self.fake_phys_plugin.on_end(u'04de74e8-193a-49d2-9ab8-cba7b49e45e8') host_reservation_update.assert_called_with( u'04de74e8-193a-49d2-9ab8-cba7b49e45e8', {'status': 'completed'}) diff --git a/blazar/tests/utils/openstack/test_nova.py b/blazar/tests/utils/openstack/test_nova.py index 7f5acd7a..9b2919a2 100644 --- a/blazar/tests/utils/openstack/test_nova.py +++ b/blazar/tests/utils/openstack/test_nova.py @@ -12,13 +12,17 @@ # implied. # See the License for the specific language governing permissions and # limitations under the License. +import uuid as uuidgen from keystoneauth1 import session from keystoneauth1 import token_endpoint from novaclient import client as nova_client +from novaclient import exceptions as nova_exceptions from oslo_config import cfg from blazar import context +from blazar.manager import exceptions as manager_exceptions +from blazar.plugins import oshosts as host_plugin from blazar import tests from blazar.utils.openstack import base from blazar.utils.openstack import nova @@ -90,3 +94,314 @@ class TestCNClient(tests.TestCase): def test_getattr(self): # TODO(n.s.): Will be done as soon as pypi package will be updated pass + + +class AggregateFake(object): + + def __init__(self, i, name, hosts): + self.id = i + self.name = name + self.hosts = hosts + + +class ReservationPoolTestCase(tests.TestCase): + + def setUp(self): + super(ReservationPoolTestCase, self).setUp() + self.pool_name = 'pool-name-xxx' + self.project_id = 'project-uuid' + self.fake_aggregate = AggregateFake(i=123, + name='fooname', + hosts=['host1', 'host2']) + physical_host_conf = cfg.CONF[host_plugin.RESOURCE_TYPE] + nova_conf = cfg.CONF.nova + self.freepool_name = nova_conf.aggregate_freepool_name + self.project_id_key = nova_conf.project_id_key + self.blazar_owner = nova_conf.blazar_owner + self.blazar_az_prefix = physical_host_conf.blazar_az_prefix + + self.fake_freepool = AggregateFake(i=456, + name=self.freepool_name, + hosts=['host3']) + + self.set_context(context.BlazarContext(project_id=self.project_id)) + + self.nova_client = nova_client + self.nova = self.patch(self.nova_client, 'Client').return_value + + self.patch(self.nova.aggregates, 'set_metadata') + self.patch(self.nova.aggregates, 'remove_host') + + self.patch(base, 'url_for').return_value = 'http://foo.bar' + self.pool = nova.ReservationPool() + + self.p_name = self.patch(self.pool, '_generate_aggregate_name') + self.p_name.return_value = self.pool_name + + def _patch_get_aggregate_from_name_or_id(self): + def get_fake_aggregate(*args): + if self.freepool_name in args: + return self.fake_freepool + else: + return self.fake_aggregate + + patched_pool = self.patch(self.pool, 'get_aggregate_from_name_or_id') + patched_pool.side_effect = get_fake_aggregate + + def test_get_aggregate_from_name_or_id(self): + def fake_aggregate_get(id): + if id == self.fake_aggregate.id: + return self.fake_aggregate + else: + raise nova_exceptions.NotFound(id) + + self.nova.aggregates.list.return_value = [self.fake_aggregate] + self.nova.aggregates.get.side_effect = fake_aggregate_get + + self.assertRaises(manager_exceptions.AggregateNotFound, + self.pool.get_aggregate_from_name_or_id, 'none') + self.assertRaises(manager_exceptions.AggregateNotFound, + self.pool.get_aggregate_from_name_or_id, '3000') + self.assertEqual(self.pool.get_aggregate_from_name_or_id('fooname'), + self.fake_aggregate) + self.assertEqual( + self.pool.get_aggregate_from_name_or_id(self.fake_aggregate), + self.fake_aggregate) + + def test_generate_aggregate_name(self): + self.uuidgen = uuidgen + self.patch(uuidgen, 'uuid4').return_value = 'foo' + self.assertEqual('foo', + nova.ReservationPool._generate_aggregate_name()) + + def test_create(self): + self.patch(self.nova.aggregates, 'create').return_value = ( + self.fake_aggregate) + + az_name = self.blazar_az_prefix + self.pool_name + + agg = self.pool.create(az=az_name) + + self.assertEqual(agg, self.fake_aggregate) + + check0 = self.nova.aggregates.create + check0.assert_called_once_with(self.pool_name, az_name) + + meta = {self.blazar_owner: self.project_id} + check1 = self.nova.aggregates.set_metadata + check1.assert_called_once_with(self.fake_aggregate, meta) + + def test_create_no_az(self): + self.patch(self.nova.aggregates, 'create').return_value = ( + self.fake_aggregate) + + self.pool.create() + + self.nova.aggregates.create.assert_called_once_with(self.pool_name, + None) + + def test_create_no_project_id(self): + self.patch(self.nova.aggregates, 'create').return_value = ( + self.fake_aggregate) + + self.nova_wrapper = self.patch(nova.NovaClientWrapper, 'nova') + + def raiseRuntimeError(): + raise RuntimeError() + + self.context_mock.side_effect = raiseRuntimeError + + self.assertRaises(manager_exceptions.ProjectIdNotFound, + self.pool.create) + + def test_delete_with_host(self): + self._patch_get_aggregate_from_name_or_id() + agg = self.pool.get('foo') + + self.pool.delete(agg) + self.nova.aggregates.delete.assert_called_once_with(agg.id) + for host in agg.hosts: + self.nova.aggregates.remove_host.assert_any_call(agg.id, host) + self.nova.aggregates.add_host.assert_any_call( + self.fake_freepool.id, host + ) + + # can't delete aggregate with hosts + self.assertRaises(manager_exceptions.AggregateHaveHost, + self.pool.delete, 'bar', + force=False) + + def test_delete_with_no_host(self): + self._patch_get_aggregate_from_name_or_id() + agg = self.pool.get('foo') + agg.hosts = [] + self.pool.delete('foo', force=False) + self.nova.aggregates.delete.assert_called_once_with(agg.id) + + def test_delete_with_no_freepool(self): + def get_fake_aggregate_but_no_freepool(*args): + if self.freepool_name in args: + raise manager_exceptions.AggregateNotFound + else: + return self.fake_aggregate + fake_pool = self.patch(self.pool, 'get_aggregate_from_name_or_id') + fake_pool.side_effect = get_fake_aggregate_but_no_freepool + agg = self.pool.get('foo') + agg.hosts = [] + self.assertRaises(manager_exceptions.NoFreePool, + self.pool.delete, 'bar', + force=False) + + def test_get_all(self): + self.pool.get_all() + self.nova.aggregates.list.assert_called_once_with() + + def test_get(self): + self._patch_get_aggregate_from_name_or_id() + agg = self.pool.get('foo') + self.assertEqual(self.fake_aggregate, agg) + + def test_add_computehost(self): + self._patch_get_aggregate_from_name_or_id() + self.pool.add_computehost('pool', 'host3') + + check0 = self.nova.aggregates.add_host + check0.assert_any_call(self.fake_aggregate.id, 'host3') + check1 = self.nova.aggregates.remove_host + check1.assert_any_call(self.fake_aggregate.id, 'host3') + + def test_add_computehost_with_host_id(self): + # NOTE(sbauza): Freepool.hosts only contains names of hosts, not UUIDs + self._patch_get_aggregate_from_name_or_id() + self.assertRaises(manager_exceptions.HostNotInFreePool, + self.pool.add_computehost, 'pool', '3') + + def test_add_computehost_not_in_freepool(self): + self._patch_get_aggregate_from_name_or_id() + self.assertRaises(manager_exceptions.HostNotInFreePool, + self.pool.add_computehost, + 'foopool', + 'ghost-host') + + def test_add_computehost_with_no_freepool(self): + def get_fake_aggregate_but_no_freepool(*args): + if self.freepool_name in args: + raise manager_exceptions.AggregateNotFound + else: + return self.fake_aggregate + + fake_pool = self.patch(self.pool, 'get_aggregate_from_name_or_id') + fake_pool.side_effect = get_fake_aggregate_but_no_freepool + + self.assertRaises(manager_exceptions.NoFreePool, + self.pool.add_computehost, + 'pool', + 'host3') + + def test_add_computehost_with_incorrect_pool(self): + def get_no_aggregate_but_freepool(*args): + if self.freepool_name in args: + return self.freepool_name + else: + raise manager_exceptions.AggregateNotFound + fake_pool = self.patch(self.pool, 'get_aggregate_from_name_or_id') + fake_pool.side_effect = get_no_aggregate_but_freepool + self.assertRaises(manager_exceptions.AggregateNotFound, + self.pool.add_computehost, + 'wrong_pool', + 'host3') + + def test_add_computehost_to_freepool(self): + self._patch_get_aggregate_from_name_or_id() + self.pool.add_computehost(self.freepool_name, 'host2') + check = self.nova.aggregates.add_host + check.assert_called_once_with(self.fake_freepool.id, 'host2') + + def test_remove_computehost_from_freepool(self): + self._patch_get_aggregate_from_name_or_id() + self.pool.remove_computehost(self.freepool_name, 'host3') + + check = self.nova.aggregates.remove_host + check.assert_called_once_with(self.fake_freepool.id, 'host3') + + def test_remove_computehost_not_existing_from_freepool(self): + self._patch_get_aggregate_from_name_or_id() + self.assertRaises(manager_exceptions.HostNotInFreePool, + self.pool.remove_computehost, + self.freepool_name, + 'hostXX') + + def test_remove_all_computehosts(self): + self._patch_get_aggregate_from_name_or_id() + self.pool.remove_all_computehosts('pool') + for host in self.fake_aggregate.hosts: + check = self.nova.aggregates.remove_host + check.assert_any_call(self.fake_aggregate.id, host) + + def test_remove_computehost_with_no_freepool(self): + def get_fake_aggregate_but_no_freepool(*args): + if self.freepool_name in args: + raise manager_exceptions.AggregateNotFound + else: + return self.fake_aggregate + + fake_pool = self.patch(self.pool, 'get_aggregate_from_name_or_id') + fake_pool.side_effect = get_fake_aggregate_but_no_freepool + + self.assertRaises(manager_exceptions.NoFreePool, + self.pool.remove_computehost, + 'pool', + 'host3') + + def test_remove_computehost_with_incorrect_pool(self): + def get_no_aggregate_but_freepool(*args): + if self.freepool_name in args: + return self.freepool_name + else: + raise manager_exceptions.AggregateNotFound + fake_pool = self.patch(self.pool, 'get_aggregate_from_name_or_id') + fake_pool.side_effect = get_no_aggregate_but_freepool + self.assertRaises(manager_exceptions.AggregateNotFound, + self.pool.remove_computehost, + 'wrong_pool', + 'host3') + + def test_remove_computehost_with_wrong_hosts(self): + self._patch_get_aggregate_from_name_or_id() + self.nova.aggregates.remove_host.side_effect = ( + nova_exceptions.NotFound(404)) + self.assertRaises(manager_exceptions.CantRemoveHost, + self.pool.remove_computehost, + 'pool', + 'host3') + + def test_remove_computehosts_with_duplicate_host(self): + self._patch_get_aggregate_from_name_or_id() + self.nova.aggregates.add_host.side_effect = ( + nova_exceptions.Conflict(409)) + self.assertRaises(manager_exceptions.CantAddHost, + self.pool.remove_computehost, + 'pool', + 'host3') + + def test_get_computehosts_with_correct_pool(self): + self._patch_get_aggregate_from_name_or_id() + hosts = self.pool.get_computehosts('foo') + self.assertEqual(hosts, self.fake_aggregate.hosts) + + def test_get_computehosts_with_incorrect_pool(self): + self.assertEqual([], self.pool.get_computehosts('wrong_pool')) + + def test_add_project(self): + self._patch_get_aggregate_from_name_or_id() + self.pool.add_project('pool', 'projectX') + check = self.nova.aggregates.set_metadata + check.assert_called_once_with(self.fake_aggregate.id, + {'projectX': self.project_id_key}) + + def test_remove_project(self): + self._patch_get_aggregate_from_name_or_id() + self.pool.remove_project('pool', 'projectY') + check = self.nova.aggregates.set_metadata + check.assert_called_once_with(self.fake_aggregate.id, + {'projectY': None}) diff --git a/blazar/utils/openstack/nova.py b/blazar/utils/openstack/nova.py index 228f7e2d..1e341006 100644 --- a/blazar/utils/openstack/nova.py +++ b/blazar/utils/openstack/nova.py @@ -12,6 +12,7 @@ # implied. # See the License for the specific language governing permissions and # limitations under the License. +import uuid as uuidgen from keystoneauth1 import session from keystoneauth1 import token_endpoint @@ -19,8 +20,11 @@ from novaclient import client as nova_client from novaclient import exceptions as nova_exception from novaclient.v2 import servers from oslo_config import cfg +from oslo_log import log as logging from blazar import context +from blazar.manager import exceptions as manager_exceptions +from blazar.plugins import oshosts from blazar.utils.openstack import base @@ -36,12 +40,27 @@ nova_opts = [ cfg.StrOpt('image_prefix', default='reserved_', deprecated_group='DEFAULT', - help='Prefix for VM images if you want to create snapshots') + help='Prefix for VM images if you want to create snapshots'), + cfg.StrOpt('aggregate_freepool_name', + default='freepool', + deprecated_group=oshosts.RESOURCE_TYPE, + help='Name of the special aggregate where all hosts ' + 'are candidate for physical host reservation'), + cfg.StrOpt('project_id_key', + default='blazar:project', + deprecated_group=oshosts.RESOURCE_TYPE, + help='Aggregate metadata value for key matching project_id'), + cfg.StrOpt('blazar_owner', + default='blazar:owner', + deprecated_group=oshosts.RESOURCE_TYPE, + help='Aggregate metadata key for knowing owner project_id') ] + CONF = cfg.CONF CONF.register_opts(nova_opts, group='nova') CONF.import_opt('identity_service', 'blazar.utils.openstack.keystone') +LOG = logging.getLogger(__name__) class BlazarNovaClient(object): @@ -179,3 +198,226 @@ class NovaClientWrapper(object): project_name=self.project_name, project_domain_name=self.project_domain_name) return nova + + +class ReservationPool(NovaClientWrapper): + def __init__(self): + super(ReservationPool, self).__init__() + self.config = CONF.nova + self.freepool_name = self.config.aggregate_freepool_name + + def get_aggregate_from_name_or_id(self, aggregate_obj): + """Return an aggregate by name or an id.""" + + aggregate = None + agg_id = None + try: + agg_id = int(aggregate_obj) + except (ValueError, TypeError): + if hasattr(aggregate_obj, 'id') and aggregate_obj.id: + # pool is an aggregate + agg_id = aggregate_obj.id + + if agg_id is not None: + try: + aggregate = self.nova.aggregates.get(agg_id) + except nova_exception.NotFound: + aggregate = None + else: + # FIXME(scroiset): can't get an aggregate by name + # so iter over all aggregate and check for the good one + all_aggregates = self.nova.aggregates.list() + for agg in all_aggregates: + if aggregate_obj == agg.name: + aggregate = agg + if aggregate: + return aggregate + else: + raise manager_exceptions.AggregateNotFound(pool=aggregate_obj) + + @staticmethod + def _generate_aggregate_name(): + return str(uuidgen.uuid4()) + + def create(self, name=None, az=None): + """Create a Pool (an Aggregate) with or without Availability Zone. + + By default expose to user the aggregate with an Availability Zone. + Return an aggregate or raise a nova exception. + + """ + + name = name or self._generate_aggregate_name() + + LOG.debug('Creating pool aggregate: %s with Availability Zone %s' + % (name, az)) + agg = self.nova.aggregates.create(name, az) + + try: + ctx = context.current() + project_id = ctx.project_id + except RuntimeError: + e = manager_exceptions.ProjectIdNotFound() + LOG.error(e.message) + raise e + + meta = {self.config.blazar_owner: project_id} + self.nova.aggregates.set_metadata(agg, meta) + + return agg + + def delete(self, pool, force=True): + """Delete an aggregate. + + pool can be an aggregate name or an aggregate id. + Remove all hosts before delete aggregate (default). + If force is False, raise exception if at least one + host is attached to. + + """ + + agg = self.get_aggregate_from_name_or_id(pool) + + hosts = agg.hosts + if len(hosts) > 0 and not force: + raise manager_exceptions.AggregateHaveHost(name=agg.name, + hosts=agg.hosts) + try: + freepool_agg = self.get(self.freepool_name) + except manager_exceptions.AggregateNotFound: + raise manager_exceptions.NoFreePool() + for host in hosts: + LOG.debug("Removing host '%s' from aggregate " + "'%s')" % (host, agg.id)) + self.nova.aggregates.remove_host(agg.id, host) + + if freepool_agg.id != agg.id: + self.nova.aggregates.add_host(freepool_agg.id, host) + + self.nova.aggregates.delete(agg.id) + + def get_all(self): + """Return all aggregate.""" + + return self.nova.aggregates.list() + + def get(self, pool): + """return details for aggregate pool or raise AggregateNotFound.""" + + return self.get_aggregate_from_name_or_id(pool) + + def get_computehosts(self, pool): + """Return a list of compute host names for an aggregate.""" + + try: + agg = self.get_aggregate_from_name_or_id(pool) + return agg.hosts + except manager_exceptions.AggregateNotFound: + return [] + + def add_computehost(self, pool, host): + """Add a compute host to an aggregate. + + The `host` must exist otherwise raise an error + and the `host` must be in the freepool. + + :param pool: Name or UUID of the pool to rattach the host + :param host: Name (not UUID) of the host to associate + :type host: str + + Return the related aggregate. + Raise an aggregate exception if something wrong. + """ + + agg = self.get_aggregate_from_name_or_id(pool) + + try: + freepool_agg = self.get(self.freepool_name) + except manager_exceptions.AggregateNotFound: + raise manager_exceptions.NoFreePool() + + if freepool_agg.id != agg.id: + if host not in freepool_agg.hosts: + raise manager_exceptions.HostNotInFreePool( + host=host, freepool_name=freepool_agg.name) + LOG.info("removing host '%s' " + "from aggregate freepool %s" % (host, freepool_agg.name)) + try: + self.remove_computehost(freepool_agg.id, host) + except nova_exception.NotFound: + raise manager_exceptions.HostNotFound(host=host) + + LOG.info("adding host '%s' to aggregate %s" % (host, agg.id)) + try: + return self.nova.aggregates.add_host(agg.id, host) + except nova_exception.NotFound: + raise manager_exceptions.HostNotFound(host=host) + except nova_exception.Conflict: + raise manager_exceptions.AggregateAlreadyHasHost(pool=pool, + host=host) + + def remove_all_computehosts(self, pool): + """Remove all compute hosts attached to an aggregate.""" + + hosts = self.get_computehosts(pool) + self.remove_computehost(pool, hosts) + + def remove_computehost(self, pool, hosts): + """Remove compute host(s) from an aggregate.""" + + if not isinstance(hosts, list): + hosts = [hosts] + + agg = self.get_aggregate_from_name_or_id(pool) + + try: + freepool_agg = self.get(self.freepool_name) + except manager_exceptions.AggregateNotFound: + raise manager_exceptions.NoFreePool() + + hosts_failing_to_remove = [] + hosts_failing_to_add = [] + hosts_not_in_freepool = [] + for host in hosts: + if freepool_agg.id == agg.id: + if host not in freepool_agg.hosts: + hosts_not_in_freepool.append(host) + continue + try: + self.nova.aggregates.remove_host(agg.id, host) + except nova_exception.ClientException: + hosts_failing_to_remove.append(host) + if freepool_agg.id != agg.id: + # NOTE(sbauza) : We don't want to put again the host in + # freepool if the requested pool is the freepool... + try: + self.nova.aggregates.add_host(freepool_agg.id, host) + except nova_exception.ClientException: + hosts_failing_to_add.append(host) + + if hosts_failing_to_remove: + raise manager_exceptions.CantRemoveHost( + host=hosts_failing_to_remove, pool=agg) + if hosts_failing_to_add: + raise manager_exceptions.CantAddHost(host=hosts_failing_to_add, + pool=freepool_agg) + if hosts_not_in_freepool: + raise manager_exceptions.HostNotInFreePool( + host=hosts_not_in_freepool, freepool_name=freepool_agg.name) + + def add_project(self, pool, project_id): + """Add a project to an aggregate.""" + + metadata = {project_id: self.config.project_id_key} + + agg = self.get_aggregate_from_name_or_id(pool) + + return self.nova.aggregates.set_metadata(agg.id, metadata) + + def remove_project(self, pool, project_id): + """Remove a project from an aggregate.""" + + agg = self.get_aggregate_from_name_or_id(pool) + + metadata = {project_id: None} + return self.nova.aggregates.set_metadata(agg.id, metadata)