diff --git a/climate/manager/exceptions.py b/climate/manager/exceptions.py index 8accf7fe..055089ad 100644 --- a/climate/manager/exceptions.py +++ b/climate/manager/exceptions.py @@ -68,6 +68,8 @@ class HostHavingServers(exceptions.ClimateException): msg_fmt = _("Servers [%(servers)s] found for host %(host)s") +# oshost plugin related exceptions + class CantAddExtraCapability(exceptions.ClimateException): code = 409 msg_fmt = _("Can't add extracapabilities %(keys)s to Host %(host)s") @@ -91,3 +93,7 @@ class WrongClientVersion(exceptions.ClimateException): class NoManagementUrl(exceptions.NotFound): code = 404 msg_fmt = _("You haven't management url for service") + + +class HypervisorNotFound(exceptions.ClimateException): + msg_fmt = _("Aggregate '%(pool)s' not found!") diff --git a/climate/plugins/oshosts/host_plugin.py b/climate/plugins/oshosts/host_plugin.py new file mode 100644 index 00000000..b28004d1 --- /dev/null +++ b/climate/plugins/oshosts/host_plugin.py @@ -0,0 +1,366 @@ +# -*- coding: utf-8 -*- +# +# Author: François Rossigneux +# +# 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 json +import six + +from novaclient import client +from oslo.config import cfg + +from climate import context +from climate.db import api as db_api +from climate.db import utils as db_utils +from climate.manager import exceptions as manager_ex +from climate.openstack.common import uuidutils +from climate.plugins import base +from climate.plugins.oshosts import nova_inventory +from climate.plugins.oshosts import reservation_pool as rp +from climate.utils import service as service_utils + + +class PhysicalHostPlugin(base.BasePlugin): + """Plugin for physical host resource.""" + resource_type = 'physical:host' + title = 'Physical Host Plugin' + description = 'This plugin starts and shutdowns the hosts.' + freepool_name = cfg.CONF[resource_type].aggregate_freepool_name + pool = None + inventory = None + + def __init__(self): + #TODO(sbauza): use catalog to find the url + auth_url = "%s://%s:%s/v2.0" % (cfg.CONF.os_auth_protocol, + cfg.CONF.os_auth_host, + cfg.CONF.os_auth_port) + #TODO(scroiset): use client wrapped by climate and use trust + self.nova = client.Client('2', + username=cfg.CONF.climate_username, + api_key=cfg.CONF.climate_password, + auth_url=auth_url, + project_id=cfg.CONF.climate_tenant_name) + + def create_reservation(self, values): + """Create reservation.""" + pool = rp.ReservationPool() + pool_name = uuidutils.generate_uuid() + pool_instance = pool.create(name=pool_name) + reservation_values = { + 'id': pool_name, + 'lease_id': values['lease_id'], + 'resource_id': pool_instance.id, + 'resource_type': values['resource_type'], + 'status': 'pending', + } + reservation = db_api.reservation_create(reservation_values) + count_range = str(values['min']) + '-' + str(values['max']) + host_values = { + 'reservation_id': reservation['id'], + 'resource_properties': values['resource_properties'], + 'hypervisor_properties': values['hypervisor_properties'], + 'count_range': count_range, + 'status': 'pending', + } + db_api.host_reservation_create(host_values) + host_ids = self._matching_hosts( + values['hypervisor_properties'], + values['resource_properties'], + count_range, + values['start_date'], + values['end_date'], + ) + if not host_ids: + raise RuntimeError('Not enough hosts available') + for host_id in host_ids: + db_api.host_allocation_create({'compute_host_id': host_id, + 'reservation_id': reservation['id']}) + + def on_start(self, resource_id): + """Add the hosts in the pool.""" + reservations = db_api.reservation_get_all_by_values( + resource_id=resource_id) + for reservation in reservations: + pool = rp.ReservationPool() + for allocation in db_api.host_allocation_get_all_by_values( + reservation_id=reservation['id']): + host = db_api.host_get(allocation['compute_host_id']) + host_name = host['hypervisor_hostname'] + pool.add_computehost(reservation['resource_id'], host_name) + + def on_end(self, resource_id): + """Remove the hosts from the pool.""" + reservations = db_api.reservation_get_all_by_values( + resource_id=resource_id) + for reservation in reservations: + db_api.reservation_update(reservation['id'], + {'status': 'completed'}) + host_reservation = db_api.host_reservation_get_by_reservation_id( + reservation['id']) + db_api.host_reservation_update(host_reservation['id'], + {'status': 'completed'}) + allocations = db_api.host_allocation_get_all_by_values( + reservation_id=reservation['id']) + pool = rp.ReservationPool() + for allocation in allocations: + db_api.host_allocation_destroy(allocation['id']) + if self.nova.hypervisors.get( + self._get_hypervisor_from_name( + allocation['compute_host_id']) + ).__dict__['running_vms'] == 0: + pool.delete(reservation['resource_id']) + #TODO(frossigneux) Kill, migrate, or increase fees... + + def setup(self, conf): + # Create freepool if not exists + with context.ClimateContext() as ctx: + ctx = ctx.elevated() + if self.pool is None: + self.pool = rp.ReservationPool() + if self.inventory is None: + self.inventory = nova_inventory.NovaInventory() + if not self._freepool_exists(): + self.pool.create(name=self.freepool_name, az=None) + + def _freepool_exists(self): + try: + self.pool.get_aggregate_from_name_or_id(self.freepool_name) + return True + except manager_ex.AggregateNotFound: + return False + + def _get_extra_capabilities(self, host_id): + extra_capabilities = {} + raw_extra_capabilities = \ + db_api.host_extra_capability_get_all_per_host(host_id) + for capability in raw_extra_capabilities: + key = capability['capability_name'] + extra_capabilities[key] = capability['capability_value'] + return extra_capabilities + + @service_utils.export_context + def get_computehost(self, host_id): + host = db_api.host_get(host_id) + extra_capabilities = self._get_extra_capabilities(host_id) + if host is not None and extra_capabilities: + res = host.copy() + res.update(extra_capabilities) + return res + else: + return host + + @service_utils.export_context + def list_computehosts(self): + raw_host_list = db_api.host_list() + host_list = [] + for host in raw_host_list: + host_list.append(self.get_computehost(host['id'])) + return host_list + + @service_utils.export_context + def create_computehost(self, host_values): + # TODO(sbauza): + # - Exception handling for HostNotFound + host_id = host_values.pop('id', None) + host_name = host_values.pop('name', None) + + host_ref = host_id or host_name + if host_ref is None: + raise manager_ex.InvalidHost(host=host_values) + servers = self.inventory.get_servers_per_host(host_ref) + if servers: + raise manager_ex.HostHavingServers(host=host_ref, + servers=servers) + host_details = self.inventory.get_host_details(host_ref) + # NOTE(sbauza): Only last duplicate name for same extra capability will + # be stored + to_store = set(host_values.keys()) - set(host_details.keys()) + extra_capabilities_keys = to_store + extra_capabilities = dict( + (key, host_values[key]) for key in extra_capabilities_keys + ) + self.pool.add_computehost(self.freepool_name, host_ref) + + host = None + cantaddextracapability = [] + try: + host = db_api.host_create(host_details) + except RuntimeError: + #We need to rollback + # TODO(sbauza): Investigate use of Taskflow for atomic transactions + self.pool.remove_computehost(self.freepool_name, host_ref) + if host: + for key in extra_capabilities: + values = {'computehost_id': host['id'], + 'capability_name': key, + 'capability_value': extra_capabilities[key], + } + try: + db_api.host_extra_capability_create(values) + except RuntimeError: + cantaddextracapability.append(key) + if cantaddextracapability: + raise manager_ex.CantAddExtraCapability( + keys=cantaddextracapability, + host=host['id']) + if host: + return self.get_computehost(host['id']) + else: + return None + + @service_utils.export_context + def update_computehost(self, host_id, values): + # NOTE (sbauza): Only update existing extra capabilites, don't create + # other ones + if values: + cant_update_extra_capability = [] + for value in values: + capabilities = db_api.host_extra_capability_get_all_per_name( + host_id, + value, + ) + for raw_capability in capabilities: + capability = { + 'capability_name': value, + 'capability_value': values[value], + } + try: + db_api.host_extra_capability_update( + raw_capability['id'], capability) + except RuntimeError: + cant_update_extra_capability.append( + raw_capability['capability_name']) + if cant_update_extra_capability: + raise manager_ex.CantAddExtraCapability( + host=host_id, + keys=cant_update_extra_capability) + return self.get_computehost(host_id) + + @service_utils.export_context + def delete_computehost(self, host_id): + # TODO(sbauza): + # - Check if no leases having this host scheduled + servers = self.inventory.get_servers_per_host(host_id) + if servers: + raise manager_ex.HostHavingServers(host=host_id, + servers=servers) + host = db_api.host_get(host_id) + if not host: + raise manager_ex.HostNotFound(host=host_id) + try: + self.pool.remove_computehost(self.freepool_name, + host['hypervisor_hostname']) + # NOTE(sbauza): Extracapabilities will be destroyed thanks to + # the DB FK. + db_api.host_destroy(host_id) + except RuntimeError: + # Nothing so bad, but we need to advert the admin he has to rerun + raise manager_ex.CantRemoveHost(host=host_id, + pool=self.freepool_name) + + def _matching_hosts(self, hypervisor_properties, resource_properties, + count_range, start_date, end_date): + """Return the matching hosts (preferably not allocated) + + """ + count_range = count_range.split('-') + min_host = count_range[0] + max_host = count_range[1] + allocated_host_ids = [] + not_allocated_host_ids = [] + filter_array = [] + # TODO(frossigneux) support "or" operator + if hypervisor_properties: + filter_array = self._convert_requirements( + hypervisor_properties) + if resource_properties: + filter_array += self._convert_requirements( + resource_properties) + for host in db_api.host_get_all_by_queries(filter_array): + if not db_api.host_allocation_get_all_by_values( + compute_host_id=host['id']): + not_allocated_host_ids.append(host['id']) + elif db_utils.get_free_periods( + host['id'], + start_date, + end_date, + end_date - start_date, + ) == [ + (start_date, end_date), + ]: + allocated_host_ids.append(host['id']) + if len(not_allocated_host_ids) >= int(min_host): + return not_allocated_host_ids[:int(max_host)] + all_host_ids = allocated_host_ids + not_allocated_host_ids + if len(all_host_ids) >= int(min_host): + return all_host_ids[:int(max_host)] + else: + return [] + + def _convert_requirements(self, requirements): + """Convert the requirements to an array of strings. + ["key op value", "key op value", ...] + + """ + # TODO(frossigneux) Support the "or" operator + # Convert text to json + if isinstance(requirements, six.string_types): + requirements = json.loads(requirements) + # Requirement list looks like ['<', '$ram', '1024'] + if self._requirements_with_three_elements(requirements): + result = [] + if requirements[0] == '=': + requirements[0] = '==' + string = (requirements[1][1:] + " " + requirements[0] + " " + + requirements[2]) + result.append(string) + return result + # Remove the 'and' element at the head of the requirement list + elif self._requirements_with_and_keyword(requirements): + return [self._convert_requirements(x)[0] + for x in requirements[1:]] + # Empty requirement list + elif isinstance(requirements, list) and not requirements: + return requirements + else: + raise RuntimeError('Malformed requirements') + + def _requirements_with_three_elements(self, requirements): + """Return true if requirement list looks like ['<', '$ram', '1024'].""" + return (isinstance(requirements, list) and + len(requirements) == 3 and + isinstance(requirements[0], six.string_types) and + isinstance(requirements[1], six.string_types) and + isinstance(requirements[2], six.string_types) and + requirements[0] in ['==', '=', '!=', '>=', '<=', '>', '<'] and + len(requirements[1]) > 1 and requirements[1][0] == '$' and + len(requirements[2]) > 1) + + def _requirements_with_and_keyword(self, requirements): + return (len(requirements) > 1 and + isinstance(requirements[0], six.string_types) and + requirements[0] == 'and' and + all(self._convert_requirements(x) for x in requirements[1:])) + + def _get_hypervisor_from_name(self, hypervisor_name): + """Return an hypervisor by name or an id.""" + hypervisor = None + all_hypervisors = self.nova.hypervisors.list() + for hyp in all_hypervisors: + if hypervisor_name == hyp.hypervisor_hostname: + hypervisor = hyp + if hypervisor: + return hypervisor + else: + raise manager_ex.HypervisorNotFound(pool=hypervisor_name) diff --git a/climate/plugins/physical_host_plugin.py b/climate/plugins/physical_host_plugin.py deleted file mode 100644 index 6ddb7d5c..00000000 --- a/climate/plugins/physical_host_plugin.py +++ /dev/null @@ -1,183 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Author: François Rossigneux -# -# 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. - -from oslo.config import cfg - -from climate import context -from climate.db import api as db_api -from climate.manager import exceptions as manager_exceptions -from climate.plugins import base -from climate.plugins.oshosts import nova_inventory -from climate.plugins.oshosts import reservation_pool as rp -from climate.utils import service as service_utils - - -class PhysicalHostPlugin(base.BasePlugin): - """Plugin for physical host resource.""" - resource_type = 'physical:host' - title = 'Physical Host Plugin' - description = 'This plugin starts and shutdowns the hosts.' - freepool_name = cfg.CONF[resource_type].aggregate_freepool_name - pool = None - inventory = None - - def on_start(self, resource_id): - """Add the hosts in the pool.""" - pass - - def on_end(self, resource_id): - """Remove the hosts from the pool.""" - pass - - def setup(self, conf): - # Create freepool if not exists - with context.ClimateContext() as ctx: - ctx = ctx.elevated() - if self.pool is None: - self.pool = rp.ReservationPool() - if self.inventory is None: - self.inventory = nova_inventory.NovaInventory() - if not self._freepool_exists(): - self.pool.create(name=self.freepool_name, az=None) - - def _freepool_exists(self): - try: - self.pool.get_aggregate_from_name_or_id(self.freepool_name) - return True - except manager_exceptions.AggregateNotFound: - return False - - def _get_extra_capabilities(self, host_id): - extra_capabilities = {} - raw_extra_capabilities = \ - db_api.host_extra_capability_get_all_per_host(host_id) - for capability in raw_extra_capabilities: - extra_capabilities[capability['capability_name']] = \ - capability['capability_value'] - return extra_capabilities - - @service_utils.export_context - def get_computehost(self, host_id): - host = db_api.host_get(host_id) - extra_capabilities = self._get_extra_capabilities(host_id) - if host is not None and extra_capabilities: - res = host.copy() - res.update(extra_capabilities) - return res - else: - return host - - @service_utils.export_context - def list_computehosts(self): - raw_host_list = db_api.host_list() - host_list = [] - for host in raw_host_list: - host_list.append(self.get_computehost(host['id'])) - return host_list - - @service_utils.export_context - def create_computehost(self, host_values): - # TODO(sbauza): - # - Exception handling for HostNotFound - host_id = host_values.pop('id', None) - host_name = host_values.pop('name', None) - - host_ref = host_id or host_name - if host_ref is None: - raise manager_exceptions.InvalidHost(host=host_values) - servers = self.inventory.get_servers_per_host(host_ref) - if servers: - raise manager_exceptions.HostHavingServers(host=host_ref, - servers=servers) - host_details = self.inventory.get_host_details(host_ref) - # NOTE(sbauza): Only last duplicate name for same extra capability will - # be stored - extra_capabilities_keys = \ - set(host_values.keys()) - set(host_details.keys()) - extra_capabilities = \ - dict((key, host_values[key]) for key in extra_capabilities_keys) - self.pool.add_computehost(self.freepool_name, host_ref) - - host = None - cantaddextracapability = [] - try: - host = db_api.host_create(host_details) - except RuntimeError: - #We need to rollback - # TODO(sbauza): Investigate use of Taskflow for atomic transactions - self.pool.remove_computehost(self.freepool_name, host_ref) - if host: - for key in extra_capabilities: - values = {'computehost_id': host['id'], - 'capability_name': key, - 'capability_value': extra_capabilities[key]} - try: - db_api.host_extra_capability_create(values) - except RuntimeError: - cantaddextracapability.append(key) - if cantaddextracapability: - raise manager_exceptions.CantAddExtraCapability( - keys=cantaddextracapability, host=host['id']) - if host: - return self.get_computehost(host['id']) - else: - return None - - @service_utils.export_context - def update_computehost(self, host_id, values): - # NOTE (sbauza): Only update existing extra capabilites, don't create - # other ones - if values: - cantupdateextracapability = [] - for value in values: - capabilities = \ - db_api.host_extra_capability_get_all_per_name(host_id, - value) - for raw_capability in capabilities: - capability = {'capability_name': value, - 'capability_value': values[value]} - try: - db_api.host_extra_capability_update( - raw_capability['id'], capability) - except RuntimeError: - cantupdateextracapability.append( - raw_capability['capability_name']) - if cantupdateextracapability: - raise manager_exceptions.CantAddExtraCapability( - host=host_id, keys=cantupdateextracapability) - return self.get_computehost(host_id) - - @service_utils.export_context - def delete_computehost(self, host_id): - # TODO(sbauza): - # - Check if no leases having this host scheduled - servers = self.inventory.get_servers_per_host(host_id) - if servers: - raise manager_exceptions.HostHavingServers(host=host_id, - servers=servers) - host = db_api.host_get(host_id) - if not host: - raise manager_exceptions.HostNotFound(host=host_id) - try: - self.pool.remove_computehost(self.freepool_name, - host['hypervisor_hostname']) - # NOTE(sbauza): Extracapabilities will be destroyed thanks to - # the DB FK. - db_api.host_destroy(host_id) - except RuntimeError: - # Nothing so bad, but we need to advert the admin he has to rerun - raise manager_exceptions.CantRemoveHost(host=host_id, - pool=self.freepool_name) diff --git a/climate/tests/manager/test_service.py b/climate/tests/manager/test_service.py index 132bb979..f715ef2b 100644 --- a/climate/tests/manager/test_service.py +++ b/climate/tests/manager/test_service.py @@ -25,7 +25,7 @@ from climate.db import api as db_api from climate import exceptions from climate.manager import service from climate.plugins import dummy_vm_plugin -from climate.plugins import physical_host_plugin +from climate.plugins.oshosts import host_plugin from climate import tests from climate.utils import trusts @@ -47,8 +47,8 @@ class ServiceTestCase(tests.TestCase): self.fake_plugin = self.patch(self.dummy_plugin, 'DummyVMPlugin') - self.physical_host_plugin = physical_host_plugin - self.fake_phys_plugin = self.patch(self.physical_host_plugin, + self.host_plugin = host_plugin + self.fake_phys_plugin = self.patch(self.host_plugin, 'PhysicalHostPlugin') self.manager = self.service.ManagerService('127.0.0.1') diff --git a/climate/tests/plugins/test_physical_host_plugin.py b/climate/tests/plugins/test_physical_host_plugin.py index f2f28365..b3fbc5f1 100644 --- a/climate/tests/plugins/test_physical_host_plugin.py +++ b/climate/tests/plugins/test_physical_host_plugin.py @@ -13,32 +13,45 @@ # See the License for the specific language governing permissions and # limitations under the License. +import datetime +import mock import testtools from climate import context from climate.db import api as db_api +from climate.db import utils as db_utils from climate.manager import exceptions as manager_exceptions from climate.manager import service +from climate.openstack.common import uuidutils +from climate.plugins.oshosts import host_plugin from climate.plugins.oshosts import nova_inventory from climate.plugins.oshosts import reservation_pool as rp -from climate.plugins import physical_host_plugin from climate import tests +from novaclient import client as nova_client + + +class AggregateFake(object): + + def __init__(self, i, name, hosts): + self.id = i + self.name = name + self.hosts = hosts class PhysicalHostPlugingSetupOnlyTestCase(tests.TestCase): + def setUp(self): super(PhysicalHostPlugingSetupOnlyTestCase, self).setUp() self.context = context self.patch(self.context, 'ClimateContext') - self.physical_host_plugin = physical_host_plugin - self.fake_phys_plugin = \ - self.physical_host_plugin.PhysicalHostPlugin() + self.host_plugin = host_plugin + self.fake_phys_plugin = self.host_plugin.PhysicalHostPlugin() self.rp = rp self.nova_inventory = nova_inventory self.rp_create = self.patch(self.rp.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') + self.db_host_extra_capability_get_all_per_host = ( + self.patch(self.db_api, 'host_extra_capability_get_all_per_host')) def test_setup(self): def fake_setup(): @@ -62,23 +75,25 @@ class PhysicalHostPlugingSetupOnlyTestCase(tests.TestCase): def test__freepool_exists_with_freepool_missing(self): def fake_get_aggregate_from_name_or_id(*args, **kwargs): raise manager_exceptions.AggregateNotFound - mock = self.patch(self.rp.ReservationPool, - 'get_aggregate_from_name_or_id') - mock.side_effect = fake_get_aggregate_from_name_or_id + rp_mock = self.patch(self.rp.ReservationPool, + 'get_aggregate_from_name_or_id') + rp_mock.side_effect = fake_get_aggregate_from_name_or_id self.fake_phys_plugin.setup(None) self.assertEqual(self.fake_phys_plugin._freepool_exists(), False) def test__get_extra_capabilities_with_values(self): - self.db_host_extra_capability_get_all_per_host.return_value = \ - [{'id': 1, - 'capability_name': 'foo', - 'capability_value': 'bar', - 'other': 'value', - 'computehost_id': 1}, - {'id': 2, - 'capability_name': 'buzz', - 'capability_value': 'word', - 'computehost_id': 1}] + self.db_host_extra_capability_get_all_per_host.return_value = [ + {'id': 1, + 'capability_name': 'foo', + 'capability_value': 'bar', + 'other': 'value', + 'computehost_id': 1 + }, + {'id': 2, + 'capability_name': 'buzz', + 'capability_value': 'word', + 'computehost_id': 1 + }] res = self.fake_phys_plugin._get_extra_capabilities(1) self.assertEqual({'foo': 'bar', 'buzz': 'word'}, res) @@ -89,29 +104,35 @@ class PhysicalHostPlugingSetupOnlyTestCase(tests.TestCase): class PhysicalHostPluginTestCase(tests.TestCase): + def setUp(self): super(PhysicalHostPluginTestCase, self).setUp() self.context = context self.patch(self.context, 'ClimateContext') + self.nova_client = nova_client + self.nova = self.patch(self.nova_client, 'Client').return_value + self.service = service self.manager = self.service.ManagerService('127.0.0.1') self.fake_host_id = '1' - self.fake_host = {'id': self.fake_host_id, - 'hypervisor_hostname': 'foo', - 'vcpus': 4, - 'cpu_info': 'foo', - 'hypervisor_type': 'xen', - 'hypervisor_version': 1, - 'memory_mb': 8192, - 'local_gb': 10} + self.fake_host = { + 'id': self.fake_host_id, + 'hypervisor_hostname': 'foo', + 'vcpus': 4, + 'cpu_info': 'foo', + 'hypervisor_type': 'xen', + 'hypervisor_version': 1, + 'memory_mb': 8192, + 'local_gb': 10, + } - self.physical_host_plugin = physical_host_plugin - self.fake_phys_plugin = \ - self.physical_host_plugin.PhysicalHostPlugin() + self.host_plugin = host_plugin + self.fake_phys_plugin = self.host_plugin.PhysicalHostPlugin() self.db_api = db_api + self.db_utils = db_utils self.db_host_get = self.patch(self.db_api, 'host_get') self.db_host_get.return_value = self.fake_host @@ -120,14 +141,17 @@ class PhysicalHostPluginTestCase(tests.TestCase): self.db_host_update = self.patch(self.db_api, 'host_update') self.db_host_destroy = self.patch(self.db_api, 'host_destroy') - self.db_host_extra_capability_get_all_per_host = \ - self.patch(self.db_api, 'host_extra_capability_get_all_per_host') - self.db_host_extra_capability_get_all_per_name = \ - self.patch(self.db_api, 'host_extra_capability_get_all_per_name') - self.db_host_extra_capability_create = \ - self.patch(self.db_api, 'host_extra_capability_create') - self.db_host_extra_capability_update = \ - self.patch(self.db_api, 'host_extra_capability_update') + self.db_host_extra_capability_get_all_per_host = self.patch( + self.db_api, 'host_extra_capability_get_all_per_host') + + self.db_host_extra_capability_get_all_per_name = self.patch( + self.db_api, 'host_extra_capability_get_all_per_name') + + self.db_host_extra_capability_create = self.patch( + self.db_api, 'host_extra_capability_create') + + self.db_host_extra_capability_update = self.patch( + self.db_api, 'host_extra_capability_update') self.rp = rp self.nova_inventory = nova_inventory @@ -141,10 +165,13 @@ class PhysicalHostPluginTestCase(tests.TestCase): self.get_servers_per_host = self.patch( self.nova_inventory.NovaInventory, 'get_servers_per_host') self.get_servers_per_host.return_value = None - self.get_extra_capabilities = self.patch(self.fake_phys_plugin, - '_get_extra_capabilities') - self.get_extra_capabilities.return_value = {'foo': 'bar', - 'buzz': 'word'} + self.get_extra_capabilities = self.patch( + self.fake_phys_plugin, '_get_extra_capabilities') + + self.get_extra_capabilities.return_value = { + 'foo': 'bar', + 'buzz': 'word', + } self.fake_phys_plugin.setup(None) def test_get_host(self): @@ -179,7 +206,8 @@ class PhysicalHostPluginTestCase(tests.TestCase): fake_request = fake_host.copy() fake_capa = {'computehost_id': '1', 'capability_name': 'foo', - 'capability_value': 'bar'} + 'capability_value': 'bar', + } self.get_extra_capabilities.return_value = {'foo': 'bar'} self.db_host_create.return_value = self.fake_host host = self.fake_phys_plugin.create_computehost(fake_request) @@ -221,10 +249,12 @@ class PhysicalHostPluginTestCase(tests.TestCase): def test_update_host(self): host_values = {'foo': 'baz'} - self.db_host_extra_capability_get_all_per_name.return_value = \ - [{'id': '1', + self.db_host_extra_capability_get_all_per_name.return_value = [ + {'id': '1', 'capability_name': 'foo', - 'capability_value': 'bar'}] + 'capability_value': 'bar' + }, + ] self.fake_phys_plugin.update_computehost(self.fake_host_id, host_values) self.db_host_extra_capability_update.assert_called_once_with( @@ -234,10 +264,12 @@ class PhysicalHostPluginTestCase(tests.TestCase): def fake_db_host_extra_capability_update(*args, **kwargs): raise RuntimeError host_values = {'foo': 'baz'} - self.db_host_extra_capability_get_all_per_name.return_value = \ - [{'id': '1', + self.db_host_extra_capability_get_all_per_name.return_value = [ + {'id': '1', 'capability_name': 'foo', - 'capability_value': 'bar'}] + 'capability_value': 'bar' + }, + ] self.db_host_extra_capability_update.side_effect = \ fake_db_host_extra_capability_update self.assertRaises(manager_exceptions.CantAddExtraCapability, @@ -268,3 +300,322 @@ class PhysicalHostPluginTestCase(tests.TestCase): self.assertRaises(manager_exceptions.CantRemoveHost, self.fake_phys_plugin.delete_computehost, self.fake_host_id) + + def test_create_reservation_no_hosts_available(self): + values = { + 'lease_id': u'018c1b43-e69e-4aef-a543-09681539cf4c', + 'min': u'1', + 'max': u'1', + 'hypervisor_properties': '["=", "$memory_mb", "256"]', + 'resource_properties': '', + 'start_date': datetime.datetime(2013, 12, 19, 20, 00), + 'end_date': datetime.datetime(2013, 12, 19, 21, 00), + 'resource_type': u'physical:host', + } + reservation_values = { + 'id': u'441c1476-9f8f-4700-9f30-cd9b6fef3509', + 'lease_id': u'018c1b43-e69e-4aef-a543-09681539cf4c', + 'resource_id': '1', + 'resource_type': u'physical:host', + 'status': 'pending', + } + generate_uuid = self.patch(uuidutils, 'generate_uuid') + generate_uuid.return_value = u'441c1476-9f8f-4700-9f30-cd9b6fef3509' + self.rp_create.return_value = mock.MagicMock(id='1') + reservation_create = self.patch(self.db_api, 'reservation_create') + reservation_create.return_value = { + 'id': u'f9894fcf-e2ed-41e9-8a4c-92fac332608e', + } + host_reservation_create = self.patch(self.db_api, + 'host_reservation_create') + matching_hosts = self.patch(self.fake_phys_plugin, '_matching_hosts') + matching_hosts.return_value = [] + self.assertRaises(RuntimeError, + self.fake_phys_plugin.create_reservation, values) + reservation_create.assert_called_once_with(reservation_values) + host_values = { + 'reservation_id': u'f9894fcf-e2ed-41e9-8a4c-92fac332608e', + 'resource_properties': '', + 'hypervisor_properties': '["=", "$memory_mb", "256"]', + 'count_range': '1-1', + 'status': 'pending' + } + host_reservation_create.assert_called_once_with(host_values) + + def test_create_reservation_hosts_available(self): + values = { + 'lease_id': u'018c1b43-e69e-4aef-a543-09681539cf4c', + 'min': u'1', + 'max': u'1', + 'hypervisor_properties': '["=", "$memory_mb", "256"]', + 'resource_properties': '', + 'start_date': datetime.datetime(2013, 12, 19, 20, 00), + 'end_date': datetime.datetime(2013, 12, 19, 21, 00), + 'resource_type': u'physical:host', + } + reservation_values = { + 'id': u'441c1476-9f8f-4700-9f30-cd9b6fef3509', + 'lease_id': u'018c1b43-e69e-4aef-a543-09681539cf4c', + 'resource_id': '1', + 'resource_type': u'physical:host', + 'status': 'pending', + } + generate_uuid = self.patch(uuidutils, 'generate_uuid') + generate_uuid.return_value = u'441c1476-9f8f-4700-9f30-cd9b6fef3509' + self.rp_create.return_value = mock.MagicMock(id='1') + reservation_create = self.patch(self.db_api, 'reservation_create') + reservation_create.return_value = { + 'id': u'f9894fcf-e2ed-41e9-8a4c-92fac332608e', + } + host_reservation_create = self.patch(self.db_api, + 'host_reservation_create') + matching_hosts = self.patch(self.fake_phys_plugin, '_matching_hosts') + matching_hosts.return_value = ['host1', 'host2'] + host_allocation_create = self.patch( + self.db_api, + 'host_allocation_create') + self.fake_phys_plugin.create_reservation(values) + reservation_create.assert_called_once_with(reservation_values) + host_values = { + 'reservation_id': u'f9894fcf-e2ed-41e9-8a4c-92fac332608e', + 'resource_properties': '', + 'hypervisor_properties': '["=", "$memory_mb", "256"]', + 'count_range': '1-1', + 'status': 'pending', + } + host_reservation_create.assert_called_once_with(host_values) + calls = [ + mock.call( + {'compute_host_id': 'host1', + 'reservation_id': u'f9894fcf-e2ed-41e9-8a4c-92fac332608e', + }), + mock.call( + {'compute_host_id': 'host2', + 'reservation_id': u'f9894fcf-e2ed-41e9-8a4c-92fac332608e', + }), + ] + host_allocation_create.assert_has_calls(calls) + + def test_on_start(self): + reservation_get_all_by_values = self.patch( + self.db_api, 'reservation_get_all_by_values') + + reservation_get_all_by_values.return_value = [ + { + 'id': u'593e7028-c0d1-4d76-8642-2ffd890b324c', + 'resource_id': u'04de74e8-193a-49d2-9ab8-cba7b49e45e8', + } + ] + host_allocation_get_all_by_values = self.patch( + self.db_api, 'host_allocation_get_all_by_values') + + host_allocation_get_all_by_values.return_value = [ + {'compute_host_id': 'host1'}, + ] + host_get = self.patch(self.db_api, 'host_get') + host_get.return_value = {'hypervisor_hostname': 'host1_hostname'} + add_computehost = self.patch( + self.rp.ReservationPool, 'add_computehost') + + self.fake_phys_plugin.on_start(u'04de74e8-193a-49d2-9ab8-cba7b49e45e8') + + add_computehost.assert_called_with( + u'04de74e8-193a-49d2-9ab8-cba7b49e45e8', 'host1_hostname') + + def test_on_end_with_instances(self): + reservation_get_all_by_values = self.patch( + self.db_api, + 'reservation_get_all_by_values') + + reservation_get_all_by_values.return_value = [ + { + 'id': u'593e7028-c0d1-4d76-8642-2ffd890b324c', + 'resource_id': u'04de74e8-193a-49d2-9ab8-cba7b49e45e8', + } + ] + reservation_update = self.patch(self.db_api, 'reservation_update') + host_reservation_get_by_reservation_id = self.patch( + self.db_api, + 'host_reservation_get_by_reservation_id') + host_reservation_get_by_reservation_id.return_value = { + 'id': u'35fc4e6a-ba57-4a36-be30-6012377a0387', + } + host_reservation_update = self.patch( + self.db_api, + 'host_reservation_update') + host_allocation_get_all_by_values = self.patch( + self.db_api, + 'host_allocation_get_all_by_values') + host_allocation_get_all_by_values.return_value = [ + {'id': u'bfa9aa0b-8042-43eb-a4e6-4555838bf64f', + 'compute_host_id': u'cdae2a65-236f-475a-977d-f6ad82f828b7', + }, + ] + host_allocation_destroy = self.patch( + self.db_api, + 'host_allocation_destroy') + delete = self.patch(self.rp.ReservationPool, 'delete') + self.patch(self.fake_phys_plugin, '_get_hypervisor_from_name') + get_hypervisors = self.patch(self.nova.hypervisors, 'get') + get_hypervisors.return_value = mock.MagicMock(running_vms=1) + self.fake_phys_plugin.on_end(u'04de74e8-193a-49d2-9ab8-cba7b49e45e8') + reservation_update.assert_called_with( + u'593e7028-c0d1-4d76-8642-2ffd890b324c', {'status': 'completed'}) + host_reservation_update.assert_called_with( + u'35fc4e6a-ba57-4a36-be30-6012377a0387', {'status': 'completed'}) + host_allocation_destroy.assert_called_with( + u'bfa9aa0b-8042-43eb-a4e6-4555838bf64f') + assert not delete.called + + def test_on_end_without_instances(self): + reservation_get_all_by_values = self.patch( + self.db_api, + 'reservation_get_all_by_values') + reservation_get_all_by_values.return_value = [ + { + 'id': u'593e7028-c0d1-4d76-8642-2ffd890b324c', + 'resource_id': u'04de74e8-193a-49d2-9ab8-cba7b49e45e8', + }, + ] + reservation_update = self.patch(self.db_api, 'reservation_update') + host_reservation_get_by_reservation_id = self.patch( + self.db_api, + 'host_reservation_get_by_reservation_id') + host_reservation_get_by_reservation_id.return_value = { + 'id': u'35fc4e6a-ba57-4a36-be30-6012377a0387', + } + host_reservation_update = self.patch( + self.db_api, + 'host_reservation_update') + host_allocation_get_all_by_values = self.patch( + self.db_api, + 'host_allocation_get_all_by_values') + host_allocation_get_all_by_values.return_value = [ + {'id': u'bfa9aa0b-8042-43eb-a4e6-4555838bf64f', + 'compute_host_id': u'cdae2a65-236f-475a-977d-f6ad82f828b7', + }, + ] + host_allocation_destroy = self.patch( + self.db_api, + 'host_allocation_destroy') + delete = self.patch(self.rp.ReservationPool, 'delete') + self.patch(self.fake_phys_plugin, '_get_hypervisor_from_name') + get_hypervisors = self.patch(self.nova.hypervisors, 'get') + get_hypervisors.return_value = mock.MagicMock(running_vms=0) + self.fake_phys_plugin.on_end(u'04de74e8-193a-49d2-9ab8-cba7b49e45e8') + reservation_update.assert_called_with( + u'593e7028-c0d1-4d76-8642-2ffd890b324c', {'status': 'completed'}) + host_reservation_update.assert_called_with( + u'35fc4e6a-ba57-4a36-be30-6012377a0387', {'status': 'completed'}) + host_allocation_destroy.assert_called_with( + u'bfa9aa0b-8042-43eb-a4e6-4555838bf64f') + delete.assert_called_with(u'04de74e8-193a-49d2-9ab8-cba7b49e45e8') + + def test_matching_hosts_not_allocated_hosts(self): + def host_allocation_get_all_by_values(**kwargs): + if kwargs['compute_host_id'] == 'host1': + return True + host_get = self.patch( + self.db_api, + 'host_get_all_by_queries') + host_get.return_value = [ + {'id': 'host1'}, + {'id': 'host2'}, + {'id': 'host3'}, + ] + host_get = self.patch( + self.db_api, + 'host_allocation_get_all_by_values') + host_get.side_effect = host_allocation_get_all_by_values + host_get = self.patch( + self.db_utils, + 'get_free_periods') + host_get.return_value = [ + (datetime.datetime(2013, 12, 19, 20, 00), + datetime.datetime(2013, 12, 19, 21, 00)), + ] + result = self.fake_phys_plugin._matching_hosts( + '[]', '[]', '1-3', + datetime.datetime(2013, 12, 19, 20, 00), + datetime.datetime(2013, 12, 19, 21, 00)) + self.assertEqual(['host2', 'host3'], result) + + def test_matching_hosts_allocated_hosts(self): + def host_allocation_get_all_by_values(**kwargs): + if kwargs['compute_host_id'] == 'host1': + return True + host_get = self.patch( + self.db_api, + 'host_get_all_by_queries') + host_get.return_value = [ + {'id': 'host1'}, + {'id': 'host2'}, + {'id': 'host3'}, + ] + host_get = self.patch( + self.db_api, + 'host_allocation_get_all_by_values') + host_get.side_effect = host_allocation_get_all_by_values + host_get = self.patch( + self.db_utils, + 'get_free_periods') + host_get.return_value = [ + (datetime.datetime(2013, 12, 19, 20, 00), + datetime.datetime(2013, 12, 19, 21, 00)), + ] + result = self.fake_phys_plugin._matching_hosts( + '[]', '[]', '3-3', + datetime.datetime(2013, 12, 19, 20, 00), + datetime.datetime(2013, 12, 19, 21, 00)) + self.assertEqual(['host1', 'host2', 'host3'], result) + + def test_matching_hosts_not_matching(self): + host_get = self.patch( + self.db_api, + 'host_get_all_by_queries') + host_get.return_value = [] + result = self.fake_phys_plugin._matching_hosts( + '["=", "$memory_mb", "2048"]', '[]', '1-1', + datetime.datetime(2013, 12, 19, 20, 00), + datetime.datetime(2013, 12, 19, 21, 00)) + self.assertEqual([], result) + + def test_convert_requirements_empty(self): + request = '[]' + result = self.fake_phys_plugin._convert_requirements(request) + self.assertEqual([], result) + + def test_convert_requirements_small(self): + request = '["=", "$memory", "4096"]' + result = self.fake_phys_plugin._convert_requirements(request) + self.assertEqual(['memory == 4096'], result) + + def test_convert_requirements_with_incorrect_syntax_1(self): + self.assertRaises( + RuntimeError, self.fake_phys_plugin._convert_requirements, + '["a", "$memory", "4096"]') + + def test_convert_requirements_with_incorrect_syntax_2(self): + self.assertRaises( + RuntimeError, self.fake_phys_plugin._convert_requirements, + '["=", "memory", "4096"]') + + def test_convert_requirements_with_incorrect_syntax_3(self): + self.assertRaises( + RuntimeError, self.fake_phys_plugin._convert_requirements, + '["=", "$memory", 4096]') + + def test_convert_requirements_complex(self): + request = '["and", [">", "$memory", "4096"], [">", "$disk", "40"]]' + result = self.fake_phys_plugin._convert_requirements(request) + self.assertEqual(['memory > 4096', 'disk > 40'], result) + + def test_convert_requirements_complex_with_incorrect_syntax_1(self): + self.assertRaises( + RuntimeError, self.fake_phys_plugin._convert_requirements, + '["and", [">", "memory", "4096"], [">", "$disk", "40"]]') + + def test_convert_requirements_complex_with_incorrect_syntax_2(self): + self.assertRaises( + RuntimeError, self.fake_phys_plugin._convert_requirements, + '["fail", [">", "$memory", "4096"], [">", "$disk", "40"]]') diff --git a/setup.cfg b/setup.cfg index 01bde6c1..2f143068 100644 --- a/setup.cfg +++ b/setup.cfg @@ -35,7 +35,7 @@ console_scripts = climate.resource.plugins = dummy.vm.plugin=climate.plugins.dummy_vm_plugin:DummyVMPlugin - physical.host.plugin=climate.plugins.physical_host_plugin:PhysicalHostPlugin + physical.host.plugin=climate.plugins.oshosts.host_plugin:PhysicalHostPlugin [build_sphinx] all_files = 1