diff --git a/heat/engine/resources/openstack/blazar/host.py b/heat/engine/resources/openstack/blazar/host.py new file mode 100644 index 0000000000..e8745dac64 --- /dev/null +++ b/heat/engine/resources/openstack/blazar/host.py @@ -0,0 +1,167 @@ +# +# 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 heat.common import exception +from heat.common.i18n import _ +from heat.engine import attributes +from heat.engine import properties +from heat.engine import resource +from heat.engine import support + + +class Host(resource.Resource): + """A resource to manage Blazar hosts. + + Host resource manages the physical hosts for the lease/reservation + within OpenStack. + + # TODO(asmita): Based on an agreement with Blazar team, this resource + class does not support updating host resource as currently Blazar does + not support to delete existing extra_capability keys while updating host. + Also, in near future, when Blazar team will come up with a new alternative + API to resolve this issue, we will need to modify this class. + """ + + support_status = support.SupportStatus(version='12.0.0') + + PROPERTIES = ( + NAME, EXTRA_CAPABILITY, + ) = ( + 'name', 'extra_capability', + ) + + ATTRIBUTES = ( + HYPERVISOR_HOSTNAME, HYPERVISOR_TYPE, HYPERVISOR_VERSION, + VCPUS, CPU_INFO, MEMORY_MB, LOCAL_GB, + SERVICE_NAME, RESERVABLE, STATUS, TRUST_ID, + EXTRA_CAPABILITY_ATTR, CREATED_AT, UPDATED_AT, + ) = ( + 'hypervisor_hostname', 'hypervisor_type', 'hypervisor_version', + 'vcpus', 'cpu_info', 'memory_mb', 'local_gb', + 'service_name', 'reservable', 'status', 'trust_id', + 'extra_capability', 'created_at', 'updated_at', + ) + + properties_schema = { + NAME: properties.Schema( + properties.Schema.STRING, + _('The name of the host.'), + required=True, + ), + EXTRA_CAPABILITY: properties.Schema( + properties.Schema.MAP, + _('The extra capability of the host.'), + ) + } + + attributes_schema = { + HYPERVISOR_HOSTNAME: attributes.Schema( + _('The hypervisor name of the host.'), + type=attributes.Schema.STRING, + ), + HYPERVISOR_TYPE: attributes.Schema( + _('The hypervisor type the host.'), + type=attributes.Schema.STRING, + ), + HYPERVISOR_VERSION: attributes.Schema( + _('The hypervisor version of the host.'), + type=attributes.Schema.INTEGER, + ), + VCPUS: attributes.Schema( + _('The number of the VCPUs of the host.'), + type=attributes.Schema.INTEGER, + ), + CPU_INFO: attributes.Schema( + _('Information of the CPU of the host.'), + type=attributes.Schema.MAP, + ), + MEMORY_MB: attributes.Schema( + _('Megabytes of the memory of the host.'), + type=attributes.Schema.INTEGER, + ), + LOCAL_GB: attributes.Schema( + _('Gigabytes of the disk of the host.'), + type=attributes.Schema.INTEGER, + ), + SERVICE_NAME: attributes.Schema( + _('The compute service name of the host.'), + type=attributes.Schema.STRING, + ), + RESERVABLE: attributes.Schema( + _('The flag which represents whether the host is reservable ' + 'or not.'), + type=attributes.Schema.BOOLEAN, + ), + STATUS: attributes.Schema( + _('The status of the host.'), + type=attributes.Schema.STRING, + ), + TRUST_ID: attributes.Schema( + _('The UUID of the trust of the host operator.'), + type=attributes.Schema.STRING, + ), + EXTRA_CAPABILITY_ATTR: attributes.Schema( + _('The extra capability of the host.'), + type=attributes.Schema.MAP, + ), + CREATED_AT: attributes.Schema( + _('The date and time when the host was created. ' + 'The date and time format must be "CCYY-MM-DD hh:mm".'), + type=attributes.Schema.STRING, + ), + UPDATED_AT: attributes.Schema( + _('The date and time when the host was updated. ' + 'The date and time format must be "CCYY-MM-DD hh:mm".'), + type=attributes.Schema.STRING + ), + } + + default_client_name = 'blazar' + + entity = 'host' + + def _parse_extra_capability(self, args): + if self.NAME in args[self.EXTRA_CAPABILITY]: + # Remove "name" key if present in the extra_capability property. + del args[self.EXTRA_CAPABILITY][self.NAME] + args.update(args[self.EXTRA_CAPABILITY]) + args.pop(self.EXTRA_CAPABILITY) + return args + + def handle_create(self): + args = dict((k, v) for k, v in self.properties.items() + if v is not None) + + if self.EXTRA_CAPABILITY in args: + args = self._parse_extra_capability(args) + + host = self.client_plugin().create_host(**args) + self.resource_id_set(host['id']) + + return host['id'] + + def _resolve_attribute(self, name): + if self.resource_id is None: + return + host = self.client_plugin().get_host(self.resource_id) + try: + return host[name] + except KeyError: + raise exception.InvalidTemplateAttribute(resource=self.name, + key=name) + + +def resource_mapping(): + return { + 'OS::Blazar::Host': Host + } diff --git a/heat/policies/resource_types.py b/heat/policies/resource_types.py index a706aea0f4..27b067c289 100644 --- a/heat/policies/resource_types.py +++ b/heat/policies/resource_types.py @@ -61,6 +61,9 @@ resource_types_policies = [ check_str=base.RULE_PROJECT_ADMIN), policy.RuleDefault( name=POLICY_ROOT % 'OS::Keystone::*', + check_str=base.RULE_PROJECT_ADMIN), + policy.RuleDefault( + name=POLICY_ROOT % 'OS::Blazar::Host', check_str=base.RULE_PROJECT_ADMIN) ] diff --git a/heat/tests/openstack/blazar/test_host.py b/heat/tests/openstack/blazar/test_host.py new file mode 100644 index 0000000000..ab3d750a96 --- /dev/null +++ b/heat/tests/openstack/blazar/test_host.py @@ -0,0 +1,167 @@ +# +# 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 blazarclient import exception as client_exception +import mock +from oslo_utils.fixture import uuidsentinel as uuids + +from heat.common import exception +from heat.common import template_format +from heat.engine.clients.os import blazar +from heat.engine.resources.openstack.blazar import host +from heat.engine import scheduler +from heat.tests import common +from heat.tests import utils + + +blazar_host_template = ''' +heat_template_version: rocky + +resources: + test-host: + type: OS::Blazar::Host + properties: + name: test-host + extra_capability: + gpu: true +''' + +blazar_host_template_extra_capability = ''' +heat_template_version: rocky + +resources: + test-host: + type: OS::Blazar::Host + properties: + name: test-host + extra_capability: + gpu: true + name: test-name +''' + + +class BlazarHostTestCase(common.HeatTestCase): + + def setUp(self): + super(BlazarHostTestCase, self).setUp() + + self.host = { + "id": uuids.id, + "name": "test-host", + "gpu": True, + "hypervisor_hostname": "compute-1", + "hypervisor_type": "QEMU", + "hypervisor_version": 2010001, + "cpu_info": "{" + "'arch': 'x86_64', 'model': 'cpu64-rhel6', " + "'vendor': 'Intel', 'features': " + "['pge', 'clflush', 'sep', 'syscall', 'msr', " + "'vmx', 'cmov', 'nx', 'pat', 'lm', 'tsc', " + "'fpu', 'fxsr', 'pae', 'mmx', 'cx8', 'mce', " + "'de', 'mca', 'pse', 'pni', 'apic', 'sse', " + "'lahf_lm', 'sse2', 'hypervisor', 'cx16', " + "'pse36', 'mttr', 'x2apic'], " + "'topology': {'cores': 1, 'cells': 1, 'threads': 1, " + "'sockets': 4}}", + "memory_mb": 8192, + "local_gb": 100, + "vcpus": 2, + "service_name": "compute-1", + "reservable": True, + "trust_id": uuids.trust_id, + "created_at": "2020-01-01 08:00", + "updated_at": "2020-01-01 12:00", + "extra_capability": "foo" + } + t = template_format.parse(blazar_host_template) + self.stack = utils.parse_stack(t) + resource_defns = self.stack.t.resource_definitions(self.stack) + self.rsrc_defn = resource_defns['test-host'] + self.client = mock.Mock() + self.patchobject(blazar.BlazarClientPlugin, 'client', + return_value=self.client) + + def _create_resource(self, name, snippet, stack): + self.client.host.create.return_value = self.host + return host.Host(name, snippet, stack) + + def test_host_create(self): + host_resource = self._create_resource('host', self.rsrc_defn, + self.stack) + + self.assertEqual(self.host['name'], + host_resource.properties.get(host.Host.NAME)) + + scheduler.TaskRunner(host_resource.create)() + self.assertEqual(uuids.id, host_resource.resource_id) + self.assertEqual((host_resource.CREATE, host_resource.COMPLETE), + host_resource.state) + self.assertEqual('host', host_resource.entity) + self.client.host.create.assert_called_once_with( + name=self.host['name'], gpu=self.host['gpu']) + + def test_host_delete(self): + host_resource = self._create_resource('host', self.rsrc_defn, + self.stack) + + scheduler.TaskRunner(host_resource.create)() + self.client.host.delete.return_value = None + self.client.host.get.side_effect = [ + 'host_obj', client_exception.BlazarClientException(code=404)] + scheduler.TaskRunner(host_resource.delete)() + self.assertEqual((host_resource.DELETE, host_resource.COMPLETE), + host_resource.state) + self.client.host.delete.assert_called_once_with(uuids.id) + + def test_host_delete_not_found(self): + host_resource = self._create_resource('host', self.rsrc_defn, + self.stack) + + scheduler.TaskRunner(host_resource.create)() + self.client.host.delete.side_effect = client_exception.\ + BlazarClientException(code=404) + self.client.host.get.side_effect = client_exception.\ + BlazarClientException(code=404) + scheduler.TaskRunner(host_resource.delete)() + self.assertEqual((host_resource.DELETE, host_resource.COMPLETE), + host_resource.state) + + def test_parse_extra_capability(self): + t = template_format.parse(blazar_host_template_extra_capability) + stack = utils.parse_stack(t) + resource_defns = self.stack.t.resource_definitions(stack) + rsrc_defn = resource_defns['test-host'] + host_resource = self._create_resource('host', rsrc_defn, stack) + args = dict((k, v) for k, v in host_resource.properties.items() + if v is not None) + parsed_args = host_resource._parse_extra_capability(args) + self.assertEqual({'gpu': True, 'name': 'test-host'}, parsed_args) + + def test_resolve_attributes(self): + host_resource = self._create_resource('host', self.rsrc_defn, + self.stack) + + scheduler.TaskRunner(host_resource.create)() + self.client.host.get.return_value = self.host + self.assertEqual(self.host['vcpus'], + host_resource._resolve_attribute(host.Host.VCPUS)) + + def test_resolve_attributes_not_found(self): + host_resource = self._create_resource('host', self.rsrc_defn, + self.stack) + + scheduler.TaskRunner(host_resource.create)() + self.client.host.get.return_value = self.host + self.assertRaises(exception.InvalidTemplateAttribute, + host_resource._resolve_attribute, + 'invalid_attribute') diff --git a/releasenotes/notes/add-blazar-host-resource-392ce00635ac27ed.yaml b/releasenotes/notes/add-blazar-host-resource-392ce00635ac27ed.yaml new file mode 100644 index 0000000000..4e41ebcb47 --- /dev/null +++ b/releasenotes/notes/add-blazar-host-resource-392ce00635ac27ed.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + A new ``OS::Blazar::Host`` resource is added to manage compute hosts for + the lease/reservation in OpenStack.