Add flavor, flavor_profile table and their APIs
This patch adds flavor and flavor_profile tables. It also implements flavors and flavorprofiles apis. Partially-Implements: Blueprint octavia-lbaas-flavors Co-Authored-By: Michael Johnson <johnsomor@gmail.com> Change-Id: I99a673438458757d0acdaa46dd8ee041edb3be9c
This commit is contained in:
parent
8b4a01c5bb
commit
637009ecd0
@ -231,14 +231,14 @@ class NoopManager(object):
|
||||
LOG.debug('Provider %s no-op, get_supported_flavor_metadata',
|
||||
self.__class__.__name__)
|
||||
|
||||
return {'amp_image_tag': 'The glance image tag to use for this load '
|
||||
'balancer.'}
|
||||
return {"amp_image_tag": "The glance image tag to use for this load "
|
||||
"balancer."}
|
||||
|
||||
def validate_flavor(self, flavor_metadata):
|
||||
LOG.debug('Provider %s no-op, validate_flavor metadata: %s',
|
||||
self.__class__.__name__, flavor_metadata)
|
||||
|
||||
flavor_hash = hash(frozenset(flavor_metadata.items()))
|
||||
flavor_hash = hash(frozenset(flavor_metadata))
|
||||
self.driverconfig[flavor_hash] = (flavor_metadata, 'validate_flavor')
|
||||
|
||||
|
||||
|
@ -79,9 +79,13 @@ class RootController(rest.RestController):
|
||||
'2018-07-31T00:00:00Z', host_url)
|
||||
self._add_a_version(versions, 'v2.3', 'v2', 'SUPPORTED',
|
||||
'2018-12-18T00:00:00Z', host_url)
|
||||
self._add_a_version(versions, 'v2.4', 'v2', 'CURRENT',
|
||||
# amp statistics
|
||||
self._add_a_version(versions, 'v2.4', 'v2', 'SUPPORTED',
|
||||
'2018-12-19T00:00:00Z', host_url)
|
||||
# Tags
|
||||
self._add_a_version(versions, 'v2.5', 'v2', 'CURRENT',
|
||||
self._add_a_version(versions, 'v2.5', 'v2', 'SUPPORTED',
|
||||
'2019-01-21T00:00:00Z', host_url)
|
||||
# Flavors
|
||||
self._add_a_version(versions, 'v2.6', 'v2', 'CURRENT',
|
||||
'2019-01-25T00:00:00Z', host_url)
|
||||
return {'versions': versions}
|
||||
|
@ -17,6 +17,8 @@ from wsmeext import pecan as wsme_pecan
|
||||
|
||||
from octavia.api.v2.controllers import amphora
|
||||
from octavia.api.v2.controllers import base
|
||||
from octavia.api.v2.controllers import flavor_profiles
|
||||
from octavia.api.v2.controllers import flavors
|
||||
from octavia.api.v2.controllers import health_monitor
|
||||
from octavia.api.v2.controllers import l7policy
|
||||
from octavia.api.v2.controllers import listener
|
||||
@ -43,6 +45,8 @@ class BaseV2Controller(base.BaseController):
|
||||
self.healthmonitors = health_monitor.HealthMonitorController()
|
||||
self.quotas = quotas.QuotasController()
|
||||
self.providers = provider.ProviderController()
|
||||
self.flavors = flavors.FlavorsController()
|
||||
self.flavorprofiles = flavor_profiles.FlavorProfileController()
|
||||
|
||||
@wsme_pecan.wsexpose(wtypes.text)
|
||||
def get(self):
|
||||
|
@ -99,6 +99,16 @@ class BaseController(rest.RestController):
|
||||
data_models.HealthMonitor, id,
|
||||
show_deleted=show_deleted)
|
||||
|
||||
def _get_db_flavor(self, session, id):
|
||||
"""Get a flavor from the database."""
|
||||
return self._get_db_obj(session, self.repositories.flavor,
|
||||
data_models.Flavor, id)
|
||||
|
||||
def _get_db_flavor_profile(self, session, id):
|
||||
"""Get a flavor profile from the database."""
|
||||
return self._get_db_obj(session, self.repositories.flavor_profile,
|
||||
data_models.FlavorProfile, id)
|
||||
|
||||
def _get_db_l7policy(self, session, id, show_deleted=True):
|
||||
"""Get a L7 Policy from the database."""
|
||||
return self._get_db_obj(session, self.repositories.l7policy,
|
||||
|
191
octavia/api/v2/controllers/flavor_profiles.py
Normal file
191
octavia/api/v2/controllers/flavor_profiles.py
Normal file
@ -0,0 +1,191 @@
|
||||
# Copyright 2014 Rackspace
|
||||
# Copyright 2016 Blue Box, an IBM Company
|
||||
#
|
||||
# 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 oslo_db import exception as odb_exceptions
|
||||
from oslo_log import log as logging
|
||||
from oslo_serialization import jsonutils
|
||||
from oslo_utils import excutils
|
||||
from oslo_utils import uuidutils
|
||||
import pecan
|
||||
from sqlalchemy.orm import exc as sa_exception
|
||||
from wsme import types as wtypes
|
||||
from wsmeext import pecan as wsme_pecan
|
||||
|
||||
from octavia.api.drivers import driver_factory
|
||||
from octavia.api.drivers import utils as driver_utils
|
||||
from octavia.api.v2.controllers import base
|
||||
from octavia.api.v2.types import flavor_profile as profile_types
|
||||
from octavia.common import constants
|
||||
from octavia.common import exceptions
|
||||
from octavia.db import api as db_api
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FlavorProfileController(base.BaseController):
|
||||
RBAC_TYPE = constants.RBAC_FLAVOR_PROFILE
|
||||
|
||||
def __init__(self):
|
||||
super(FlavorProfileController, self).__init__()
|
||||
|
||||
@wsme_pecan.wsexpose(profile_types.FlavorProfileRootResponse, wtypes.text,
|
||||
[wtypes.text], ignore_extra_args=True)
|
||||
def get_one(self, id, fields=None):
|
||||
"""Gets a flavor profile's detail."""
|
||||
context = pecan.request.context.get('octavia_context')
|
||||
self._auth_validate_action(context, context.project_id,
|
||||
constants.RBAC_GET_ONE)
|
||||
db_flavor_profile = self._get_db_flavor_profile(context.session, id)
|
||||
result = self._convert_db_to_type(db_flavor_profile,
|
||||
profile_types.FlavorProfileResponse)
|
||||
if fields is not None:
|
||||
result = self._filter_fields([result], fields)[0]
|
||||
return profile_types.FlavorProfileRootResponse(flavorprofile=result)
|
||||
|
||||
@wsme_pecan.wsexpose(profile_types.FlavorProfilesRootResponse,
|
||||
[wtypes.text], ignore_extra_args=True)
|
||||
def get_all(self, fields=None):
|
||||
"""Lists all flavor profiles."""
|
||||
pcontext = pecan.request.context
|
||||
context = pcontext.get('octavia_context')
|
||||
self._auth_validate_action(context, context.project_id,
|
||||
constants.RBAC_GET_ALL)
|
||||
db_flavor_profiles, links = self.repositories.flavor_profile.get_all(
|
||||
context.session,
|
||||
pagination_helper=pcontext.get(constants.PAGINATION_HELPER))
|
||||
result = self._convert_db_to_type(
|
||||
db_flavor_profiles, [profile_types.FlavorProfileResponse])
|
||||
if fields is not None:
|
||||
result = self._filter_fields(result, fields)
|
||||
return profile_types.FlavorProfilesRootResponse(
|
||||
flavorprofiles=result, flavorprofile_links=links)
|
||||
|
||||
@wsme_pecan.wsexpose(profile_types.FlavorProfileRootResponse,
|
||||
body=profile_types.FlavorProfileRootPOST,
|
||||
status_code=201)
|
||||
def post(self, flavor_profile_):
|
||||
"""Creates a flavor Profile."""
|
||||
flavorprofile = flavor_profile_.flavorprofile
|
||||
context = pecan.request.context.get('octavia_context')
|
||||
self._auth_validate_action(context, context.project_id,
|
||||
constants.RBAC_POST)
|
||||
# Do a basic JSON validation on the metadata
|
||||
try:
|
||||
flavor_data_dict = jsonutils.loads(flavorprofile.flavor_data)
|
||||
except Exception:
|
||||
raise exceptions.InvalidOption(
|
||||
value=flavorprofile.flavor_data,
|
||||
option=constants.FLAVOR_DATA)
|
||||
|
||||
# Validate that the provider driver supports the metadata
|
||||
driver = driver_factory.get_driver(flavorprofile.provider_name)
|
||||
driver_utils.call_provider(driver.name, driver.validate_flavor,
|
||||
flavor_data_dict)
|
||||
|
||||
lock_session = db_api.get_session(autocommit=False)
|
||||
try:
|
||||
flavorprofile_dict = flavorprofile.to_dict(render_unsets=True)
|
||||
flavorprofile_dict['id'] = uuidutils.generate_uuid()
|
||||
db_flavor_profile = self.repositories.flavor_profile.create(
|
||||
lock_session, **flavorprofile_dict)
|
||||
lock_session.commit()
|
||||
except odb_exceptions.DBDuplicateEntry:
|
||||
lock_session.rollback()
|
||||
raise exceptions.IDAlreadyExists()
|
||||
except Exception:
|
||||
with excutils.save_and_reraise_exception():
|
||||
lock_session.rollback()
|
||||
result = self._convert_db_to_type(
|
||||
db_flavor_profile, profile_types.FlavorProfileResponse)
|
||||
return profile_types.FlavorProfileRootResponse(flavorprofile=result)
|
||||
|
||||
@wsme_pecan.wsexpose(profile_types.FlavorProfileRootResponse,
|
||||
wtypes.text, status_code=200,
|
||||
body=profile_types.FlavorProfileRootPUT)
|
||||
def put(self, id, flavor_profile_):
|
||||
"""Updates a flavor Profile."""
|
||||
flavorprofile = flavor_profile_.flavorprofile
|
||||
context = pecan.request.context.get('octavia_context')
|
||||
self._auth_validate_action(context, context.project_id,
|
||||
constants.RBAC_PUT)
|
||||
|
||||
# Don't allow changes to the flavor_data or provider_name if it
|
||||
# is in use.
|
||||
if (not isinstance(flavorprofile.flavor_data, wtypes.UnsetType) or
|
||||
not isinstance(flavorprofile.provider_name, wtypes.UnsetType)):
|
||||
if self.repositories.flavor.count(context.session,
|
||||
flavor_profile_id=id) > 0:
|
||||
raise exceptions.ObjectInUse(object='Flavor profile', id=id)
|
||||
|
||||
if not isinstance(flavorprofile.flavor_data, wtypes.UnsetType):
|
||||
# Do a basic JSON validation on the metadata
|
||||
try:
|
||||
flavor_data_dict = jsonutils.loads(flavorprofile.flavor_data)
|
||||
except Exception:
|
||||
raise exceptions.InvalidOption(
|
||||
value=flavorprofile.flavor_data,
|
||||
option=constants.FLAVOR_DATA)
|
||||
|
||||
if isinstance(flavorprofile.provider_name, wtypes.UnsetType):
|
||||
db_flavor_profile = self._get_db_flavor_profile(
|
||||
context.session, id)
|
||||
provider_driver = db_flavor_profile.provider_name
|
||||
else:
|
||||
provider_driver = flavorprofile.provider_name
|
||||
|
||||
# Validate that the provider driver supports the metadata
|
||||
driver = driver_factory.get_driver(provider_driver)
|
||||
driver_utils.call_provider(driver.name, driver.validate_flavor,
|
||||
flavor_data_dict)
|
||||
|
||||
lock_session = db_api.get_session(autocommit=False)
|
||||
try:
|
||||
flavorprofile_dict = flavorprofile.to_dict(render_unsets=False)
|
||||
if flavorprofile_dict:
|
||||
db_flavor_profile = self.repositories.flavor_profile.update(
|
||||
lock_session, id, **flavorprofile_dict)
|
||||
lock_session.commit()
|
||||
except Exception:
|
||||
with excutils.save_and_reraise_exception():
|
||||
lock_session.rollback()
|
||||
|
||||
# Force SQL alchemy to query the DB, otherwise we get inconsistent
|
||||
# results
|
||||
context.session.expire_all()
|
||||
db_flavor_profile = self._get_db_flavor_profile(context.session, id)
|
||||
result = self._convert_db_to_type(
|
||||
db_flavor_profile, profile_types.FlavorProfileResponse)
|
||||
return profile_types.FlavorProfileRootResponse(flavorprofile=result)
|
||||
|
||||
@wsme_pecan.wsexpose(None, wtypes.text, status_code=204)
|
||||
def delete(self, flavor_profile_id):
|
||||
"""Deletes a Flavor Profile"""
|
||||
context = pecan.request.context.get('octavia_context')
|
||||
|
||||
self._auth_validate_action(context, context.project_id,
|
||||
constants.RBAC_DELETE)
|
||||
|
||||
# Don't allow it to be deleted if it is in use by a flavor
|
||||
if self.repositories.flavor.count(
|
||||
context.session, flavor_profile_id=flavor_profile_id) > 0:
|
||||
raise exceptions.ObjectInUse(object='Flavor profile',
|
||||
id=flavor_profile_id)
|
||||
|
||||
try:
|
||||
self.repositories.flavor_profile.delete(context.session,
|
||||
id=flavor_profile_id)
|
||||
except sa_exception.NoResultFound:
|
||||
raise exceptions.NotFound(resource='Flavor profile',
|
||||
id=flavor_profile_id)
|
144
octavia/api/v2/controllers/flavors.py
Normal file
144
octavia/api/v2/controllers/flavors.py
Normal file
@ -0,0 +1,144 @@
|
||||
# Copyright 2014 Rackspace
|
||||
# Copyright 2016 Blue Box, an IBM Company
|
||||
#
|
||||
# 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 oslo_db import exception as odb_exceptions
|
||||
from oslo_log import log as logging
|
||||
from oslo_utils import excutils
|
||||
from oslo_utils import uuidutils
|
||||
import pecan
|
||||
from sqlalchemy.orm import exc as sa_exception
|
||||
from wsme import types as wtypes
|
||||
from wsmeext import pecan as wsme_pecan
|
||||
|
||||
from octavia.api.v2.controllers import base
|
||||
from octavia.api.v2.types import flavors as flavor_types
|
||||
from octavia.common import constants
|
||||
from octavia.common import exceptions
|
||||
from octavia.db import api as db_api
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FlavorsController(base.BaseController):
|
||||
RBAC_TYPE = constants.RBAC_FLAVOR
|
||||
|
||||
def __init__(self):
|
||||
super(FlavorsController, self).__init__()
|
||||
|
||||
@wsme_pecan.wsexpose(flavor_types.FlavorRootResponse, wtypes.text,
|
||||
[wtypes.text], ignore_extra_args=True)
|
||||
def get_one(self, id, fields=None):
|
||||
"""Gets a flavor's detail."""
|
||||
context = pecan.request.context.get('octavia_context')
|
||||
self._auth_validate_action(context, context.project_id,
|
||||
constants.RBAC_GET_ONE)
|
||||
|
||||
db_flavor = self._get_db_flavor(context.session, id)
|
||||
result = self._convert_db_to_type(db_flavor,
|
||||
flavor_types.FlavorResponse)
|
||||
if fields is not None:
|
||||
result = self._filter_fields([result], fields)[0]
|
||||
return flavor_types.FlavorRootResponse(flavor=result)
|
||||
|
||||
@wsme_pecan.wsexpose(flavor_types.FlavorsRootResponse,
|
||||
[wtypes.text], ignore_extra_args=True)
|
||||
def get_all(self, fields=None):
|
||||
"""Lists all flavors."""
|
||||
pcontext = pecan.request.context
|
||||
context = pcontext.get('octavia_context')
|
||||
self._auth_validate_action(context, context.project_id,
|
||||
constants.RBAC_GET_ALL)
|
||||
db_flavors, links = self.repositories.flavor.get_all(
|
||||
context.session,
|
||||
pagination_helper=pcontext.get(constants.PAGINATION_HELPER))
|
||||
result = self._convert_db_to_type(
|
||||
db_flavors, [flavor_types.FlavorResponse])
|
||||
if fields is not None:
|
||||
result = self._filter_fields(result, fields)
|
||||
return flavor_types.FlavorsRootResponse(
|
||||
flavors=result, flavors_links=links)
|
||||
|
||||
@wsme_pecan.wsexpose(flavor_types.FlavorRootResponse,
|
||||
body=flavor_types.FlavorRootPOST, status_code=201)
|
||||
def post(self, flavor_):
|
||||
"""Creates a flavor."""
|
||||
flavor = flavor_.flavor
|
||||
context = pecan.request.context.get('octavia_context')
|
||||
self._auth_validate_action(context, context.project_id,
|
||||
constants.RBAC_POST)
|
||||
|
||||
# TODO(johnsom) Validate the flavor profile ID
|
||||
|
||||
lock_session = db_api.get_session(autocommit=False)
|
||||
try:
|
||||
flavor_dict = flavor.to_dict(render_unsets=True)
|
||||
flavor_dict['id'] = uuidutils.generate_uuid()
|
||||
db_flavor = self.repositories.flavor.create(lock_session,
|
||||
**flavor_dict)
|
||||
lock_session.commit()
|
||||
except odb_exceptions.DBDuplicateEntry:
|
||||
lock_session.rollback()
|
||||
raise exceptions.RecordAlreadyExists(field='flavor',
|
||||
name=flavor.name)
|
||||
except Exception:
|
||||
with excutils.save_and_reraise_exception():
|
||||
lock_session.rollback()
|
||||
result = self._convert_db_to_type(db_flavor,
|
||||
flavor_types.FlavorResponse)
|
||||
return flavor_types.FlavorRootResponse(flavor=result)
|
||||
|
||||
@wsme_pecan.wsexpose(flavor_types.FlavorRootResponse,
|
||||
wtypes.text, status_code=200,
|
||||
body=flavor_types.FlavorRootPUT)
|
||||
def put(self, id, flavor_):
|
||||
flavor = flavor_.flavor
|
||||
context = pecan.request.context.get('octavia_context')
|
||||
self._auth_validate_action(context, context.project_id,
|
||||
constants.RBAC_PUT)
|
||||
lock_session = db_api.get_session(autocommit=False)
|
||||
try:
|
||||
flavor_dict = flavor.to_dict(render_unsets=False)
|
||||
if flavor_dict:
|
||||
db_flavor = self.repositories.flavor.update(lock_session, id,
|
||||
**flavor_dict)
|
||||
lock_session.commit()
|
||||
except Exception:
|
||||
with excutils.save_and_reraise_exception():
|
||||
lock_session.rollback()
|
||||
|
||||
# Force SQL alchemy to query the DB, otherwise we get inconsistent
|
||||
# results
|
||||
context.session.expire_all()
|
||||
db_flavor = self._get_db_flavor(context.session, id)
|
||||
result = self._convert_db_to_type(db_flavor,
|
||||
flavor_types.FlavorResponse)
|
||||
return flavor_types.FlavorRootResponse(flavor=result)
|
||||
|
||||
@wsme_pecan.wsexpose(None, wtypes.text, status_code=204)
|
||||
def delete(self, flavor_id):
|
||||
"""Deletes a Flavor"""
|
||||
context = pecan.request.context.get('octavia_context')
|
||||
|
||||
self._auth_validate_action(context, context.project_id,
|
||||
constants.RBAC_DELETE)
|
||||
|
||||
try:
|
||||
self.repositories.flavor.delete(context.session, id=flavor_id)
|
||||
# Handle when load balancers still reference this flavor
|
||||
except odb_exceptions.DBReferenceError:
|
||||
raise exceptions.ObjectInUse(object='Flavor', id=flavor_id)
|
||||
except sa_exception.NoResultFound:
|
||||
raise exceptions.NotFound(resource='Flavor',
|
||||
id=flavor_id)
|
69
octavia/api/v2/types/flavor_profile.py
Normal file
69
octavia/api/v2/types/flavor_profile.py
Normal file
@ -0,0 +1,69 @@
|
||||
# Copyright 2014 Rackspace
|
||||
#
|
||||
# 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 wsme import types as wtypes
|
||||
|
||||
from octavia.api.common import types
|
||||
|
||||
|
||||
class BaseFlavorProfileType(types.BaseType):
|
||||
_type_to_model_map = {}
|
||||
_child_map = {}
|
||||
|
||||
|
||||
class FlavorProfileResponse(BaseFlavorProfileType):
|
||||
"""Defines which attributes are to be shown on any response."""
|
||||
id = wtypes.wsattr(wtypes.UuidType())
|
||||
name = wtypes.wsattr(wtypes.StringType())
|
||||
provider_name = wtypes.wsattr(wtypes.StringType())
|
||||
flavor_data = wtypes.wsattr(wtypes.StringType())
|
||||
|
||||
@classmethod
|
||||
def from_data_model(cls, data_model, children=False):
|
||||
flavorprofile = super(FlavorProfileResponse, cls).from_data_model(
|
||||
data_model, children=children)
|
||||
return flavorprofile
|
||||
|
||||
|
||||
class FlavorProfileRootResponse(types.BaseType):
|
||||
flavorprofile = wtypes.wsattr(FlavorProfileResponse)
|
||||
|
||||
|
||||
class FlavorProfilesRootResponse(types.BaseType):
|
||||
flavorprofiles = wtypes.wsattr([FlavorProfileResponse])
|
||||
flavorprofile_links = wtypes.wsattr([types.PageType])
|
||||
|
||||
|
||||
class FlavorProfilePOST(BaseFlavorProfileType):
|
||||
"""Defines mandatory and optional attributes of a POST request."""
|
||||
name = wtypes.wsattr(wtypes.StringType(max_length=255), mandatory=True)
|
||||
provider_name = wtypes.wsattr(wtypes.StringType(max_length=255),
|
||||
mandatory=True)
|
||||
flavor_data = wtypes.wsattr(wtypes.StringType(max_length=4096),
|
||||
mandatory=True)
|
||||
|
||||
|
||||
class FlavorProfileRootPOST(types.BaseType):
|
||||
flavorprofile = wtypes.wsattr(FlavorProfilePOST)
|
||||
|
||||
|
||||
class FlavorProfilePUT(BaseFlavorProfileType):
|
||||
"""Defines the attributes of a PUT request."""
|
||||
name = wtypes.wsattr(wtypes.StringType(max_length=255))
|
||||
provider_name = wtypes.wsattr(wtypes.StringType(max_length=255))
|
||||
flavor_data = wtypes.wsattr(wtypes.StringType(max_length=4096))
|
||||
|
||||
|
||||
class FlavorProfileRootPUT(types.BaseType):
|
||||
flavorprofile = wtypes.wsattr(FlavorProfilePUT)
|
69
octavia/api/v2/types/flavors.py
Normal file
69
octavia/api/v2/types/flavors.py
Normal file
@ -0,0 +1,69 @@
|
||||
# Copyright 2014 Rackspace
|
||||
#
|
||||
# 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 wsme import types as wtypes
|
||||
|
||||
from octavia.api.common import types
|
||||
|
||||
|
||||
class BaseFlavorType(types.BaseType):
|
||||
_type_to_model_map = {}
|
||||
_child_map = {}
|
||||
|
||||
|
||||
class FlavorResponse(BaseFlavorType):
|
||||
"""Defines which attributes are to be shown on any response."""
|
||||
id = wtypes.wsattr(wtypes.UuidType())
|
||||
name = wtypes.wsattr(wtypes.StringType())
|
||||
description = wtypes.wsattr(wtypes.StringType())
|
||||
enabled = wtypes.wsattr(bool)
|
||||
flavor_profile_id = wtypes.wsattr(wtypes.StringType())
|
||||
|
||||
@classmethod
|
||||
def from_data_model(cls, data_model, children=False):
|
||||
flavor = super(FlavorResponse, cls).from_data_model(
|
||||
data_model, children=children)
|
||||
return flavor
|
||||
|
||||
|
||||
class FlavorRootResponse(types.BaseType):
|
||||
flavor = wtypes.wsattr(FlavorResponse)
|
||||
|
||||
|
||||
class FlavorsRootResponse(types.BaseType):
|
||||
flavors = wtypes.wsattr([FlavorResponse])
|
||||
flavors_links = wtypes.wsattr([types.PageType])
|
||||
|
||||
|
||||
class FlavorPOST(BaseFlavorType):
|
||||
"""Defines mandatory and optional attributes of a POST request."""
|
||||
name = wtypes.wsattr(wtypes.StringType(max_length=255), mandatory=True)
|
||||
description = wtypes.wsattr(wtypes.StringType(max_length=255))
|
||||
enabled = wtypes.wsattr(bool, default=True)
|
||||
flavor_profile_id = wtypes.wsattr(wtypes.UuidType(), mandatory=True)
|
||||
|
||||
|
||||
class FlavorRootPOST(types.BaseType):
|
||||
flavor = wtypes.wsattr(FlavorPOST)
|
||||
|
||||
|
||||
class FlavorPUT(BaseFlavorType):
|
||||
"""Defines the attributes of a PUT request."""
|
||||
name = wtypes.wsattr(wtypes.StringType(max_length=255))
|
||||
description = wtypes.wsattr(wtypes.StringType(max_length=255))
|
||||
enabled = wtypes.wsattr(bool)
|
||||
|
||||
|
||||
class FlavorRootPUT(types.BaseType):
|
||||
flavor = wtypes.wsattr(FlavorPUT)
|
@ -534,6 +534,8 @@ RBAC_L7RULE = '{}:l7rule:'.format(LOADBALANCER_API)
|
||||
RBAC_QUOTA = '{}:quota:'.format(LOADBALANCER_API)
|
||||
RBAC_AMPHORA = '{}:amphora:'.format(LOADBALANCER_API)
|
||||
RBAC_PROVIDER = '{}:provider:'.format(LOADBALANCER_API)
|
||||
RBAC_FLAVOR = '{}:flavor:'.format(LOADBALANCER_API)
|
||||
RBAC_FLAVOR_PROFILE = '{}:flavor-profile:'.format(LOADBALANCER_API)
|
||||
RBAC_POST = 'post'
|
||||
RBAC_PUT = 'put'
|
||||
RBAC_PUT_FAILOVER = 'put_failover'
|
||||
@ -562,3 +564,5 @@ AMP_NETNS_SVC_PREFIX = 'amphora-netns'
|
||||
|
||||
# Amphora Feature Compatibility
|
||||
HTTP_REUSE = 'has_http_reuse'
|
||||
|
||||
FLAVOR_DATA = 'flavor_data'
|
||||
|
@ -728,3 +728,25 @@ class Quotas(BaseDataModel):
|
||||
self.in_use_load_balancer = in_use_load_balancer
|
||||
self.in_use_member = in_use_member
|
||||
self.in_use_pool = in_use_pool
|
||||
|
||||
|
||||
class Flavor(BaseDataModel):
|
||||
|
||||
def __init__(self, id=None, name=None,
|
||||
description=None, enabled=None,
|
||||
flavor_profile_id=None):
|
||||
self.id = id
|
||||
self.name = name
|
||||
self.description = description
|
||||
self.enabled = enabled
|
||||
self.flavor_profile_id = flavor_profile_id
|
||||
|
||||
|
||||
class FlavorProfile(BaseDataModel):
|
||||
|
||||
def __init__(self, id=None, name=None, provider_name=None,
|
||||
flavor_data=None):
|
||||
self.id = id
|
||||
self.name = name
|
||||
self.provider_name = provider_name
|
||||
self.flavor_data = flavor_data
|
||||
|
@ -216,6 +216,11 @@ class IDAlreadyExists(APIException):
|
||||
code = 409
|
||||
|
||||
|
||||
class RecordAlreadyExists(APIException):
|
||||
msg = _('A %(field)s of %(name)s already exists.')
|
||||
code = 409
|
||||
|
||||
|
||||
class NoReadyAmphoraeException(OctaviaException):
|
||||
message = _('There are not any READY amphora available.')
|
||||
|
||||
@ -367,3 +372,8 @@ class ProviderUnsupportedOptionError(APIException):
|
||||
|
||||
class InputFileError(OctaviaException):
|
||||
message = _('Error with file %(file_name)s. Reason: %(reason)s')
|
||||
|
||||
|
||||
class ObjectInUse(APIException):
|
||||
msg = _("%(object)s %(id)s is in use and cannot be modified.")
|
||||
code = 409
|
||||
|
@ -31,7 +31,7 @@ class OctaviaBase(models.ModelBase):
|
||||
# objects.
|
||||
if obj.__class__.__name__ in ['Member', 'Pool', 'LoadBalancer',
|
||||
'Listener', 'Amphora', 'L7Policy',
|
||||
'L7Rule']:
|
||||
'L7Rule', 'Flavor', 'FlavorProfile']:
|
||||
return obj.__class__.__name__ + obj.id
|
||||
elif obj.__class__.__name__ in ['SessionPersistence', 'HealthMonitor']:
|
||||
return obj.__class__.__name__ + obj.pool_id
|
||||
|
@ -0,0 +1,53 @@
|
||||
# Copyright 2017 Walmart Stores Inc.
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""add flavor and flavor_profile table
|
||||
|
||||
Revision ID: b9c703669314
|
||||
Revises: 4f65b4f91c39
|
||||
Create Date: 2018-01-02 16:05:29.745457
|
||||
|
||||
"""
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'b9c703669314'
|
||||
down_revision = '4f65b4f91c39'
|
||||
|
||||
|
||||
def upgrade():
|
||||
|
||||
op.create_table(
|
||||
u'flavor_profile',
|
||||
sa.Column(u'id', sa.String(36), nullable=False),
|
||||
sa.Column(u'name', sa.String(255), nullable=False),
|
||||
sa.Column(u'provider_name', sa.String(255), nullable=False),
|
||||
sa.Column(u'flavor_data', sa.String(4096), nullable=False),
|
||||
sa.PrimaryKeyConstraint(u'id'))
|
||||
|
||||
op.create_table(
|
||||
u'flavor',
|
||||
sa.Column(u'id', sa.String(36), nullable=False),
|
||||
sa.Column(u'name', sa.String(255), nullable=False),
|
||||
sa.Column(u'description', sa.String(255), nullable=True),
|
||||
sa.Column(u'enabled', sa.Boolean(), nullable=False),
|
||||
sa.Column(u'flavor_profile_id', sa.String(36), nullable=False),
|
||||
sa.ForeignKeyConstraint([u'flavor_profile_id'],
|
||||
[u'flavor_profile.id'],
|
||||
name=u'fk_flavor_flavor_profile_id'),
|
||||
sa.PrimaryKeyConstraint(u'id'),
|
||||
sa.UniqueConstraint(u'name',
|
||||
name=u'uq_flavor_name'),)
|
@ -1,5 +1,6 @@
|
||||
# Copyright 2014 Rackspace
|
||||
# Copyright 2016 Blue Box, an IBM Company
|
||||
# Copyright 2017 Walmart Stores Inc.
|
||||
#
|
||||
# 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
|
||||
@ -13,6 +14,7 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
|
||||
from oslo_db.sqlalchemy import models
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.ext import orderinglist
|
||||
@ -21,6 +23,8 @@ from sqlalchemy.orm import validates
|
||||
from sqlalchemy.sql import func
|
||||
|
||||
from octavia.api.v2.types import amphora
|
||||
from octavia.api.v2.types import flavor_profile
|
||||
from octavia.api.v2.types import flavors
|
||||
from octavia.api.v2.types import health_monitor
|
||||
from octavia.api.v2.types import l7policy
|
||||
from octavia.api.v2.types import l7rule
|
||||
@ -717,3 +721,40 @@ class Quotas(base_models.BASE):
|
||||
in_use_load_balancer = sa.Column(sa.Integer(), nullable=True)
|
||||
in_use_member = sa.Column(sa.Integer(), nullable=True)
|
||||
in_use_pool = sa.Column(sa.Integer(), nullable=True)
|
||||
|
||||
|
||||
class FlavorProfile(base_models.BASE, base_models.IdMixin,
|
||||
base_models.NameMixin):
|
||||
|
||||
__data_model__ = data_models.FlavorProfile
|
||||
|
||||
__tablename__ = "flavor_profile"
|
||||
|
||||
__v2_wsme__ = flavor_profile.FlavorProfileResponse
|
||||
|
||||
provider_name = sa.Column(sa.String(255), nullable=False)
|
||||
flavor_data = sa.Column(sa.String(4096), nullable=False)
|
||||
|
||||
|
||||
class Flavor(base_models.BASE,
|
||||
base_models.IdMixin,
|
||||
base_models.NameMixin):
|
||||
|
||||
__data_model__ = data_models.Flavor
|
||||
|
||||
__tablename__ = "flavor"
|
||||
|
||||
__v2_wsme__ = flavors.FlavorResponse
|
||||
|
||||
__table_args__ = (
|
||||
sa.UniqueConstraint('name',
|
||||
name='uq_flavor_name'),
|
||||
)
|
||||
|
||||
description = sa.Column(sa.String(255), nullable=True)
|
||||
enabled = sa.Column(sa.Boolean(), nullable=False)
|
||||
flavor_profile_id = sa.Column(
|
||||
sa.String(36),
|
||||
sa.ForeignKey("flavor_profile.id",
|
||||
name="fk_flavor_flavor_profile_id"),
|
||||
nullable=False)
|
||||
|
@ -187,6 +187,8 @@ class Repositories(object):
|
||||
self.amp_build_slots = AmphoraBuildSlotsRepository()
|
||||
self.amp_build_req = AmphoraBuildReqRepository()
|
||||
self.quotas = QuotasRepository()
|
||||
self.flavor = FlavorRepository()
|
||||
self.flavor_profile = FlavorProfileRepository()
|
||||
|
||||
def create_load_balancer_and_vip(self, session, lb_dict, vip_dict):
|
||||
"""Inserts load balancer and vip entities into the database.
|
||||
@ -1768,3 +1770,11 @@ class QuotasRepository(BaseRepository):
|
||||
quotas.member = None
|
||||
quotas.pool = None
|
||||
session.flush()
|
||||
|
||||
|
||||
class FlavorRepository(BaseRepository):
|
||||
model_class = models.Flavor
|
||||
|
||||
|
||||
class FlavorProfileRepository(BaseRepository):
|
||||
model_class = models.FlavorProfile
|
||||
|
@ -15,6 +15,8 @@ import itertools
|
||||
|
||||
from octavia.policies import amphora
|
||||
from octavia.policies import base
|
||||
from octavia.policies import flavor
|
||||
from octavia.policies import flavor_profile
|
||||
from octavia.policies import healthmonitor
|
||||
from octavia.policies import l7policy
|
||||
from octavia.policies import l7rule
|
||||
@ -29,6 +31,8 @@ from octavia.policies import quota
|
||||
def list_rules():
|
||||
return itertools.chain(
|
||||
base.list_rules(),
|
||||
flavor.list_rules(),
|
||||
flavor_profile.list_rules(),
|
||||
healthmonitor.list_rules(),
|
||||
l7policy.list_rules(),
|
||||
l7rule.list_rules(),
|
||||
|
61
octavia/policies/flavor.py
Normal file
61
octavia/policies/flavor.py
Normal file
@ -0,0 +1,61 @@
|
||||
# Copyright 2017 Walmart Stores Inc..
|
||||
# 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 oslo_policy import policy
|
||||
|
||||
from octavia.common import constants
|
||||
|
||||
|
||||
rules = [
|
||||
policy.DocumentedRuleDefault(
|
||||
'{rbac_obj}{action}'.format(rbac_obj=constants.RBAC_FLAVOR,
|
||||
action=constants.RBAC_GET_ALL),
|
||||
constants.RULE_API_READ,
|
||||
"List Flavors",
|
||||
[{'method': 'GET', 'path': '/v2.0/lbaas/flavors'}]
|
||||
),
|
||||
policy.DocumentedRuleDefault(
|
||||
'{rbac_obj}{action}'.format(rbac_obj=constants.RBAC_FLAVOR,
|
||||
action=constants.RBAC_POST),
|
||||
constants.RULE_API_ADMIN,
|
||||
"Create a Flavor",
|
||||
[{'method': 'POST', 'path': '/v2.0/lbaas/flavors'}]
|
||||
),
|
||||
policy.DocumentedRuleDefault(
|
||||
'{rbac_obj}{action}'.format(rbac_obj=constants.RBAC_FLAVOR,
|
||||
action=constants.RBAC_PUT),
|
||||
constants.RULE_API_ADMIN,
|
||||
"Update a Flavor",
|
||||
[{'method': 'PUT', 'path': '/v2.0/lbaas/flavors/{flavor_id}'}]
|
||||
),
|
||||
policy.DocumentedRuleDefault(
|
||||
'{rbac_obj}{action}'.format(rbac_obj=constants.RBAC_FLAVOR,
|
||||
action=constants.RBAC_GET_ONE),
|
||||
constants.RULE_API_READ,
|
||||
"Show Flavor details",
|
||||
[{'method': 'GET',
|
||||
'path': '/v2.0/lbaas/flavors/{flavor_id}'}]
|
||||
),
|
||||
policy.DocumentedRuleDefault(
|
||||
'{rbac_obj}{action}'.format(rbac_obj=constants.RBAC_FLAVOR,
|
||||
action=constants.RBAC_DELETE),
|
||||
constants.RULE_API_ADMIN,
|
||||
"Remove a flavor",
|
||||
[{'method': 'DELETE',
|
||||
'path': '/v2.0/lbaas/flavors/{flavor_id}'}]
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
def list_rules():
|
||||
return rules
|
62
octavia/policies/flavor_profile.py
Normal file
62
octavia/policies/flavor_profile.py
Normal file
@ -0,0 +1,62 @@
|
||||
# Copyright 2017 Walmart Stores Inc..
|
||||
# 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 oslo_policy import policy
|
||||
|
||||
from octavia.common import constants
|
||||
|
||||
|
||||
rules = [
|
||||
policy.DocumentedRuleDefault(
|
||||
'{rbac_obj}{action}'.format(rbac_obj=constants.RBAC_FLAVOR_PROFILE,
|
||||
action=constants.RBAC_GET_ALL),
|
||||
constants.RULE_API_ADMIN,
|
||||
"List Flavors",
|
||||
[{'method': 'GET', 'path': '/v2.0/lbaas/flavorprofiles'}]
|
||||
),
|
||||
policy.DocumentedRuleDefault(
|
||||
'{rbac_obj}{action}'.format(rbac_obj=constants.RBAC_FLAVOR_PROFILE,
|
||||
action=constants.RBAC_POST),
|
||||
constants.RULE_API_ADMIN,
|
||||
"Create a Flavor",
|
||||
[{'method': 'POST', 'path': '/v2.0/lbaas/flavorprofiles'}]
|
||||
),
|
||||
policy.DocumentedRuleDefault(
|
||||
'{rbac_obj}{action}'.format(rbac_obj=constants.RBAC_FLAVOR_PROFILE,
|
||||
action=constants.RBAC_PUT),
|
||||
constants.RULE_API_ADMIN,
|
||||
"Update a Flavor",
|
||||
[{'method': 'PUT',
|
||||
'path': '/v2.0/lbaas/flavorprofiles/{flavor_profile_id}'}]
|
||||
),
|
||||
policy.DocumentedRuleDefault(
|
||||
'{rbac_obj}{action}'.format(rbac_obj=constants.RBAC_FLAVOR_PROFILE,
|
||||
action=constants.RBAC_GET_ONE),
|
||||
constants.RULE_API_ADMIN,
|
||||
"Show Flavor details",
|
||||
[{'method': 'GET',
|
||||
'path': '/v2.0/lbaas/flavorprofiles/{flavor_profile_id}'}]
|
||||
),
|
||||
policy.DocumentedRuleDefault(
|
||||
'{rbac_obj}{action}'.format(rbac_obj=constants.RBAC_FLAVOR_PROFILE,
|
||||
action=constants.RBAC_DELETE),
|
||||
constants.RULE_API_ADMIN,
|
||||
"Remove a flavor",
|
||||
[{'method': 'DELETE',
|
||||
'path': '/v2.0/lbaas/flavorprofiles/{flavor_profile_id}'}]
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
def list_rules():
|
||||
return rules
|
@ -46,13 +46,15 @@ class TestRootController(base_db_test.OctaviaDBTestBase):
|
||||
versions = self._get_versions_with_config(
|
||||
api_v1_enabled=True, api_v2_enabled=True)
|
||||
version_ids = tuple(v.get('id') for v in versions)
|
||||
self.assertEqual(7, len(version_ids))
|
||||
self.assertEqual(8, len(version_ids))
|
||||
self.assertIn('v1', version_ids)
|
||||
self.assertIn('v2.0', version_ids)
|
||||
self.assertIn('v2.1', version_ids)
|
||||
self.assertIn('v2.2', version_ids)
|
||||
self.assertIn('v2.3', version_ids)
|
||||
self.assertIn('v2.4', version_ids)
|
||||
self.assertIn('v2.5', version_ids)
|
||||
self.assertIn('v2.6', version_ids)
|
||||
|
||||
# Each version should have a 'self' 'href' to the API version URL
|
||||
# [{u'rel': u'self', u'href': u'http://localhost/v2'}]
|
||||
@ -72,12 +74,14 @@ class TestRootController(base_db_test.OctaviaDBTestBase):
|
||||
def test_api_v1_disabled(self):
|
||||
versions = self._get_versions_with_config(
|
||||
api_v1_enabled=False, api_v2_enabled=True)
|
||||
self.assertEqual(6, len(versions))
|
||||
self.assertEqual(7, len(versions))
|
||||
self.assertEqual('v2.0', versions[0].get('id'))
|
||||
self.assertEqual('v2.1', versions[1].get('id'))
|
||||
self.assertEqual('v2.2', versions[2].get('id'))
|
||||
self.assertEqual('v2.3', versions[3].get('id'))
|
||||
self.assertEqual('v2.4', versions[4].get('id'))
|
||||
self.assertEqual('v2.5', versions[5].get('id'))
|
||||
self.assertEqual('v2.6', versions[6].get('id'))
|
||||
|
||||
def test_api_v2_disabled(self):
|
||||
versions = self._get_versions_with_config(
|
||||
|
@ -32,6 +32,14 @@ class BaseAPITest(base_db_test.OctaviaDBTestBase):
|
||||
BASE_PATH = '/v2'
|
||||
BASE_PATH_v2_0 = '/v2.0'
|
||||
|
||||
# /lbaas/flavors
|
||||
FLAVORS_PATH = '/flavors'
|
||||
FLAVOR_PATH = FLAVORS_PATH + '/{flavor_id}'
|
||||
|
||||
# /lbaas/flavorprofiles
|
||||
FPS_PATH = '/flavorprofiles'
|
||||
FP_PATH = FPS_PATH + '/{fp_id}'
|
||||
|
||||
# /lbaas/loadbalancers
|
||||
LBS_PATH = '/lbaas/loadbalancers'
|
||||
LB_PATH = LBS_PATH + '/{lb_id}'
|
||||
@ -89,6 +97,7 @@ class BaseAPITest(base_db_test.OctaviaDBTestBase):
|
||||
enabled_provider_drivers={
|
||||
'amphora': 'Amp driver.',
|
||||
'noop_driver': 'NoOp driver.',
|
||||
'noop_driver-alt': 'NoOp driver alt alisas.',
|
||||
'octavia': 'Octavia driver.'})
|
||||
self.lb_repo = repositories.LoadBalancerRepository()
|
||||
self.listener_repo = repositories.ListenerRepository()
|
||||
@ -99,6 +108,8 @@ class BaseAPITest(base_db_test.OctaviaDBTestBase):
|
||||
self.l7rule_repo = repositories.L7RuleRepository()
|
||||
self.health_monitor_repo = repositories.HealthMonitorRepository()
|
||||
self.amphora_repo = repositories.AmphoraRepository()
|
||||
self.flavor_repo = repositories.FlavorRepository()
|
||||
self.flavor_profile_repo = repositories.FlavorProfileRepository()
|
||||
patcher2 = mock.patch('octavia.certificates.manager.barbican.'
|
||||
'BarbicanCertManager')
|
||||
self.cert_manager_mock = patcher2.start()
|
||||
@ -183,6 +194,21 @@ class BaseAPITest(base_db_test.OctaviaDBTestBase):
|
||||
expect_errors=expect_errors)
|
||||
return response
|
||||
|
||||
def create_flavor(self, name, description, flavor_profile_id, enabled):
|
||||
req_dict = {'name': name, 'description': description,
|
||||
'flavor_profile_id': flavor_profile_id,
|
||||
'enabled': enabled}
|
||||
body = {'flavor': req_dict}
|
||||
response = self.post(self.FLAVORS_PATH, body)
|
||||
return response.json.get('flavor')
|
||||
|
||||
def create_flavor_profile(self, name, privider_name, flavor_data):
|
||||
req_dict = {'name': name, 'provider_name': privider_name,
|
||||
constants.FLAVOR_DATA: flavor_data}
|
||||
body = {'flavorprofile': req_dict}
|
||||
response = self.post(self.FPS_PATH, body)
|
||||
return response.json.get('flavorprofile')
|
||||
|
||||
def create_load_balancer(self, vip_subnet_id,
|
||||
**optionals):
|
||||
req_dict = {'vip_subnet_id': vip_subnet_id,
|
||||
|
530
octavia/tests/functional/api/v2/test_flavor_profiles.py
Normal file
530
octavia/tests/functional/api/v2/test_flavor_profiles.py
Normal file
@ -0,0 +1,530 @@
|
||||
# Copyright 2017 Walmart Stores Inc.
|
||||
#
|
||||
# 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 oslo_config import cfg
|
||||
from oslo_config import fixture as oslo_fixture
|
||||
from oslo_db import exception as odb_exceptions
|
||||
from oslo_utils import uuidutils
|
||||
|
||||
from octavia.common import constants
|
||||
import octavia.common.context
|
||||
from octavia.tests.functional.api.v2 import base
|
||||
|
||||
|
||||
class TestFlavorProfiles(base.BaseAPITest):
|
||||
root_tag = 'flavorprofile'
|
||||
root_tag_list = 'flavorprofiles'
|
||||
root_tag_links = 'flavorprofile_links'
|
||||
|
||||
def _assert_request_matches_response(self, req, resp, **optionals):
|
||||
self.assertTrue(uuidutils.is_uuid_like(resp.get('id')))
|
||||
self.assertEqual(req.get('name'), resp.get('name'))
|
||||
self.assertEqual(req.get('provider_name'),
|
||||
resp.get('provider_name'))
|
||||
self.assertEqual(req.get(constants.FLAVOR_DATA),
|
||||
resp.get(constants.FLAVOR_DATA))
|
||||
|
||||
def test_empty_list(self):
|
||||
response = self.get(self.FPS_PATH)
|
||||
api_list = response.json.get(self.root_tag_list)
|
||||
self.assertEqual([], api_list)
|
||||
|
||||
def test_create(self):
|
||||
fp_json = {'name': 'test1', 'provider_name': 'noop_driver',
|
||||
constants.FLAVOR_DATA: '{"hello": "world"}'}
|
||||
body = self._build_body(fp_json)
|
||||
response = self.post(self.FPS_PATH, body)
|
||||
api_fp = response.json.get(self.root_tag)
|
||||
self._assert_request_matches_response(fp_json, api_fp)
|
||||
|
||||
def test_create_with_missing_name(self):
|
||||
fp_json = {'provider_name': 'pr1', constants.FLAVOR_DATA: '{"x": "y"}'}
|
||||
body = self._build_body(fp_json)
|
||||
response = self.post(self.FPS_PATH, body, status=400)
|
||||
err_msg = ("Invalid input for field/attribute name. Value: "
|
||||
"'None'. Mandatory field missing.")
|
||||
self.assertEqual(err_msg, response.json.get('faultstring'))
|
||||
|
||||
def test_create_with_missing_provider(self):
|
||||
fp_json = {'name': 'xyz', constants.FLAVOR_DATA: '{"x": "y"}'}
|
||||
body = self._build_body(fp_json)
|
||||
response = self.post(self.FPS_PATH, body, status=400)
|
||||
err_msg = ("Invalid input for field/attribute provider_name. "
|
||||
"Value: 'None'. Mandatory field missing.")
|
||||
self.assertEqual(err_msg, response.json.get('faultstring'))
|
||||
|
||||
def test_create_with_missing_flavor_data(self):
|
||||
fp_json = {'name': 'xyz', 'provider_name': 'pr1'}
|
||||
body = self._build_body(fp_json)
|
||||
response = self.post(self.FPS_PATH, body, status=400)
|
||||
err_msg = ("Invalid input for field/attribute flavor_data. "
|
||||
"Value: 'None'. Mandatory field missing.")
|
||||
self.assertEqual(err_msg, response.json.get('faultstring'))
|
||||
|
||||
def test_create_with_empty_flavor_data(self):
|
||||
fp_json = {'name': 'test1', 'provider_name': 'noop_driver',
|
||||
constants.FLAVOR_DATA: '{}'}
|
||||
body = self._build_body(fp_json)
|
||||
response = self.post(self.FPS_PATH, body)
|
||||
api_fp = response.json.get(self.root_tag)
|
||||
self._assert_request_matches_response(fp_json, api_fp)
|
||||
|
||||
def test_create_with_long_name(self):
|
||||
fp_json = {'name': 'n' * 256, 'provider_name': 'test1',
|
||||
constants.FLAVOR_DATA: '{"hello": "world"}'}
|
||||
body = self._build_body(fp_json)
|
||||
self.post(self.FPS_PATH, body, status=400)
|
||||
|
||||
def test_create_with_long_provider(self):
|
||||
fp_json = {'name': 'name1', 'provider_name': 'n' * 256,
|
||||
constants.FLAVOR_DATA: '{"hello": "world"}'}
|
||||
body = self._build_body(fp_json)
|
||||
self.post(self.FPS_PATH, body, status=400)
|
||||
|
||||
def test_create_with_long_flavor_data(self):
|
||||
fp_json = {'name': 'name1', 'provider_name': 'amp',
|
||||
constants.FLAVOR_DATA: 'n' * 4097}
|
||||
body = self._build_body(fp_json)
|
||||
self.post(self.FPS_PATH, body, status=400)
|
||||
|
||||
def test_create_authorized(self):
|
||||
fp_json = {'name': 'test1', 'provider_name': 'noop_driver',
|
||||
constants.FLAVOR_DATA: '{"hello": "world"}'}
|
||||
body = self._build_body(fp_json)
|
||||
self.conf = self.useFixture(oslo_fixture.Config(cfg.CONF))
|
||||
auth_strategy = self.conf.conf.api_settings.get('auth_strategy')
|
||||
self.conf.config(group='api_settings', auth_strategy=constants.TESTING)
|
||||
project_id = uuidutils.generate_uuid()
|
||||
with mock.patch.object(octavia.common.context.Context, 'project_id',
|
||||
project_id):
|
||||
override_credentials = {
|
||||
'service_user_id': None,
|
||||
'user_domain_id': None,
|
||||
'is_admin_project': True,
|
||||
'service_project_domain_id': None,
|
||||
'service_project_id': None,
|
||||
'roles': ['load-balancer_member'],
|
||||
'user_id': None,
|
||||
'is_admin': True,
|
||||
'service_user_domain_id': None,
|
||||
'project_domain_id': None,
|
||||
'service_roles': [],
|
||||
'project_id': project_id}
|
||||
with mock.patch(
|
||||
"oslo_context.context.RequestContext.to_policy_values",
|
||||
return_value=override_credentials):
|
||||
response = self.post(self.FPS_PATH, body)
|
||||
self.conf.config(group='api_settings', auth_strategy=auth_strategy)
|
||||
api_fp = response.json.get(self.root_tag)
|
||||
self._assert_request_matches_response(fp_json, api_fp)
|
||||
|
||||
def test_create_not_authorized(self):
|
||||
self.conf = self.useFixture(oslo_fixture.Config(cfg.CONF))
|
||||
auth_strategy = self.conf.conf.api_settings.get('auth_strategy')
|
||||
self.conf.config(group='api_settings', auth_strategy=constants.TESTING)
|
||||
fp_json = {'name': 'name',
|
||||
'provider_name': 'xyz', constants.FLAVOR_DATA: '{"x": "y"}'}
|
||||
body = self._build_body(fp_json)
|
||||
response = self.post(self.FPS_PATH, body, status=403)
|
||||
api_fp = response.json
|
||||
self.conf.config(group='api_settings', auth_strategy=auth_strategy)
|
||||
self.assertEqual(self.NOT_AUTHORIZED_BODY, api_fp)
|
||||
|
||||
def test_create_db_failure(self):
|
||||
fp_json = {'name': 'test1', 'provider_name': 'noop_driver',
|
||||
constants.FLAVOR_DATA: '{"hello": "world"}'}
|
||||
body = self._build_body(fp_json)
|
||||
with mock.patch("octavia.db.repositories.FlavorProfileRepository."
|
||||
"create") as mock_create:
|
||||
mock_create.side_effect = Exception
|
||||
self.post(self.FPS_PATH, body, status=500)
|
||||
|
||||
mock_create.side_effect = odb_exceptions.DBDuplicateEntry
|
||||
self.post(self.FPS_PATH, body, status=409)
|
||||
|
||||
def test_create_with_invalid_json(self):
|
||||
fp_json = {'name': 'test1', 'provider_name': 'noop_driver',
|
||||
constants.FLAVOR_DATA: '{hello: "world"}'}
|
||||
body = self._build_body(fp_json)
|
||||
self.post(self.FPS_PATH, body, status=400)
|
||||
|
||||
def test_get(self):
|
||||
fp = self.create_flavor_profile('name', 'noop_driver',
|
||||
'{"x": "y"}')
|
||||
self.assertTrue(uuidutils.is_uuid_like(fp.get('id')))
|
||||
response = self.get(
|
||||
self.FP_PATH.format(
|
||||
fp_id=fp.get('id'))).json.get(self.root_tag)
|
||||
self.assertEqual('name', response.get('name'))
|
||||
self.assertEqual(fp.get('id'), response.get('id'))
|
||||
|
||||
def test_get_one_fields_filter(self):
|
||||
fp = self.create_flavor_profile('name', 'noop_driver',
|
||||
'{"x": "y"}')
|
||||
self.assertTrue(uuidutils.is_uuid_like(fp.get('id')))
|
||||
response = self.get(
|
||||
self.FP_PATH.format(fp_id=fp.get('id')), params={
|
||||
'fields': ['id', 'provider_name']}).json.get(self.root_tag)
|
||||
self.assertEqual(fp.get('id'), response.get('id'))
|
||||
self.assertIn(u'id', response)
|
||||
self.assertIn(u'provider_name', response)
|
||||
self.assertNotIn(u'name', response)
|
||||
self.assertNotIn(constants.FLAVOR_DATA, response)
|
||||
|
||||
def test_get_authorized(self):
|
||||
fp = self.create_flavor_profile('name', 'noop_driver',
|
||||
'{"x": "y"}')
|
||||
self.assertTrue(uuidutils.is_uuid_like(fp.get('id')))
|
||||
self.conf = self.useFixture(oslo_fixture.Config(cfg.CONF))
|
||||
auth_strategy = self.conf.conf.api_settings.get('auth_strategy')
|
||||
self.conf.config(group='api_settings', auth_strategy=constants.TESTING)
|
||||
project_id = uuidutils.generate_uuid()
|
||||
with mock.patch.object(octavia.common.context.Context, 'project_id',
|
||||
project_id):
|
||||
override_credentials = {
|
||||
'service_user_id': None,
|
||||
'user_domain_id': None,
|
||||
'is_admin_project': True,
|
||||
'service_project_domain_id': None,
|
||||
'service_project_id': None,
|
||||
'roles': ['load-balancer_member'],
|
||||
'user_id': None,
|
||||
'is_admin': True,
|
||||
'service_user_domain_id': None,
|
||||
'project_domain_id': None,
|
||||
'service_roles': [],
|
||||
'project_id': project_id}
|
||||
with mock.patch(
|
||||
"oslo_context.context.RequestContext.to_policy_values",
|
||||
return_value=override_credentials):
|
||||
response = self.get(
|
||||
self.FP_PATH.format(
|
||||
fp_id=fp.get('id'))).json.get(self.root_tag)
|
||||
self.conf.config(group='api_settings', auth_strategy=auth_strategy)
|
||||
self.assertEqual('name', response.get('name'))
|
||||
self.assertEqual(fp.get('id'), response.get('id'))
|
||||
|
||||
def test_get_not_authorized(self):
|
||||
fp = self.create_flavor_profile('name', 'noop_driver',
|
||||
'{"x": "y"}')
|
||||
self.assertTrue(uuidutils.is_uuid_like(fp.get('id')))
|
||||
self.conf = self.useFixture(oslo_fixture.Config(cfg.CONF))
|
||||
auth_strategy = self.conf.conf.api_settings.get('auth_strategy')
|
||||
self.conf.config(group='api_settings', auth_strategy=constants.TESTING)
|
||||
self.get(self.FP_PATH.format(fp_id=fp.get('id')), status=403)
|
||||
self.conf.config(group='api_settings', auth_strategy=auth_strategy)
|
||||
|
||||
def test_get_all(self):
|
||||
fp1 = self.create_flavor_profile('test1', 'noop_driver',
|
||||
'{"image": "ubuntu"}')
|
||||
ref_fp_1 = {u'flavor_data': u'{"image": "ubuntu"}',
|
||||
u'id': fp1.get('id'), u'name': u'test1',
|
||||
u'provider_name': u'noop_driver'}
|
||||
self.assertTrue(uuidutils.is_uuid_like(fp1.get('id')))
|
||||
fp2 = self.create_flavor_profile('test2', 'noop_driver-alt',
|
||||
'{"image": "ubuntu"}')
|
||||
ref_fp_2 = {u'flavor_data': u'{"image": "ubuntu"}',
|
||||
u'id': fp2.get('id'), u'name': u'test2',
|
||||
u'provider_name': u'noop_driver-alt'}
|
||||
self.assertTrue(uuidutils.is_uuid_like(fp2.get('id')))
|
||||
|
||||
response = self.get(self.FPS_PATH)
|
||||
api_list = response.json.get(self.root_tag_list)
|
||||
self.assertEqual(2, len(api_list))
|
||||
self.assertIn(ref_fp_1, api_list)
|
||||
self.assertIn(ref_fp_2, api_list)
|
||||
|
||||
def test_get_all_fields_filter(self):
|
||||
fp1 = self.create_flavor_profile('test1', 'noop_driver',
|
||||
'{"image": "ubuntu"}')
|
||||
self.assertTrue(uuidutils.is_uuid_like(fp1.get('id')))
|
||||
fp2 = self.create_flavor_profile('test2', 'noop_driver-alt',
|
||||
'{"image": "ubuntu"}')
|
||||
self.assertTrue(uuidutils.is_uuid_like(fp2.get('id')))
|
||||
|
||||
response = self.get(self.FPS_PATH, params={
|
||||
'fields': ['id', 'name']})
|
||||
api_list = response.json.get(self.root_tag_list)
|
||||
self.assertEqual(2, len(api_list))
|
||||
for profile in api_list:
|
||||
self.assertIn(u'id', profile)
|
||||
self.assertIn(u'name', profile)
|
||||
self.assertNotIn(u'provider_name', profile)
|
||||
self.assertNotIn(constants.FLAVOR_DATA, profile)
|
||||
|
||||
def test_get_all_authorized(self):
|
||||
fp1 = self.create_flavor_profile('test1', 'noop_driver',
|
||||
'{"image": "ubuntu"}')
|
||||
self.assertTrue(uuidutils.is_uuid_like(fp1.get('id')))
|
||||
fp2 = self.create_flavor_profile('test2', 'noop_driver-alt',
|
||||
'{"image": "ubuntu"}')
|
||||
self.assertTrue(uuidutils.is_uuid_like(fp2.get('id')))
|
||||
self.conf = self.useFixture(oslo_fixture.Config(cfg.CONF))
|
||||
auth_strategy = self.conf.conf.api_settings.get('auth_strategy')
|
||||
self.conf.config(group='api_settings', auth_strategy=constants.TESTING)
|
||||
project_id = uuidutils.generate_uuid()
|
||||
with mock.patch.object(octavia.common.context.Context, 'project_id',
|
||||
project_id):
|
||||
override_credentials = {
|
||||