Add a Nova Flavor resource.

This will allow using more fine-grained specification of computational
resources (RAM, CPU, disk, etc.).

A nova flavor resource has the following format:

  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

Change-Id: I79812f0ef0d0dc616ccb3361e9b5864faa877df1
Implements: blueprint dynamic-flavors
This commit is contained in:
Dimitri Mazmanov 2014-04-23 10:58:36 +02:00
parent 635fad8ffb
commit 60f07fbdac
6 changed files with 257 additions and 0 deletions

View File

@ -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.

View File

@ -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
}

View File

@ -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())