Nova Flavor Resource Update

Add ability to associate private flavors with multiple tenants.
Also updated 'test_flavor' file to reflect changes and test
functionality.

Change-Id: I841460b936ae52b3a91981029b42212ae484c3e4
Implements: blueprint nova-flavor-resource
This commit is contained in:
Chris 2016-11-07 13:13:24 -06:00
parent 2ea7781920
commit 1b64098cdf
2 changed files with 113 additions and 10 deletions

View File

@ -10,13 +10,19 @@
# 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 oslo_log import log as logging
from heat.common import exception
from heat.common.i18n import _
from heat.common.i18n import _LI
from heat.engine import attributes
from heat.engine import constraints
from heat.engine import properties
from heat.engine import resource
from heat.engine import support
from heat.engine import translation
LOG = logging.getLogger(__name__)
class NovaFlavor(resource.Resource):
@ -41,10 +47,10 @@ class NovaFlavor(resource.Resource):
entity = 'flavors'
PROPERTIES = (
ID, NAME, RAM, VCPUS, DISK, SWAP,
TENANTS, ID, NAME, RAM, VCPUS, DISK, SWAP,
EPHEMERAL, RXTX_FACTOR, EXTRA_SPECS, IS_PUBLIC
) = (
'flavorid', 'name', 'ram', 'vcpus', 'disk', 'swap',
'tenants', 'flavorid', 'name', 'ram', 'vcpus', 'disk', 'swap',
'ephemeral', 'rxtx_factor', 'extra_specs', 'is_public',
)
@ -55,6 +61,17 @@ class NovaFlavor(resource.Resource):
)
properties_schema = {
TENANTS: properties.Schema(
properties.Schema.LIST,
_('List of tenants.'),
update_allowed=True,
default=[],
schema=properties.Schema(
properties.Schema.STRING,
constraints=[constraints.CustomConstraint('keystone.project')]
),
support_status=support.SupportStatus(version='8.0.0')
),
ID: properties.Schema(
properties.Schema.STRING,
_('Unique ID of the flavor. If not specified, '
@ -127,6 +144,17 @@ class NovaFlavor(resource.Resource):
)
}
def translation_rules(self, properties):
return [
translation.TranslationRule(
properties,
translation.TranslationRule.RESOLVE,
[self.TENANTS],
client_plugin=self.client_plugin(),
finder='get_project_id'
)
]
def handle_create(self):
args = dict(self.properties)
if not args['flavorid']:
@ -134,15 +162,23 @@ class NovaFlavor(resource.Resource):
if not args['name']:
args['name'] = self.physical_resource_name()
flavor_keys = args.pop(self.EXTRA_SPECS)
tenants = args.pop(self.TENANTS)
flavor = self.client().flavors.create(**args)
self.resource_id_set(flavor.id)
if flavor_keys:
flavor.set_keys(flavor_keys)
tenant = self.stack.context.tenant_id
if not args['is_public']:
# grant access only to the active project(private flavor)
self.client().flavor_access.add_tenant_access(flavor, tenant)
if not self.IS_PUBLIC:
if not tenants:
LOG.info(_LI('Tenant property is recommended if IS_PUBLIC'
'is false.'))
tenant = self.stack.context.tenant_id
self.client().flavor_access.add_tenant_access(flavor, tenant)
else:
for tenant in tenants:
# grant access only to the active project(private flavor)
self.client().flavor_access.add_tenant_access(flavor,
tenant)
def handle_update(self, json_snippet, tmpl_diff, prop_diff):
"""Update nova flavor."""
@ -153,6 +189,23 @@ class NovaFlavor(resource.Resource):
new_keys = prop_diff.get(self.EXTRA_SPECS)
if new_keys is not None:
flavor.set_keys(new_keys)
"""Update tenant access list."""
if self.TENANTS in prop_diff and not self.IS_PUBLIC:
kwargs = {'flavor': self.resource_id}
old_tenants = [
x.tenant_id for x in self.client().flavor_access.list(**kwargs)
] or []
new_tenants = prop_diff.get(self.TENANTS) or []
tenants_to_remove = list(set(old_tenants) - set(new_tenants))
tenants_to_add = list(set(new_tenants) - set(old_tenants))
if tenants_to_remove or tenants_to_add:
flavor = self.client().flavors.get(self.resource_id)
for _tenant in tenants_to_remove:
self.client().flavor_access.remove_tenant_access(flavor,
_tenant)
for _tenant in tenants_to_add:
self.client().flavor_access.add_tenant_access(flavor,
_tenant)
def _resolve_attribute(self, name):
if self.resource_id is None:

View File

@ -31,7 +31,8 @@ flavor_template = {
'swap': 2,
'rxtx_factor': 1.0,
'ephemeral': 0,
'extra_specs': {"foo": "bar"}
'extra_specs': {"foo": "bar"},
'tenants': []
}
}
}
@ -45,11 +46,15 @@ class NovaFlavorTest(common.HeatTestCase):
return_value=True)
self.ctx = utils.dummy_context()
def create_flavor(self, with_name_id=False):
def create_flavor(self, with_name_id=False, is_public=True):
if with_name_id:
props = flavor_template['resources']['my_flavor']['properties']
props['name'] = 'test_flavor'
props['flavorid'] = '1234'
if not is_public:
props = flavor_template['resources']['my_flavor']['properties']
props['is_public'] = False
props['tenants'] = ["foo", "bar"]
self.stack = stack.Stack(
self.ctx, 'nova_flavor_test_stack',
template.Template(flavor_template)
@ -108,17 +113,21 @@ class NovaFlavorTest(common.HeatTestCase):
self.assertTrue(self.my_flavor.FnGetAtt('is_public'))
def test_private_flavor_handle_create(self):
self.create_flavor()
self.create_flavor(is_public=False)
value = mock.MagicMock()
flavor_id = '927202df-1afb-497f-8368-9c2d2f26e5db'
value.id = flavor_id
value.is_public = False
self.my_flavor.IS_PUBLIC = False
self.flavors.create.return_value = value
self.flavors.get.return_value = value
self.my_flavor.handle_create()
value.set_keys.assert_called_once_with({"foo": "bar"})
self.assertEqual(flavor_id, self.my_flavor.resource_id)
self.assertFalse(self.my_flavor.FnGetAtt('is_public'))
client_test = self.my_flavor.client().flavor_access.add_tenant_access
test_tenants = [mock.call(value, 'foo'), mock.call(value, 'bar')]
self.assertEqual(test_tenants, client_test.call_args_list)
def test_flavor_handle_update_keys(self):
self.create_flavor()
@ -133,6 +142,47 @@ class NovaFlavorTest(common.HeatTestCase):
value.unset_keys.assert_called_once_with({})
value.set_keys.assert_called_once_with(new_keys)
def test_flavor_handle_update_add_tenants(self):
self.create_flavor()
value = mock.MagicMock()
new_tenants = ["new_foo", "new_bar"]
prop_diff = {'tenants': new_tenants}
self.my_flavor.IS_PUBLIC = False
self.flavors.get.return_value = value
self.my_flavor.handle_update(json_snippet=None,
tmpl_diff=None, prop_diff=prop_diff)
test_tenants_add = [mock.call(value, 'new_foo'),
mock.call(value, 'new_bar')]
test_add = self.my_flavor.client().flavor_access.add_tenant_access
self.assertItemsEqual(test_tenants_add,
test_add.call_args_list)
def test_flavor_handle_update_remove_tenants(self):
self.create_flavor(is_public=False)
value = mock.MagicMock()
new_tenants = []
prop_diff = {'tenants': new_tenants}
self.my_flavor.IS_PUBLIC = False
self.flavors.get.return_value = value
itemFoo = mock.MagicMock()
itemFoo.tenant_id = 'foo'
itemBar = mock.MagicMock()
itemBar.tenant_id = 'bar'
self.my_flavor.client().flavor_access.list.return_value = [itemFoo,
itemBar]
self.my_flavor.handle_update(json_snippet=None,
tmpl_diff=None, prop_diff=prop_diff)
test_tenants_remove = [mock.call(value, 'foo'),
mock.call(value, 'bar')]
test_rem = self.my_flavor.client().flavor_access.remove_tenant_access
self.assertItemsEqual(test_tenants_remove,
test_rem.call_args_list)
def test_flavor_show_resource(self):
self.create_flavor()
self.my_flavor.resource_id = 'flavor_test_id'