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:
parent
635fad8ffb
commit
60f07fbdac
55
contrib/nova_flavor/nova_flavor/README.md
Normal file
55
contrib/nova_flavor/nova_flavor/README.md
Normal 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.
|
||||
|
0
contrib/nova_flavor/nova_flavor/__init__.py
Normal file
0
contrib/nova_flavor/nova_flavor/__init__.py
Normal file
114
contrib/nova_flavor/nova_flavor/resources/nova_flavor.py
Normal file
114
contrib/nova_flavor/nova_flavor/resources/nova_flavor.py
Normal 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
|
||||
}
|
0
contrib/nova_flavor/nova_flavor/tests/__init__.py
Normal file
0
contrib/nova_flavor/nova_flavor/tests/__init__.py
Normal file
88
contrib/nova_flavor/nova_flavor/tests/test_nova_flavor.py
Normal file
88
contrib/nova_flavor/nova_flavor/tests/test_nova_flavor.py
Normal 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())
|
Loading…
Reference in New Issue
Block a user