d3be869160
The /345_require_online_migration_completion cell database migration added in Ocata prevents anyone from upgrading the cell database schema until they have performed the online flavor data migrations via the nova-manage db online_data_migrations command. This means we can remove the compatibility code in the Flavor object which performs a lookup routine in the API database and if not found it falls back to the cell database, which at this point should be empty, therefore making that dead code now. We can't yet remove the DEPRECATED_FIELDS because that would impact the object version, so this adds a TODO to remove those when we create a version 2.0 of this object. Change-Id: I4107af592448e20fe929f8fa3112929e3685b581
259 lines
9.0 KiB
Python
259 lines
9.0 KiB
Python
# Copyright 2015 Hewlett-Packard Development Company, L.P.
|
|
#
|
|
# All Rights Reserved.
|
|
#
|
|
# 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 testscenarios
|
|
|
|
from nova import context
|
|
from nova import exception as ex
|
|
from nova import objects
|
|
from nova import test
|
|
from nova.tests import fixtures as nova_fixtures
|
|
from nova.tests.functional import integrated_helpers as helper
|
|
from nova.tests.unit import policy_fixture
|
|
|
|
|
|
def rand_flavor(**kwargs):
|
|
flav = {
|
|
'name': 'name-%s' % helper.generate_random_alphanumeric(10),
|
|
'id': helper.generate_random_alphanumeric(10),
|
|
'ram': int(helper.generate_random_numeric(2)) + 1,
|
|
'disk': int(helper.generate_random_numeric(3)),
|
|
'vcpus': int(helper.generate_random_numeric(1)) + 1,
|
|
}
|
|
flav.update(kwargs)
|
|
return flav
|
|
|
|
|
|
class FlavorManageFullstack(testscenarios.WithScenarios, test.TestCase):
|
|
"""Tests for flavors manage administrative command.
|
|
|
|
Extension: os-flavors-manage
|
|
|
|
os-flavors-manage adds a set of admin functions to the flavors
|
|
resource for the creation and deletion of flavors.
|
|
|
|
POST /v2/flavors:
|
|
|
|
::
|
|
|
|
{
|
|
'name': NAME, # string, required unique
|
|
'id': ID, # string, required unique
|
|
'ram': RAM, # in MB, required
|
|
'vcpus': VCPUS, # int value, required
|
|
'disk': DISK, # in GB, required
|
|
'OS-FLV-EXT-DATA:ephemeral', # in GB, ephemeral disk size
|
|
'is_public': IS_PUBLIC, # boolean
|
|
'swap': SWAP, # in GB?
|
|
'rxtx_factor': RXTX, # ???
|
|
}
|
|
|
|
Returns Flavor
|
|
|
|
DELETE /v2/flavors/ID
|
|
|
|
|
|
Functional Test Scope:
|
|
|
|
This test starts the wsgi stack for the nova api services, uses an
|
|
in memory database to ensure the path through the wsgi layer to
|
|
the database.
|
|
|
|
"""
|
|
|
|
_additional_fixtures = []
|
|
|
|
scenarios = [
|
|
# test v2.1 base microversion
|
|
('v2_1', {
|
|
'api_major_version': 'v2.1'}),
|
|
]
|
|
|
|
def setUp(self):
|
|
super(FlavorManageFullstack, self).setUp()
|
|
|
|
# load any additional fixtures specified by the scenario
|
|
for fix in self._additional_fixtures:
|
|
self.useFixture(fix())
|
|
|
|
self.useFixture(policy_fixture.RealPolicyFixture())
|
|
api_fixture = self.useFixture(
|
|
nova_fixtures.OSAPIFixture(
|
|
api_version=self.api_major_version))
|
|
|
|
# NOTE(sdague): because this test is primarily an admin API
|
|
# test default self.api to the admin api.
|
|
self.api = api_fixture.admin_api
|
|
self.user_api = api_fixture.api
|
|
|
|
def assertFlavorDbEqual(self, flav, flavdb):
|
|
# a mapping of the REST params to the db fields
|
|
mapping = {
|
|
'name': 'name',
|
|
'disk': 'root_gb',
|
|
'ram': 'memory_mb',
|
|
'vcpus': 'vcpus',
|
|
'id': 'flavorid',
|
|
'swap': 'swap'
|
|
}
|
|
for k, v in mapping.items():
|
|
if k in flav:
|
|
self.assertEqual(flav[k], flavdb[v],
|
|
"%s != %s" % (flav, flavdb))
|
|
|
|
def assertFlavorAPIEqual(self, flav, flavapi):
|
|
# for all keys in the flavor, ensure they are correctly set in
|
|
# flavapi response.
|
|
for k in flav:
|
|
if k in flavapi:
|
|
self.assertEqual(flav[k], flavapi[k],
|
|
"%s != %s" % (flav, flavapi))
|
|
else:
|
|
self.fail("Missing key: %s in flavor: %s" % (k, flavapi))
|
|
|
|
def assertFlavorInList(self, flav, flavlist):
|
|
for item in flavlist['flavors']:
|
|
if flav['id'] == item['id']:
|
|
self.assertEqual(flav['name'], item['name'])
|
|
return
|
|
self.fail("%s not found in %s" % (flav, flavlist))
|
|
|
|
def assertFlavorNotInList(self, flav, flavlist):
|
|
for item in flavlist['flavors']:
|
|
if flav['id'] == item['id']:
|
|
self.fail("%s found in %s" % (flav, flavlist))
|
|
|
|
def test_flavor_manage_func_negative(self):
|
|
"""Test flavor manage edge conditions.
|
|
|
|
- Bogus body is a 400
|
|
- Unknown flavor is a 404
|
|
- Deleting unknown flavor is a 404
|
|
"""
|
|
# Test for various API failure conditions
|
|
# bad body is 400
|
|
resp = self.api.api_post('flavors', '', check_response_status=False)
|
|
self.assertEqual(400, resp.status)
|
|
|
|
# get unknown flavor is 404
|
|
resp = self.api.api_delete('flavors/foo', check_response_status=False)
|
|
self.assertEqual(404, resp.status)
|
|
|
|
# delete unknown flavor is 404
|
|
resp = self.api.api_delete('flavors/foo', check_response_status=False)
|
|
self.assertEqual(404, resp.status)
|
|
|
|
ctx = context.get_admin_context()
|
|
# bounds conditions - invalid vcpus
|
|
flav = {'flavor': rand_flavor(vcpus=0)}
|
|
resp = self.api.api_post('flavors', flav, check_response_status=False)
|
|
self.assertEqual(400, resp.status, resp)
|
|
# ... and ensure that we didn't leak it into the db
|
|
self.assertRaises(ex.FlavorNotFound,
|
|
objects.Flavor.get_by_flavor_id,
|
|
ctx, flav['flavor']['id'])
|
|
|
|
# bounds conditions - invalid ram
|
|
flav = {'flavor': rand_flavor(ram=0)}
|
|
|
|
resp = self.api.api_post('flavors', flav, check_response_status=False)
|
|
self.assertEqual(400, resp.status)
|
|
# ... and ensure that we didn't leak it into the db
|
|
self.assertRaises(ex.FlavorNotFound,
|
|
objects.Flavor.get_by_flavor_id,
|
|
ctx, flav['flavor']['id'])
|
|
|
|
# NOTE(sdague): if there are other bounds conditions that
|
|
# should be checked, stack them up here.
|
|
|
|
def test_flavor_manage_deleted(self):
|
|
"""Ensure the behavior around a deleted flavor is stable.
|
|
|
|
- Fetching a deleted flavor works, and returns the flavor info.
|
|
- Listings should not contain deleted flavors
|
|
|
|
"""
|
|
# create a deleted flavor
|
|
new_flav = {'flavor': rand_flavor()}
|
|
self.api.api_post('flavors', new_flav)
|
|
self.api.api_delete('flavors/%s' % new_flav['flavor']['id'])
|
|
|
|
# deleted flavor should not show up in a list
|
|
resp = self.api.api_get('flavors')
|
|
self.assertFlavorNotInList(new_flav['flavor'], resp.body)
|
|
|
|
def test_flavor_manage_func(self):
|
|
"""Basic flavor creation lifecycle testing.
|
|
|
|
- Creating a flavor
|
|
- Ensure it's in the database
|
|
- Ensure it's in the listing
|
|
- Delete it
|
|
- Ensure it's hidden in the database
|
|
"""
|
|
|
|
ctx = context.get_admin_context()
|
|
flav1 = {
|
|
'flavor': rand_flavor(),
|
|
}
|
|
|
|
# Create flavor and ensure it made it to the database
|
|
self.api.api_post('flavors', flav1)
|
|
|
|
flav1db = objects.Flavor.get_by_flavor_id(ctx, flav1['flavor']['id'])
|
|
self.assertFlavorDbEqual(flav1['flavor'], flav1db)
|
|
|
|
# Ensure new flavor is seen in the listing
|
|
resp = self.api.api_get('flavors')
|
|
self.assertFlavorInList(flav1['flavor'], resp.body)
|
|
|
|
# Delete flavor and ensure it was removed from the database
|
|
self.api.api_delete('flavors/%s' % flav1['flavor']['id'])
|
|
self.assertRaises(ex.FlavorNotFound,
|
|
objects.Flavor.get_by_flavor_id,
|
|
ctx, flav1['flavor']['id'])
|
|
|
|
resp = self.api.api_delete('flavors/%s' % flav1['flavor']['id'],
|
|
check_response_status=False)
|
|
self.assertEqual(404, resp.status)
|
|
|
|
def test_flavor_manage_permissions(self):
|
|
"""Ensure that regular users can't create or delete flavors.
|
|
|
|
"""
|
|
ctx = context.get_admin_context()
|
|
flav1 = {'flavor': rand_flavor()}
|
|
|
|
# Ensure user can't create flavor
|
|
resp = self.user_api.api_post('flavors', flav1,
|
|
check_response_status=False)
|
|
self.assertEqual(403, resp.status)
|
|
# ... and that it didn't leak through
|
|
self.assertRaises(ex.FlavorNotFound,
|
|
objects.Flavor.get_by_flavor_id,
|
|
ctx, flav1['flavor']['id'])
|
|
|
|
# Create the flavor as the admin user
|
|
self.api.api_post('flavors', flav1)
|
|
|
|
# Ensure user can't delete flavors from our cloud
|
|
resp = self.user_api.api_delete('flavors/%s' % flav1['flavor']['id'],
|
|
check_response_status=False)
|
|
self.assertEqual(403, resp.status)
|
|
# ... and ensure that we didn't actually delete the flavor,
|
|
# this will throw an exception if we did.
|
|
objects.Flavor.get_by_flavor_id(ctx, flav1['flavor']['id'])
|