diff --git a/contrib/nova_flavor/nova_flavor/README.md b/contrib/nova_flavor/nova_flavor/README.md new file mode 100644 index 0000000000..323514107c --- /dev/null +++ b/contrib/nova_flavor/nova_flavor/README.md @@ -0,0 +1,55 @@ +Nova Flavor plugin for OpenStack Heat +===================================== + +This plugin enables using Nova Flavors as resources in a Heat template. + +Note that the current implementation of the Nova Flavor resource does not +allow specifying the name and flavorid properties for the resource. +This is done to avoid potential naming collision upon flavor creation as +all flavor have a global scope. + +### 1. Install the Nova Flavor plugin in Heat + +NOTE: Heat scans several directories to find plugins. The list of directories +is specified in the configuration file "heat.conf" with the "plugin_dirs" +directive. + +### 2. Restart heat + +Only the process "heat-engine" needs to be restarted to load the new installed +plugin. + +### Template Format + +Here's an example nova flavor resource: +```yaml +heat_template_version: 2013-05-23 +description: Heat Flavor creation example +resources: + test_flavor: + type: OS::Nova::Flavor + properties: + ram: 1024 + vcpus: 1 + disk: 20 + swap: 2 +``` + +### Issues with the Nova Flavor plugin + +By default only the admin tenant can manage flavors because of the default +policy in Nova: ```"compute_extension:flavormanage": "rule:admin_api"``` + +To let the possibility to all tenants to create flavors, the rule must be +replaced with the following: ```"compute_extension:flavormanage": ""``` + +The following error occurs if the policy has not been correctly set: + ERROR: Policy doesn't allow compute_extension:flavormanage to be performed. + +Currently all nova flavors have a global scope, which leads to several issues: +1. Per-stack flavor creation will pollute the global flavor list. +2. If two stacks create a flavor with the same name collision will occur, +which will lead to the following error: + + ERROR (Conflict): Flavor with name dupflavor already exists. + diff --git a/contrib/nova_flavor/nova_flavor/__init__.py b/contrib/nova_flavor/nova_flavor/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/contrib/nova_flavor/nova_flavor/resources/__init__.py b/contrib/nova_flavor/nova_flavor/resources/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/contrib/nova_flavor/nova_flavor/resources/nova_flavor.py b/contrib/nova_flavor/nova_flavor/resources/nova_flavor.py new file mode 100644 index 0000000000..fef9bd7d87 --- /dev/null +++ b/contrib/nova_flavor/nova_flavor/resources/nova_flavor.py @@ -0,0 +1,114 @@ +# +# 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 novaclient import exceptions as nova_exceptions + +from heat.engine import properties +from heat.engine import resource +from heat.openstack.common.gettextutils import _ +from heat.openstack.common import log as logging + +logger = logging.getLogger(__name__) + + +class NovaFlavor(resource.Resource): + """ + A resource for creating OpenStack virtual hardware templates. + + Due to default nova security policy usage of this resource is limited to + being used by administrators only. The rights may also be delegated to + other users by redefining the access controls on the nova-api server. + + Note that the current implementation of the Nova Flavor resource does not + allow specifying the name and flavorid properties for the resource. + This is done to avoid potential naming collision upon flavor creation as + all flavor have a global scope. + """ + + PROPERTIES = ( + RAM, VCPUS, DISK, SWAP, EPHEMERAL, + RXTX_FACTOR, + ) = ( + 'ram', 'vcpus', 'disk', 'swap', 'ephemeral', + 'rxtx_factor', + ) + + properties_schema = { + RAM: properties.Schema( + properties.Schema.INTEGER, + _('Memory in MB for the flavor.'), + required=True + ), + VCPUS: properties.Schema( + properties.Schema.INTEGER, + _('Number of VCPUs for the flavor.'), + required=True + ), + DISK: properties.Schema( + properties.Schema.INTEGER, + _('Size of local disk in GB. Set the value to 0 to remove limit ' + 'on disk size.'), + required=True, + default=0 + ), + SWAP: properties.Schema( + properties.Schema.INTEGER, + _('Swap space in MB.'), + default=0 + ), + EPHEMERAL: properties.Schema( + properties.Schema.INTEGER, + _('Size of a secondary ephemeral data disk in GB.'), + default=0 + ), + RXTX_FACTOR: properties.Schema( + properties.Schema.NUMBER, + _('RX/TX factor.'), + default=1.0 + ), + } + + def __init__(self, name, json_snippet, stack): + super(NovaFlavor, self).__init__(name, json_snippet, stack) + + def handle_create(self): + args = dict(self.properties) + args['flavorid'] = 'auto' + args['name'] = self.physical_resource_name() + args['is_public'] = False + + flavor = self.nova().flavors.create(**args) + self.resource_id_set(flavor.id) + + tenant = self.stack.context.tenant_id + # grant access to the active project and the admin project + self.nova().flavor_access.add_tenant_access(flavor, tenant) + self.nova().flavor_access.add_tenant_access(flavor, 'admin') + + def handle_delete(self): + if self.resource_id is None: + return + + try: + self.nova().flavors.delete(self.resource_id) + except nova_exceptions.NotFound: + logger.debug( + _('Could not find flavor %s.') % self.resource_id) + + self.resource_id_set(None) + + +def resource_mapping(): + return { + 'OS::Nova::Flavor': NovaFlavor + } diff --git a/contrib/nova_flavor/nova_flavor/tests/__init__.py b/contrib/nova_flavor/nova_flavor/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/contrib/nova_flavor/nova_flavor/tests/test_nova_flavor.py b/contrib/nova_flavor/nova_flavor/tests/test_nova_flavor.py new file mode 100644 index 0000000000..b893927363 --- /dev/null +++ b/contrib/nova_flavor/nova_flavor/tests/test_nova_flavor.py @@ -0,0 +1,88 @@ +# +# 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 mock + +from heat.engine import parser +from heat.engine import template +from heat.engine import resource +from heat.tests.common import HeatTestCase +from heat.tests import utils +from novaclient.exceptions import NotFound + +from ..resources.nova_flavor import resource_mapping # noqa +from ..resources.nova_flavor import NovaFlavor # noqa + +flavor_template = { + 'heat_template_version': '2013-05-23', + 'resources': { + 'my_flavor': { + 'type': 'OS::Nova::Flavor', + 'properties': { + 'ram': 1024, + 'vcpus': 2, + 'disk': 20, + 'swap': 2, + 'rxtx_factor': 1.0, + 'ephemeral': 0, + } + } + } +} + + +class NovaFlavorTest(HeatTestCase): + def setUp(self): + super(NovaFlavorTest, self).setUp() + + self.ctx = utils.dummy_context() + + # For unit testing purpose. Register resource provider + # explicitly. + resource._register_class("OS::Nova::Flavor", NovaFlavor) + + self.stack = parser.Stack( + self.ctx, 'nova_flavor_test_stack', + template.Template(flavor_template) + ) + + self.my_flavor = self.stack['my_flavor'] + nova = mock.MagicMock() + self.novaclient = mock.MagicMock() + self.my_flavor.nova = nova + nova.return_value = self.novaclient + self.flavors = self.novaclient.flavors + + def test_resource_mapping(self): + mapping = resource_mapping() + self.assertEqual(1, len(mapping)) + self.assertEqual(NovaFlavor, mapping['OS::Nova::Flavor']) + self.assertIsInstance(self.my_flavor, NovaFlavor) + + def test_flavor_handle_create(self): + value = mock.MagicMock() + flavor_id = '927202df-1afb-497f-8368-9c2d2f26e5db' + value.id = flavor_id + self.flavors.create.return_value = value + self.my_flavor.handle_create() + self.assertEqual(flavor_id, self.my_flavor.resource_id) + + def test_flavor_handle_delete(self): + self.resource_id = None + self.assertIsNone(self.my_flavor.handle_delete()) + flavor_id = '927202df-1afb-497f-8368-9c2d2f26e5db' + self.my_flavor.resource_id = flavor_id + self.flavors.delete.return_value = None + self.assertIsNone(self.my_flavor.handle_delete()) + self.flavors.delete.side_effect = NotFound(404, "Not found") + self.assertIsNone(self.my_flavor.handle_delete())