Availability Zone admin API

Adds the ability for admins to create/manage availability_zones
and profiles for use with upcoming functionality. Works like flavors.

Depends-On: https://review.opendev.org/#/c/694057/
Change-Id: I468d9fdf8c9d0898f9e30f04ac233510a10a53fc
This commit is contained in:
Adam Harwell 2019-11-12 18:15:38 -08:00
parent 4a5c24ef6f
commit 8ae6bc3697
77 changed files with 3817 additions and 58 deletions

View File

@ -7,6 +7,18 @@ path-amphora-id:
in: path
required: true
type: uuid
path-availability-zone-name:
description: |
The name of the availability zone to query.
in: path
required: true
type: string
path-availability-zone-profile-id:
description: |
The ID of the availability zone profile to query.
in: path
required: true
type: uuid
path-flavor-id:
description: |
The ID of the flavor to query.
@ -220,6 +232,66 @@ api_version_status:
in: body
required: true
type: string
availability-zone:
description: |
An availability zone object.
in: body
required: true
type: object
availability-zone-capabilities:
description: |
The provider availability zone capabilities dictonary object.
in: body
required: true
type: object
availability-zone-capability-description:
description: |
The provider availability zone capability description.
in: body
required: true
type: string
availability-zone-capability-name:
description: |
The provider availability zone capability name.
in: body
required: true
type: string
availability-zone-data:
description: |
The JSON string containing the availability zone metadata.
in: body
required: true
type: string
availability-zone-data-optional:
description: |
The JSON string containing the availability zone metadata.
in: body
required: false
type: string
availability-zone-profile:
description: |
An ``availability zone profile`` object.
in: body
required: true
type: object
availability-zone-profile-id:
description: |
The ID of the availability zone profile.
in: body
required: true
type: uuid
availability-zone-profiles:
description: |
A list of ``availability zone profile`` objects.
in: body
required: true
type: array
availability-zones:
description: |
A list of ``availability zone`` objects.
in: body
required: true
type: array
backup:
description: |
Is the member a backup? Backup members only receive traffic when all

View File

@ -0,0 +1,290 @@
.. -*- rst -*-
List Availability Zones
=======================
.. rest_method:: GET /v2.0/lbaas/availabilityzones
List all available availability zones.
Use the ``fields`` query parameter to control which fields are
returned in the response body. Additionally, you can filter results
by using query string parameters. For information, see :ref:`filtering`.
The list might be empty.
**New in version 2.14**
.. rest_status_code:: success ../http-status.yaml
- 200
.. rest_status_code:: error ../http-status.yaml
- 400
- 401
- 500
Request
-------
.. rest_parameters:: ../parameters.yaml
- fields: fields
Curl Example
------------
.. literalinclude:: examples/availabilityzone-list-curl
:language: bash
Response Parameters
-------------------
.. rest_parameters:: ../parameters.yaml
- description: description
- enabled: enabled
- availability_zone_profile_id: availability-zone-profile-id
- availability_zones: availability-zones
- name: name
Response Example
----------------
.. literalinclude:: examples/availabilityzone-list-response.json
:language: javascript
Create Availability Zone
========================
.. rest_method:: POST /v2.0/lbaas/availabilityzones
Creates an availability zone.
If the API cannot fulfill the request due to insufficient data or
data that is not valid, the service returns the HTTP ``Bad Request
(400)`` response code with information about the failure in the
response body. Validation errors require that you correct the error
and submit the request again.
If you are not an administrative user the service returns the HTTP ``Forbidden
(403)`` response code.
**New in version 2.14**
.. rest_status_code:: success ../http-status.yaml
- 201
.. rest_status_code:: error ../http-status.yaml
- 400
- 401
- 403
- 500
Request
-------
.. rest_parameters:: ../parameters.yaml
- description: description-optional
- enabled: enabled-optional
- availability_zone: availability-zone
- availability_zone_profile_id: availability-zone-profile-id
- name: name
Request Example
---------------
.. literalinclude:: examples/availabilityzone-create-request.json
:language: javascript
Curl Example
------------
.. literalinclude:: examples/availabilityzone-create-curl
:language: bash
Response Parameters
-------------------
.. rest_parameters:: ../parameters.yaml
- description: description
- enabled: enabled
- availability_zone_profile_id: availability-zone-profile-id
- availability_zone: availability-zone
- name: name
Response Example
----------------
.. literalinclude:: examples/availabilityzone-create-response.json
:language: javascript
Show Availability Zone Details
==============================
.. rest_method:: GET /v2.0/lbaas/availabilityzones/{availability_zone_name}
Shows the details of an availability zone.
Use the ``fields`` query parameter to control which fields are
returned in the response body. Additionally, you can filter results
by using query string parameters. For information, see :ref:`filtering`.
This operation does not require a request body.
**New in version 2.14**
.. rest_status_code:: success ../http-status.yaml
- 200
.. rest_status_code:: error ../http-status.yaml
- 401
- 404
- 500
Request
-------
.. rest_parameters:: ../parameters.yaml
- fields: fields
- availability_zone_name: path-availability-zone-name
Curl Example
------------
.. literalinclude:: examples/availabilityzone-show-curl
:language: bash
Response Parameters
-------------------
.. rest_parameters:: ../parameters.yaml
- description: description
- enabled: enabled
- availability_zone_profile_id: availability-zone-profile-id
- availability_zone: availability-zone
- name: name
Response Example
----------------
.. literalinclude:: examples/availabilityzone-show-response.json
:language: javascript
Update an Availability Zone
===========================
.. rest_method:: PUT /v2.0/lbaas/availabilityzones/{availability_zone_name}
Update an availability zone.
If you are not an administrative user the service returns the HTTP ``Forbidden
(403)`` response code.
**New in version 2.14**
.. rest_status_code:: success ../http-status.yaml
- 200
.. rest_status_code:: error ../http-status.yaml
- 400
- 401
- 403
- 404
- 500
Request
-------
.. rest_parameters:: ../parameters.yaml
- description: description-optional
- enabled: enabled-optional
- availability_zone: availability-zone
- availability_zone_name: path-availability-zone-name
Request Example
---------------
.. literalinclude:: examples/availabilityzone-update-request.json
:language: javascript
Curl Example
------------
.. literalinclude:: examples/availabilityzone-update-curl
:language: bash
Response Parameters
-------------------
.. rest_parameters:: ../parameters.yaml
- description: description
- enabled: enabled
- availability_zone_profile_id: availability-zone-profile-id
- availability_zone: availability-zone
- name: name
Response Example
----------------
.. literalinclude:: examples/availabilityzone-update-response.json
:language: javascript
Remove an Availability Zone
===========================
.. rest_method:: DELETE /v2.0/lbaas/availabilityzones/{availability_zone_name}
Remove an availability zone and its associated configuration.
If any load balancers are using this availability zone the service returns
the HTTP ``Conflict (409)`` response code.
If you are not an administrative user the service returns the HTTP ``Forbidden
(403)`` response code.
**New in version 2.14**
.. rest_status_code:: success ../http-status.yaml
- 204
.. rest_status_code:: error ../http-status.yaml
- 401
- 403
- 404
- 409
- 500
Request
-------
.. rest_parameters:: ../parameters.yaml
- availability_zone_name: path-availability-zone-name
Curl Example
------------
.. literalinclude:: examples/availabilityzone-delete-curl
:language: bash
Response
--------
There is no body content for the response of a successful DELETE request.

View File

@ -0,0 +1,297 @@
.. -*- rst -*-
List Availability Zone Profiles
===============================
.. rest_method:: GET /v2.0/lbaas/availabilityzoneprofiles
List all available Availability Zone Profiles.
Use the ``fields`` query parameter to control which fields are
returned in the response body. Additionally, you can filter results
by using query string parameters. For information, see :ref:`filtering`.
If you are not an administrative user the service returns the HTTP ``Forbidden
(403)`` response code.
The list might be empty.
**New in version 2.14**
.. rest_status_code:: success ../http-status.yaml
- 200
.. rest_status_code:: error ../http-status.yaml
- 400
- 401
- 403
- 500
Request
-------
.. rest_parameters:: ../parameters.yaml
- fields: fields
Curl Example
------------
.. literalinclude:: examples/availabilityzoneprofile-list-curl
:language: bash
Response Parameters
-------------------
.. rest_parameters:: ../parameters.yaml
- availability_zone_data: availability-zone-data
- availability_zone_profiles: availability-zone-profiles
- id: availability-zone-profile-id
- name: name
- provider_name: provider-name
Response Example
----------------
.. literalinclude:: examples/availabilityzoneprofile-list-response.json
:language: javascript
Create Availability Zone Profile
================================
.. rest_method:: POST /v2.0/lbaas/availabilityzoneprofiles
Creates a Availability Zone Profile.
If the API cannot fulfill the request due to insufficient data or
data that is not valid, the service returns the HTTP ``Bad Request
(400)`` response code with information about the failure in the
response body. Validation errors require that you correct the error
and submit the request again.
If you are not an administrative user the service returns the HTTP ``Forbidden
(403)`` response code.
**New in version 2.14**
.. rest_status_code:: success ../http-status.yaml
- 201
.. rest_status_code:: error ../http-status.yaml
- 400
- 401
- 403
- 500
Request
-------
.. rest_parameters:: ../parameters.yaml
- availability_zone_data: availability-zone-data
- availability_zone_profile: availability-zone-profile
- name: name
- provider_name: provider-name
Request Example
---------------
.. literalinclude:: examples/availabilityzoneprofile-create-request.json
:language: javascript
Curl Example
------------
.. literalinclude:: examples/availabilityzoneprofile-create-curl
:language: bash
Response Parameters
-------------------
.. rest_parameters:: ../parameters.yaml
- availability_zone_data: availability-zone-data
- availability_zone_profile: availability-zone-profile
- id: availability-zone-profile-id
- name: name
- provider_name: provider-name
Response Example
----------------
.. literalinclude:: examples/availabilityzoneprofile-create-response.json
:language: javascript
Show Availability Zone Profile Details
======================================
.. rest_method:: GET /v2.0/lbaas/availabilityzoneprofiles/{availability_zone_profile_id}
Shows the details of a Availability Zone Profile.
Use the ``fields`` query parameter to control which fields are
returned in the response body. Additionally, you can filter results
by using query string parameters. For information, see :ref:`filtering`.
If you are not an administrative user the service returns the HTTP ``Forbidden
(403)`` response code.
This operation does not require a request body.
**New in version 2.14**
.. rest_status_code:: success ../http-status.yaml
- 200
.. rest_status_code:: error ../http-status.yaml
- 401
- 403
- 404
- 500
Request
-------
.. rest_parameters:: ../parameters.yaml
- fields: fields
- availability_zone_profile_id: path-availability-zone-profile-id
Curl Example
------------
.. literalinclude:: examples/availabilityzoneprofile-show-curl
:language: bash
Response Parameters
-------------------
.. rest_parameters:: ../parameters.yaml
- availability_zone_data: availability-zone-data
- availability_zone_profile: availability-zone-profile
- id: availability-zone-profile-id
- name: name
- provider_name: provider-name
Response Example
----------------
.. literalinclude:: examples/availabilityzoneprofile-show-response.json
:language: javascript
Update a Availability Zone Profile
==================================
.. rest_method:: PUT /v2.0/lbaas/availabilityzoneprofiles/{availability_zone_profile_id}
Update a Availability Zone Profile.
If you are not an administrative user the service returns the HTTP ``Forbidden
(403)`` response code.
**New in version 2.14**
.. rest_status_code:: success ../http-status.yaml
- 200
.. rest_status_code:: error ../http-status.yaml
- 400
- 401
- 403
- 404
- 500
Request
-------
.. rest_parameters:: ../parameters.yaml
- availability_zone_data: availability-zone-data-optional
- availability_zone_profile: availability-zone-profile
- availability_zone_profile_id: path-availability-zone-profile-id
- name: name-optional
- provider_name: provider-name-optional
Request Example
---------------
.. literalinclude:: examples/availabilityzoneprofile-update-request.json
:language: javascript
Curl Example
------------
.. literalinclude:: examples/availabilityzoneprofile-update-curl
:language: bash
Response Parameters
-------------------
.. rest_parameters:: ../parameters.yaml
- availability_zone_data: availability-zone-data
- availability_zone_profile: availability-zone-profile
- id: availability-zone-profile-id
- name: name
- provider_name: provider-name
Response Example
----------------
.. literalinclude:: examples/availabilityzoneprofile-update-response.json
:language: javascript
Remove a Availability Zone Profile
==================================
.. rest_method:: DELETE /v2.0/lbaas/availabilityzoneprofiles/{availability_zone_profile_id}
Remove a Availability Zone Profile and its associated configuration.
If any availability zone is using this Availability Zone Profile the service
returns the HTTP ``Conflict (409)`` response code.
If you are not an administrative user the service returns the HTTP ``Forbidden
(403)`` response code.
**New in version 2.14**
.. rest_status_code:: success ../http-status.yaml
- 204
.. rest_status_code:: error ../http-status.yaml
- 401
- 403
- 404
- 409
- 500
Request
-------
.. rest_parameters:: ../parameters.yaml
- availability_zone_profile_id: path-availability-zone-profile-id
Curl Example
------------
.. literalinclude:: examples/availabilityzoneprofile-delete-curl
:language: bash
Response
--------
There is no body content for the response of a successful DELETE request.

View File

@ -0,0 +1 @@
curl -X POST -H "Content-Type: application/json" -H "X-Auth-Token: <token>" -d '{"availability_zone":{"name":"my_az","description":"My availability zone.","enabled":true,"availability_zone_profile_id":"5712097e-0092-45dc-bff0-ab68b61ad51a"}}' http://198.51.100.10:9876/v2.0/lbaas/availabilityzones

View File

@ -0,0 +1,8 @@
{
"availability_zone": {
"name": "my_az",
"description": "My availability zone.",
"enabled": true,
"availability_zone_profile_id": "5712097e-0092-45dc-bff0-ab68b61ad51a"
}
}

View File

@ -0,0 +1,8 @@
{
"availability_zone": {
"name": "my_az",
"description": "My availability zone.",
"enabled": true,
"availability_zone_profile_id": "5712097e-0092-45dc-bff0-ab68b61ad51a"
}
}

View File

@ -0,0 +1 @@
curl -X DELETE -H "X-Auth-Token: <token>" http://198.51.100.10:9876/v2.0/lbaas/availabilityzones/my_az

View File

@ -0,0 +1 @@
curl -X GET -H "X-Auth-Token: <token>" http://198.51.100.10:9876/v2.0/lbaas/availabilityzones

View File

@ -0,0 +1,10 @@
{
"availability_zones": [
{
"name": "my_az",
"description": "My availability zone.",
"enabled": true,
"availability_zone_profile_id": "5712097e-0092-45dc-bff0-ab68b61ad51a"
}
]
}

View File

@ -0,0 +1 @@
curl -X GET -H "X-Auth-Token: <token>" http://198.51.100.10:9876/v2.0/lbaas/availabilityzones/my_az

View File

@ -0,0 +1,8 @@
{
"availability_zone": {
"name": "my_az",
"description": "My availability zone.",
"enabled": true,
"availability_zone_profile_id": "5712097e-0092-45dc-bff0-ab68b61ad51a"
}
}

View File

@ -0,0 +1 @@
curl -X PUT -H "Content-Type: application/json" -H "X-Auth-Token: <token>" -d '{"availability_zone":{"description":"My availability zone.","enabled":false}}' http://198.51.100.10:9876/v2.0/lbaas/availabilityzones/my_az

View File

@ -0,0 +1,6 @@
{
"availability_zone": {
"description": "My availability zone.",
"enabled": false
}
}

View File

@ -0,0 +1,8 @@
{
"availability_zone": {
"name": "my_az",
"description": "My availability zone.",
"enabled": false,
"availability_zone_profile_id": "5712097e-0092-45dc-bff0-ab68b61ad51a"
}
}

View File

@ -0,0 +1 @@
curl -X POST -H "Content-Type: application/json" -H "X-Auth-Token: <token>" -d '{"availability_zone_profile":{"name":"some_az","provider_name":"amphora","availability_zone_data":"{\"compute_zone\": \"az1\"}"}}' http://198.51.100.10:9876/v2.0/lbaas/availabilityzoneprofiles

View File

@ -0,0 +1,8 @@
{
"availability_zone_profile":
{
"name": "some_az",
"provider_name": "amphora",
"availability_zone_data": "{\"compute_zone\": \"az1\"}"
}
}

View File

@ -0,0 +1,9 @@
{
"availability_zone_profile":
{
"id": "5712097e-0092-45dc-bff0-ab68b61ad51a",
"name": "some_az",
"provider_name": "amphora",
"availability_zone_data": "{\"compute_zone\": \"az1\"}"
}
}

View File

@ -0,0 +1 @@
curl -X DELETE -H "X-Auth-Token: <token>" http://198.51.100.10:9876/v2.0/lbaas/availabilityzoneprofiles/5712097e-0092-45dc-bff0-ab68b61ad51a

View File

@ -0,0 +1 @@
curl -X GET -H "X-Auth-Token: <token>" http://198.51.100.10:9876/v2.0/lbaas/availabilityzoneprofiles

View File

@ -0,0 +1,10 @@
{
"availability_zone_profiles": [
{
"id": "5712097e-0092-45dc-bff0-ab68b61ad51a",
"name": "some_az",
"provider_name": "amphora",
"availability_zone_data": "{\"compute_zone\": \"az1\"}"
}
]
}

View File

@ -0,0 +1 @@
curl -X GET -H "X-Auth-Token: <token>" http://198.51.100.10:9876/v2.0/lbaas/availabilityzoneprofiles/5712097e-0092-45dc-bff0-ab68b61ad51a

View File

@ -0,0 +1,9 @@
{
"availability_zone_profile":
{
"id": "5712097e-0092-45dc-bff0-ab68b61ad51a",
"name": "some_az",
"provider_name": "amphora",
"availability_zone_data": "{\"compute_zone\": \"az1\"}"
}
}

View File

@ -0,0 +1 @@
curl -X PUT -H "Content-Type: application/json" -H "X-Auth-Token: <token>" -d '{"availability_zone_profile":{"name":"other_az","provider_name":"amphora","availability_zone_data":"{\"compute_zone\": \"az2\"}"}}' http://198.51.100.10:9876/v2.0/lbaas/availabilityzoneprofiles/5712097e-0092-45dc-bff0-ab68b61ad51a

View File

@ -0,0 +1,8 @@
{
"availability_zone_profile":
{
"name": "other_az",
"provider_name": "amphora",
"availability_zone_data": "{\"compute_zone\": \"az2\"}"
}
}

View File

@ -0,0 +1,9 @@
{
"availability_zone_profile":
{
"id": "5712097e-0092-45dc-bff0-ab68b61ad51a",
"name": "other_az",
"provider_name": "amphora",
"availability_zone_data": "{\"compute_zone\": \"az2\"}"
}
}

View File

@ -0,0 +1 @@
curl -X GET -H "X-Auth-Token: <token>" http://198.51.100.10:9876/v2/lbaas/providers/amphora/availability_zone_capabilities

View File

@ -0,0 +1,8 @@
{
"availability_zone_capabilities": [
{
"name": "compute_zone",
"description": "The compute availability zone."
}
]
}

View File

@ -97,7 +97,7 @@ Request
- name: name
Request Example
----------------
---------------
.. literalinclude:: examples/flavor-create-request.json
:language: javascript

View File

@ -99,7 +99,7 @@ Request
- provider_name: provider-name
Request Example
----------------
---------------
.. literalinclude:: examples/flavorprofile-create-request.json
:language: javascript

View File

@ -66,6 +66,16 @@ Flavor Profiles
---------------
.. include:: flavorprofile.inc
------------------
Availability Zones
------------------
.. include:: availabilityzone.inc
--------------------------
Availability Zone Profiles
--------------------------
.. include:: availabilityzoneprofile.inc
--------
Amphorae
--------

View File

@ -103,3 +103,57 @@ Response Example
.. literalinclude:: examples/provider-flavor-capability-show-response.json
:language: javascript
Show Provider Availability Zone Capabilities
============================================
.. rest_method:: GET /v2/lbaas/providers/{provider}/availability_zone_capabilities
Shows the provider driver availability zone capabilities. These are the
features of the provider driver that can be configured in an Octavia
availability zone. This API returns a list of dictionaries with the name and
description of each availability zone capability of the provider.
The list might be empty and a provider driver may not implement this feature.
**New in version 2.14**
.. rest_status_code:: success ../http-status.yaml
- 200
.. rest_status_code:: error ../http-status.yaml
- 400
- 401
- 403
- 500
Request
-------
.. rest_parameters:: ../parameters.yaml
- fields: fields
- provider: path-provider
Curl Example
------------
.. literalinclude:: examples/provider-availability-zone-capability-show-curl
:language: bash
Response Parameters
-------------------
.. rest_parameters:: ../parameters.yaml
- availability_zone_capabilities: availability-zone-capabilities
- name: availability-zone-capability-name
- description: availability-zone-capability-description
Response Example
----------------
.. literalinclude:: examples/provider-availability-zone-capability-show-response.json
:language: javascript

View File

@ -0,0 +1,47 @@
# Copyright 2018 Rackspace US Inc. All rights reserved.
# Copyright 2019 Verizon Media
#
# 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 octavia.common import constants as consts
# This is a JSON schema validation dictionary
# https://json-schema.org/latest/json-schema-validation.html
#
# Note: This is used to generate the amphora driver "supported availability
# zone metadata" dictionary. Each property should include a description
# for the user to understand what this availability zone setting does.
#
# Where possible, the property name should match the configuration file name
# for the setting. The configuration file setting is the default when a
# setting is not defined in an availability zone profile.
SUPPORTED_AVAILABILITY_ZONE_SCHEMA = {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Octavia Amphora Driver Availability Zone Metadata Schema",
"description": "This schema is used to validate new availability zone "
"profiles submitted for use in an amphora driver "
"availability zone.",
"type": "object",
"additionalProperties": False,
"properties": {
consts.COMPUTE_ZONE: {
"type": "string",
"description": "The compute availability zone."
},
consts.MANAGEMENT_NETWORK: {
"type": "string",
"description": "The management network ID for the amphora."
}
}
}

View File

@ -24,6 +24,7 @@ from octavia_lib.api.drivers import data_models as driver_dm
from octavia_lib.api.drivers import exceptions
from octavia_lib.api.drivers import provider_base as driver_base
from octavia.api.drivers.amphora_driver import availability_zone_schema
from octavia.api.drivers.amphora_driver import flavor_schema
from octavia.api.drivers import utils as driver_utils
from octavia.common import constants as consts
@ -385,3 +386,70 @@ class AmphoraProviderDriver(driver_base.ProviderDriver):
# TODO(johnsom) Fix this to raise a NotFound error
# when the octavia-lib supports it.
compute_driver.validate_flavor(compute_flavor)
# Availability Zone
def get_supported_availability_zone_metadata(self):
"""Returns the valid availability zone metadata keys and descriptions.
This extracts the valid availability zone metadata keys and
descriptions from the JSON validation schema and returns it as a
dictionary.
:return: Dictionary of availability zone metadata keys and descriptions
:raises DriverError: An unexpected error occurred.
"""
try:
props = (
availability_zone_schema.SUPPORTED_AVAILABILITY_ZONE_SCHEMA[
'properties'])
return {k: v.get('description', '') for k, v in props.items()}
except Exception as e:
raise exceptions.DriverError(
user_fault_string='Failed to get the supported availability '
'zone metadata due to: {}'.format(str(e)),
operator_fault_string='Failed to get the supported '
'availability zone metadata due to: '
'{}'.format(str(e)))
def validate_availability_zone(self, availability_zone_dict):
"""Validates availability zone profile data.
This will validate an availability zone profile dataset against the
availability zone settings the amphora driver supports.
:param availability_zone_dict: The availability zone dict to validate.
:type availability_zone_dict: dict
:return: None
:raises DriverError: An unexpected error occurred.
:raises UnsupportedOptionError: If the driver does not support
one of the availability zone settings.
"""
try:
validate(
availability_zone_dict,
availability_zone_schema.SUPPORTED_AVAILABILITY_ZONE_SCHEMA)
except js_exceptions.ValidationError as e:
error_object = ''
if e.relative_path:
error_object = '{} '.format(e.relative_path[0])
raise exceptions.UnsupportedOptionError(
user_fault_string='{0}{1}'.format(error_object, e.message),
operator_fault_string=str(e))
except Exception as e:
raise exceptions.DriverError(
user_fault_string='Failed to validate the availability zone '
'metadata due to: {}'.format(str(e)),
operator_fault_string='Failed to validate the availability '
'zone metadata due to: {}'.format(str(e))
)
compute_zone = availability_zone_dict.get(consts.COMPUTE_ZONE, None)
if compute_zone:
compute_driver = stevedore_driver.DriverManager(
namespace='octavia.compute.drivers',
name=CONF.controller_worker.compute_driver,
invoke_on_load=True
).driver
# TODO(johnsom) Fix this to raise a NotFound error
# when the octavia-lib supports it.
compute_driver.validate_availability_zone(compute_zone)

View File

@ -24,6 +24,7 @@ from octavia_lib.api.drivers import data_models as driver_dm
from octavia_lib.api.drivers import exceptions
from octavia_lib.api.drivers import provider_base as driver_base
from octavia.api.drivers.amphora_driver import availability_zone_schema
from octavia.api.drivers.amphora_driver import flavor_schema
from octavia.api.drivers import utils as driver_utils
from octavia.common import constants as consts
@ -384,3 +385,70 @@ class AmphoraProviderDriver(driver_base.ProviderDriver):
# TODO(johnsom) Fix this to raise a NotFound error
# when the octavia-lib supports it.
compute_driver.validate_flavor(compute_flavor)
# Availability Zone
def get_supported_availability_zone_metadata(self):
"""Returns the valid availability zone metadata keys and descriptions.
This extracts the valid availability zone metadata keys and
descriptions from the JSON validation schema and returns it as a
dictionary.
:return: Dictionary of availability zone metadata keys and descriptions
:raises DriverError: An unexpected error occurred.
"""
try:
props = (
availability_zone_schema.SUPPORTED_AVAILABILITY_ZONE_SCHEMA[
'properties'])
return {k: v.get('description', '') for k, v in props.items()}
except Exception as e:
raise exceptions.DriverError(
user_fault_string='Failed to get the supported availability '
'zone metadata due to: {}'.format(str(e)),
operator_fault_string='Failed to get the supported '
'availability zone metadata due to: '
'{}'.format(str(e)))
def validate_availability_zone(self, availability_zone_dict):
"""Validates availability zone profile data.
This will validate an availability zone profile dataset against the
availability zone settings the amphora driver supports.
:param availability_zone_dict: The availability zone dict to validate.
:type availability_zone_dict: dict
:return: None
:raises DriverError: An unexpected error occurred.
:raises UnsupportedOptionError: If the driver does not support
one of the availability zone settings.
"""
try:
validate(
availability_zone_dict,
availability_zone_schema.SUPPORTED_AVAILABILITY_ZONE_SCHEMA)
except js_exceptions.ValidationError as e:
error_object = ''
if e.relative_path:
error_object = '{} '.format(e.relative_path[0])
raise exceptions.UnsupportedOptionError(
user_fault_string='{0}{1}'.format(error_object, e.message),
operator_fault_string=str(e))
except Exception as e:
raise exceptions.DriverError(
user_fault_string='Failed to validate the availability zone '
'metadata due to: {}'.format(str(e)),
operator_fault_string='Failed to validate the availability '
'zone metadata due to: {}'.format(str(e))
)
compute_zone = availability_zone_dict.get(consts.COMPUTE_ZONE, None)
if compute_zone:
compute_driver = stevedore_driver.DriverManager(
namespace='octavia.compute.drivers',
name=CONF.controller_worker.compute_driver,
invoke_on_load=True
).driver
# TODO(johnsom) Fix this to raise a NotFound error
# when the octavia-lib supports it.
compute_driver.validate_availability_zone(compute_zone)

View File

@ -242,6 +242,23 @@ class NoopManager(object):
flavor_hash = hash(frozenset(flavor_metadata))
self.driverconfig[flavor_hash] = (flavor_metadata, 'validate_flavor')
# Availability Zone
def get_supported_availability_zone_metadata(self):
LOG.debug(
'Provider %s no-op, get_supported_availability_zone_metadata',
self.__class__.__name__)
return {"compute_zone": "The compute availability zone to use for "
"this loadbalancer."}
def validate_availability_zone(self, availability_zone_metadata):
LOG.debug('Provider %s no-op, validate_availability_zone metadata: %s',
self.__class__.__name__, availability_zone_metadata)
availability_zone_hash = hash(frozenset(availability_zone_metadata))
self.driverconfig[availability_zone_hash] = (
availability_zone_metadata, 'validate_availability_zone')
class NoopProviderDriver(driver_base.ProviderDriver):
def __init__(self):
@ -334,3 +351,10 @@ class NoopProviderDriver(driver_base.ProviderDriver):
def validate_flavor(self, flavor_metadata):
self.driver.validate_flavor(flavor_metadata)
# Availability Zone
def get_supported_availability_zone_metadata(self):
return self.driver.get_supported_availability_zone_metadata()
def validate_availability_zone(self, availability_zone_metadata):
self.driver.validate_availability_zone(availability_zone_metadata)

View File

@ -88,6 +88,9 @@ class RootController(rest.RestController):
self._add_a_version(versions, 'v2.12', 'v2', 'SUPPORTED',
'2019-09-11T00:00:00Z', host_url)
# SOURCE_IP_PORT algorithm
self._add_a_version(versions, 'v2.13', 'v2', 'CURRENT',
self._add_a_version(versions, 'v2.13', 'v2', 'SUPPORTED',
'2019-09-13T00:00:00Z', host_url)
# Availability Zones
self._add_a_version(versions, 'v2.14', 'v2', 'CURRENT',
'2019-11-10T00:00:00Z', host_url)
return {'versions': versions}

View File

@ -16,6 +16,8 @@ from wsme import types as wtypes
from wsmeext import pecan as wsme_pecan
from octavia.api.v2.controllers import amphora
from octavia.api.v2.controllers import availability_zone_profiles
from octavia.api.v2.controllers import availability_zones
from octavia.api.v2.controllers import base
from octavia.api.v2.controllers import flavor_profiles
from octavia.api.v2.controllers import flavors
@ -47,6 +49,10 @@ class BaseV2Controller(base.BaseController):
self.providers = provider.ProviderController()
self.flavors = flavors.FlavorsController()
self.flavorprofiles = flavor_profiles.FlavorProfileController()
self.availabilityzones = (
availability_zones.AvailabilityZonesController())
self.availabilityzoneprofiles = (
availability_zone_profiles.AvailabilityZoneProfileController())
@wsme_pecan.wsexpose(wtypes.text)
def get(self):

View File

@ -0,0 +1,237 @@
# Copyright 2019 Verizon Media
#
# 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 availability_zone_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 AvailabilityZoneProfileController(base.BaseController):
RBAC_TYPE = constants.RBAC_AVAILABILITY_ZONE_PROFILE
def __init__(self):
super(AvailabilityZoneProfileController, self).__init__()
@wsme_pecan.wsexpose(profile_types.AvailabilityZoneProfileRootResponse,
wtypes.text, [wtypes.text], ignore_extra_args=True)
def get_one(self, id, fields=None):
"""Gets an Availability Zone Profile's detail."""
context = pecan.request.context.get('octavia_context')
self._auth_validate_action(context, context.project_id,
constants.RBAC_GET_ONE)
if id == constants.NIL_UUID:
raise exceptions.NotFound(resource='Availability Zone Profile',
id=constants.NIL_UUID)
db_availability_zone_profile = self._get_db_availability_zone_profile(
context.session, id)
result = self._convert_db_to_type(
db_availability_zone_profile,
profile_types.AvailabilityZoneProfileResponse)
if fields is not None:
result = self._filter_fields([result], fields)[0]
return profile_types.AvailabilityZoneProfileRootResponse(
availability_zone_profile=result)
@wsme_pecan.wsexpose(profile_types.AvailabilityZoneProfilesRootResponse,
[wtypes.text], ignore_extra_args=True)
def get_all(self, fields=None):
"""Lists all Availability Zone Profiles."""
pcontext = pecan.request.context
context = pcontext.get('octavia_context')
self._auth_validate_action(context, context.project_id,
constants.RBAC_GET_ALL)
db_availability_zone_profiles, links = (
self.repositories.availability_zone_profile.get_all(
context.session,
pagination_helper=pcontext.get(constants.PAGINATION_HELPER)))
result = self._convert_db_to_type(
db_availability_zone_profiles,
[profile_types.AvailabilityZoneProfileResponse])
if fields is not None:
result = self._filter_fields(result, fields)
return profile_types.AvailabilityZoneProfilesRootResponse(
availability_zone_profiles=result,
availability_zone_profile_links=links)
@wsme_pecan.wsexpose(profile_types.AvailabilityZoneProfileRootResponse,
body=profile_types.AvailabilityZoneProfileRootPOST,
status_code=201)
def post(self, availability_zone_profile_):
"""Creates an Availability Zone Profile."""
availability_zone_profile = (
availability_zone_profile_.availability_zone_profile)
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:
availability_zone_data_dict = jsonutils.loads(
availability_zone_profile.availability_zone_data)
except Exception:
raise exceptions.InvalidOption(
value=availability_zone_profile.availability_zone_data,
option=constants.AVAILABILITY_ZONE_DATA)
# Validate that the provider driver supports the metadata
driver = driver_factory.get_driver(
availability_zone_profile.provider_name)
driver_utils.call_provider(
driver.name, driver.validate_availability_zone,
availability_zone_data_dict)
lock_session = db_api.get_session(autocommit=False)
try:
availability_zone_profile_dict = availability_zone_profile.to_dict(
render_unsets=True)
availability_zone_profile_dict['id'] = uuidutils.generate_uuid()
db_availability_zone_profile = (
self.repositories.availability_zone_profile.create(
lock_session, **availability_zone_profile_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_availability_zone_profile,
profile_types.AvailabilityZoneProfileResponse)
return profile_types.AvailabilityZoneProfileRootResponse(
availability_zone_profile=result)
def _validate_update_azp(self, context, id, availability_zone_profile):
if availability_zone_profile.name is None:
raise exceptions.InvalidOption(value=None, option=constants.NAME)
if availability_zone_profile.provider_name is None:
raise exceptions.InvalidOption(
value=None, option=constants.PROVIDER_NAME)
if availability_zone_profile.availability_zone_data is None:
raise exceptions.InvalidOption(
value=None, option=constants.AVAILABILITY_ZONE_DATA)
# Don't allow changes to the availability_zone_data or provider_name if
# it is in use.
if (not isinstance(availability_zone_profile.availability_zone_data,
wtypes.UnsetType) or
not isinstance(availability_zone_profile.provider_name,
wtypes.UnsetType)):
if self.repositories.availability_zone.count(
context.session, availability_zone_profile_id=id) > 0:
raise exceptions.ObjectInUse(
object='Availability Zone Profile', id=id)
@wsme_pecan.wsexpose(profile_types.AvailabilityZoneProfileRootResponse,
wtypes.text, status_code=200,
body=profile_types.AvailabilityZoneProfileRootPUT)
def put(self, id, availability_zone_profile_):
"""Updates an Availability Zone Profile."""
availability_zone_profile = (
availability_zone_profile_.availability_zone_profile)
context = pecan.request.context.get('octavia_context')
self._auth_validate_action(context, context.project_id,
constants.RBAC_PUT)
self._validate_update_azp(context, id, availability_zone_profile)
if id == constants.NIL_UUID:
raise exceptions.NotFound(resource='Availability Zone Profile',
id=constants.NIL_UUID)
if not isinstance(availability_zone_profile.availability_zone_data,
wtypes.UnsetType):
# Do a basic JSON validation on the metadata
try:
availability_zone_data_dict = jsonutils.loads(
availability_zone_profile.availability_zone_data)
except Exception:
raise exceptions.InvalidOption(
value=availability_zone_profile.availability_zone_data,
option=constants.FLAVOR_DATA)
if isinstance(availability_zone_profile.provider_name,
wtypes.UnsetType):
db_availability_zone_profile = (
self._get_db_availability_zone_profile(
context.session, id))
provider_driver = db_availability_zone_profile.provider_name
else:
provider_driver = availability_zone_profile.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_availability_zone,
availability_zone_data_dict)
lock_session = db_api.get_session(autocommit=False)
try:
availability_zone_profile_dict = availability_zone_profile.to_dict(
render_unsets=False)
if availability_zone_profile_dict:
self.repositories.availability_zone_profile.update(
lock_session, id, **availability_zone_profile_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_availability_zone_profile = self._get_db_availability_zone_profile(
context.session, id)
result = self._convert_db_to_type(
db_availability_zone_profile,
profile_types.AvailabilityZoneProfileResponse)
return profile_types.AvailabilityZoneProfileRootResponse(
availability_zone_profile=result)
@wsme_pecan.wsexpose(None, wtypes.text, status_code=204)
def delete(self, availability_zone_profile_id):
"""Deletes an Availability Zone Profile"""
context = pecan.request.context.get('octavia_context')
self._auth_validate_action(context, context.project_id,
constants.RBAC_DELETE)
if availability_zone_profile_id == constants.NIL_UUID:
raise exceptions.NotFound(resource='Availability Zone Profile',
id=constants.NIL_UUID)
# Don't allow it to be deleted if it is in use by an availability zone
if self.repositories.availability_zone.count(
context.session,
availability_zone_profile_id=availability_zone_profile_id) > 0:
raise exceptions.ObjectInUse(object='Availability Zone Profile',
id=availability_zone_profile_id)
try:
self.repositories.availability_zone_profile.delete(
context.session, id=availability_zone_profile_id)
except sa_exception.NoResultFound:
raise exceptions.NotFound(resource='Availability Zone Profile',
id=availability_zone_profile_id)

View File

@ -0,0 +1,176 @@
# Copyright 2019 Verizon Media
#
# 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 api as oslo_db_api
from oslo_db import exception as odb_exceptions
from oslo_log import log as logging
from oslo_utils import excutils
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 availability_zones as availability_zone_types
from octavia.common import constants
from octavia.common import exceptions
from octavia.db import api as db_api
LOG = logging.getLogger(__name__)
class AvailabilityZonesController(base.BaseController):
RBAC_TYPE = constants.RBAC_AVAILABILITY_ZONE
def __init__(self):
super(AvailabilityZonesController, self).__init__()
@wsme_pecan.wsexpose(availability_zone_types.AvailabilityZoneRootResponse,
wtypes.text, [wtypes.text], ignore_extra_args=True)
def get_one(self, name, fields=None):
"""Gets an Availability Zone's detail."""
context = pecan.request.context.get('octavia_context')
self._auth_validate_action(context, context.project_id,
constants.RBAC_GET_ONE)
if name == constants.NIL_UUID:
raise exceptions.NotFound(resource='Availability Zone',
id=constants.NIL_UUID)
db_availability_zone = self._get_db_availability_zone(
context.session, name)
result = self._convert_db_to_type(
db_availability_zone,
availability_zone_types.AvailabilityZoneResponse)
if fields is not None:
result = self._filter_fields([result], fields)[0]
return availability_zone_types.AvailabilityZoneRootResponse(
availability_zone=result)
@wsme_pecan.wsexpose(availability_zone_types.AvailabilityZonesRootResponse,
[wtypes.text], ignore_extra_args=True)
def get_all(self, fields=None):
"""Lists all Availability Zones."""
pcontext = pecan.request.context
context = pcontext.get('octavia_context')
self._auth_validate_action(context, context.project_id,
constants.RBAC_GET_ALL)
db_availability_zones, links = (
self.repositories.availability_zone.get_all(
context.session,
pagination_helper=pcontext.get(constants.PAGINATION_HELPER)))
result = self._convert_db_to_type(
db_availability_zones,
[availability_zone_types.AvailabilityZoneResponse])
if fields is not None:
result = self._filter_fields(result, fields)
return availability_zone_types.AvailabilityZonesRootResponse(
availability_zones=result, availability_zones_links=links)
@wsme_pecan.wsexpose(availability_zone_types.AvailabilityZoneRootResponse,
body=availability_zone_types.AvailabilityZoneRootPOST,
status_code=201)
def post(self, availability_zone_):
"""Creates an Availability Zone."""
availability_zone = availability_zone_.availability_zone
context = pecan.request.context.get('octavia_context')
self._auth_validate_action(context, context.project_id,
constants.RBAC_POST)
lock_session = db_api.get_session(autocommit=False)
try:
availability_zone_dict = availability_zone.to_dict(
render_unsets=True)
db_availability_zone = self.repositories.availability_zone.create(
lock_session, **availability_zone_dict)
lock_session.commit()
except odb_exceptions.DBDuplicateEntry:
lock_session.rollback()
raise exceptions.RecordAlreadyExists(field='availability zone',
name=availability_zone.name)
except Exception:
with excutils.save_and_reraise_exception():
lock_session.rollback()
result = self._convert_db_to_type(
db_availability_zone,
availability_zone_types.AvailabilityZoneResponse)
return availability_zone_types.AvailabilityZoneRootResponse(
availability_zone=result)
@wsme_pecan.wsexpose(availability_zone_types.AvailabilityZoneRootResponse,
wtypes.text, status_code=200,
body=availability_zone_types.AvailabilityZoneRootPUT)
def put(self, name, availability_zone_):
availability_zone = availability_zone_.availability_zone
context = pecan.request.context.get('octavia_context')
self._auth_validate_action(context, context.project_id,
constants.RBAC_PUT)
if name == constants.NIL_UUID:
raise exceptions.NotFound(resource='Availability Zone',
id=constants.NIL_UUID)
lock_session = db_api.get_session(autocommit=False)
try:
availability_zone_dict = availability_zone.to_dict(
render_unsets=False)
if availability_zone_dict:
self.repositories.availability_zone.update(
lock_session, name, **availability_zone_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_availability_zone = self._get_db_availability_zone(
context.session, name)
result = self._convert_db_to_type(
db_availability_zone,
availability_zone_types.AvailabilityZoneResponse)
return availability_zone_types.AvailabilityZoneRootResponse(
availability_zone=result)
@oslo_db_api.wrap_db_retry(max_retries=5, retry_on_deadlock=True)
@wsme_pecan.wsexpose(None, wtypes.text, status_code=204)
def delete(self, availability_zone_name):
"""Deletes an Availability Zone"""
context = pecan.request.context.get('octavia_context')
self._auth_validate_action(context, context.project_id,
constants.RBAC_DELETE)
if availability_zone_name == constants.NIL_UUID:
raise exceptions.NotFound(resource='Availability Zone',
id=constants.NIL_UUID)
serial_session = db_api.get_session(autocommit=False)
serial_session.connection(
execution_options={'isolation_level': 'SERIALIZABLE'})
try:
self.repositories.availability_zone.delete(
serial_session, name=availability_zone_name)
serial_session.commit()
# Handle when load balancers still reference this availability_zone
except odb_exceptions.DBReferenceError:
serial_session.rollback()
raise exceptions.ObjectInUse(object='Availability Zone',
id=availability_zone_name)
except sa_exception.NoResultFound:
serial_session.rollback()
raise exceptions.NotFound(resource='Availability Zone',
id=availability_zone_name)
except Exception as e:
with excutils.save_and_reraise_exception():
LOG.error(
'Unknown availability_zone delete exception: %s', str(e))
serial_session.rollback()
finally:
serial_session.close()

View File

@ -119,6 +119,23 @@ class BaseController(pecan.rest.RestController):
return self._get_db_obj(session, self.repositories.flavor_profile,
data_models.FlavorProfile, id)
def _get_db_availability_zone(self, session, name):
"""Get an availability zone from the database."""
db_obj = self.repositories.availability_zone.get(session, name=name)
if not db_obj:
LOG.debug('%(obj_name)s %(name)s not found',
{'obj_name': data_models.AvailabilityZone._name(),
'name': name})
raise exceptions.NotFound(
resource=data_models.AvailabilityZone._name(), id=name)
return db_obj
def _get_db_availability_zone_profile(self, session, id):
"""Get an availability zone profile from the database."""
return self._get_db_obj(session,
self.repositories.availability_zone_profile,
data_models.AvailabilityZoneProfile, 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,

View File

@ -61,9 +61,14 @@ class ProviderController(base.BaseController):
Currently it checks if this was a flavor capabilities request and
routes the request to the FlavorCapabilitiesController.
"""
if provider and remainder and remainder[0] == 'flavor_capabilities':
if provider and remainder:
if remainder[0] == 'flavor_capabilities':
return (FlavorCapabilitiesController(provider=provider),
remainder[1:])
if remainder[0] == 'availability_zone_capabilities':
return (
AvailabilityZoneCapabilitiesController(provider=provider),
remainder[1:])
return None
@ -115,3 +120,55 @@ class FlavorCapabilitiesController(base.BaseController):
response_list = self._filter_fields(response_list, fields)
return provider_types.FlavorCapabilitiesResponse(
flavor_capabilities=response_list)
class AvailabilityZoneCapabilitiesController(base.BaseController):
RBAC_TYPE = constants.RBAC_PROVIDER_AVAILABILITY_ZONE
def __init__(self, provider):
super(AvailabilityZoneCapabilitiesController, self).__init__()
self.provider = provider
@wsme_pecan.wsexpose(provider_types.AvailabilityZoneCapabilitiesResponse,
[wtypes.text], ignore_extra_args=True,
status_code=200)
def get_all(self, fields=None):
context = pecan.request.context.get('octavia_context')
self._auth_validate_action(context, context.project_id,
constants.RBAC_GET_ALL)
self.driver = driver_factory.get_driver(self.provider)
try:
metadata_dict = (
self.driver.get_supported_availability_zone_metadata())
except driver_except.NotImplementedError as e:
LOG.warning(
'Provider %s get_supported_availability_zone_metadata() '
'reported: %s', self.provider, e.operator_fault_string)
raise exceptions.ProviderNotImplementedError(
prov=self.provider, user_msg=e.user_fault_string)
# Apply any valid filters provided as URL parameters
name_filter = None
description_filter = None
pagination_helper = pecan.request.context.get(
constants.PAGINATION_HELPER)
if pagination_helper:
name_filter = pagination_helper.params.get(constants.NAME)
description_filter = pagination_helper.params.get(
constants.DESCRIPTION)
if name_filter:
metadata_dict = {
key: value for key, value in six.iteritems(metadata_dict) if
key == name_filter}
if description_filter:
metadata_dict = {
key: value for key, value in six.iteritems(metadata_dict) if
value == description_filter}
response_list = [
provider_types.ProviderResponse(name=key, description=value) for
key, value in six.iteritems(metadata_dict)]
if fields is not None:
response_list = self._filter_fields(response_list, fields)
return provider_types.AvailabilityZoneCapabilitiesResponse(
availability_zone_capabilities=response_list)

View File

@ -0,0 +1,71 @@
# Copyright 2019 Verizon Media
#
# 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 BaseAvailabilityZoneProfileType(types.BaseType):
_type_to_model_map = {}
_child_map = {}
class AvailabilityZoneProfileResponse(BaseAvailabilityZoneProfileType):
"""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())
availability_zone_data = wtypes.wsattr(wtypes.StringType())
@classmethod
def from_data_model(cls, data_model, children=False):
availability_zone_profile = super(
AvailabilityZoneProfileResponse, cls).from_data_model(
data_model, children=children)
return availability_zone_profile
class AvailabilityZoneProfileRootResponse(types.BaseType):
availability_zone_profile = wtypes.wsattr(AvailabilityZoneProfileResponse)
class AvailabilityZoneProfilesRootResponse(types.BaseType):
availability_zone_profiles = wtypes.wsattr(
[AvailabilityZoneProfileResponse])
availability_zone_profile_links = wtypes.wsattr([types.PageType])
class AvailabilityZoneProfilePOST(BaseAvailabilityZoneProfileType):
"""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)
availability_zone_data = wtypes.wsattr(wtypes.StringType(max_length=4096),
mandatory=True)
class AvailabilityZoneProfileRootPOST(types.BaseType):
availability_zone_profile = wtypes.wsattr(AvailabilityZoneProfilePOST)
class AvailabilityZoneProfilePUT(BaseAvailabilityZoneProfileType):
"""Defines the attributes of a PUT request."""
name = wtypes.wsattr(wtypes.StringType(max_length=255))
provider_name = wtypes.wsattr(wtypes.StringType(max_length=255))
availability_zone_data = wtypes.wsattr(wtypes.StringType(max_length=4096))
class AvailabilityZoneProfileRootPUT(types.BaseType):
availability_zone_profile = wtypes.wsattr(AvailabilityZoneProfilePUT)

View File

@ -0,0 +1,69 @@
# Copyright 2019 Verizon Media
#
# 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 BaseAvailabilityZoneType(types.BaseType):
_type_to_model_map = {}
_child_map = {}
class AvailabilityZoneResponse(BaseAvailabilityZoneType):
"""Defines which attributes are to be shown on any response."""
name = wtypes.wsattr(wtypes.StringType())
description = wtypes.wsattr(wtypes.StringType())
enabled = wtypes.wsattr(bool)
availability_zone_profile_id = wtypes.wsattr(wtypes.StringType())
@classmethod
def from_data_model(cls, data_model, children=False):
availability_zone = super(
AvailabilityZoneResponse, cls).from_data_model(
data_model, children=children)
return availability_zone
class AvailabilityZoneRootResponse(types.BaseType):
availability_zone = wtypes.wsattr(AvailabilityZoneResponse)
class AvailabilityZonesRootResponse(types.BaseType):
availability_zones = wtypes.wsattr([AvailabilityZoneResponse])
availability_zones_links = wtypes.wsattr([types.PageType])
class AvailabilityZonePOST(BaseAvailabilityZoneType):
"""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)
availability_zone_profile_id = wtypes.wsattr(wtypes.UuidType(),
mandatory=True)
class AvailabilityZoneRootPOST(types.BaseType):
availability_zone = wtypes.wsattr(AvailabilityZonePOST)
class AvailabilityZonePUT(BaseAvailabilityZoneType):
"""Defines the attributes of a PUT request."""
description = wtypes.wsattr(wtypes.StringType(max_length=255))
enabled = wtypes.wsattr(bool)
class AvailabilityZoneRootPUT(types.BaseType):
availability_zone = wtypes.wsattr(AvailabilityZonePUT)

View File

@ -28,3 +28,7 @@ class ProvidersRootResponse(types.BaseType):
class FlavorCapabilitiesResponse(types.BaseType):
flavor_capabilities = wtypes.wsattr([ProviderResponse])
class AvailabilityZoneCapabilitiesResponse(types.BaseType):
availability_zone_capabilities = wtypes.wsattr([ProviderResponse])

View File

@ -302,6 +302,7 @@ CLIENT_CA_TLS_CERTIFICATE_ID = 'client_ca_tls_certificate_id'
CLIENT_CRL_CONTAINER_ID = 'client_crl_container_id'
COMPUTE_ID = 'compute_id'
COMPUTE_OBJ = 'compute_obj'
COMPUTE_ZONE = 'compute_zone'
CONN_MAX_RETRIES = 'conn_max_retries'
CONN_RETRY_INTERVAL = 'conn_retry_interval'
CREATED_AT = 'created_at'
@ -333,6 +334,7 @@ LOADBALANCER = 'loadbalancer'
LOADBALANCER_ID = 'loadbalancer_id'
LOAD_BALANCER_ID = 'load_balancer_id'
LOAD_BALANCER_UPDATES = 'load_balancer_updates'
MANAGEMENT_NETWORK = 'management_network'
MEMBER = 'member'
MEMBER_ID = 'member_id'
MEMBER_PORTS = 'member_ports'
@ -648,8 +650,13 @@ RBAC_QUOTA = '{}:quota:'.format(LOADBALANCER_API)
RBAC_AMPHORA = '{}:amphora:'.format(LOADBALANCER_API)
RBAC_PROVIDER = '{}:provider:'.format(LOADBALANCER_API)
RBAC_PROVIDER_FLAVOR = '{}:provider-flavor:'.format(LOADBALANCER_API)
RBAC_PROVIDER_AVAILABILITY_ZONE = '{}:provider-availability-zone:'.format(
LOADBALANCER_API)
RBAC_FLAVOR = '{}:flavor:'.format(LOADBALANCER_API)
RBAC_FLAVOR_PROFILE = '{}:flavor-profile:'.format(LOADBALANCER_API)
RBAC_AVAILABILITY_ZONE = '{}:availability-zone:'.format(LOADBALANCER_API)
RBAC_AVAILABILITY_ZONE_PROFILE = '{}:availability-zone-profile:'.format(
LOADBALANCER_API)
RBAC_POST = 'post'
RBAC_PUT = 'put'
RBAC_PUT_CONFIG = 'put_config'
@ -675,10 +682,12 @@ AMP_NETNS_SVC_PREFIX = 'amphora-netns'
# Amphora Feature Compatibility
HTTP_REUSE = 'has_http_reuse'
# TODO(johnsom) convert this to octavia_lib constant flavor
# TODO(johnsom) convert these to octavia_lib constants
# once octavia is transitioned to use octavia_lib
FLAVOR = 'flavor'
FLAVOR_DATA = 'flavor_data'
AVAILABILITY_ZONE = 'availability_zone'
AVAILABILITY_ZONE_DATA = 'availability_zone_data'
# Flavor metadata
LOADBALANCER_TOPOLOGY = 'loadbalancer_topology'

View File

@ -781,6 +781,26 @@ class FlavorProfile(BaseDataModel):
self.flavor_data = flavor_data
class AvailabilityZone(BaseDataModel):
def __init__(self, name=None, description=None, enabled=None,
availability_zone_profile_id=None):
self.name = name
self.description = description
self.enabled = enabled
self.availability_zone_profile_id = availability_zone_profile_id
class AvailabilityZoneProfile(BaseDataModel):
def __init__(self, id=None, name=None, provider_name=None,
availability_zone_data=None):
self.id = id
self.name = name
self.provider_name = provider_name
self.availability_zone_data = availability_zone_data
class ListenerCidr(BaseDataModel):
def __init__(self, listener_id=None, cidr=None):

View File

@ -123,3 +123,13 @@ class ComputeBase(object):
:raises: NotFound
:raises: NotImplementedError
"""
@abc.abstractmethod
def validate_availability_zone(self, availability_zone):
"""Validates that a compute availability zone exists.
:param availability_zone: Name of the compute availability zone.
:return: None
:raises: NotFound
:raises: NotImplementedError
"""

View File

@ -111,6 +111,12 @@ class NoopManager(object):
self.__class__.__name__, flavor_id)
self.computeconfig[flavor_id] = (flavor_id, 'validate_flavor')
def validate_availability_zone(self, availability_zone):
LOG.debug("Compute %s no-op, validate_availability_zone name %s",
self.__class__.__name__, availability_zone)
self.computeconfig[availability_zone] = (
availability_zone, 'validate_availability_zone')
class NoopComputeDriver(driver_base.ComputeBase):
def __init__(self):
@ -155,3 +161,6 @@ class NoopComputeDriver(driver_base.ComputeBase):
def validate_flavor(self, flavor_id):
self.driver.validate_flavor(flavor_id)
def validate_availability_zone(self, availability_zone):
self.driver.validate_availability_zone(availability_zone)

View File

@ -89,6 +89,7 @@ class VirtualMachineManager(compute_base.ComputeBase):
self.manager = self._nova_client.servers
self.server_groups = self._nova_client.server_groups
self.flavor_manager = self._nova_client.flavors
self.availability_zone_manager = self._nova_client.availability_zones
self.volume_driver = stevedore_driver.DriverManager(
namespace='octavia.volume.drivers',
name=CONF.controller_worker.volume_driver,
@ -398,3 +399,24 @@ class VirtualMachineManager(compute_base.ComputeBase):
LOG.exception('Nova reports a failure getting flavor details for '
'flavor ID %s: %s', flavor_id, e)
raise
def validate_availability_zone(self, availability_zone):
"""Validates that an availability zone exists in nova.
:param availability_zone: Name of the availability zone to lookup.
:raises: NotFound
:returns: None
"""
try:
compute_zones = [
a.zoneName for a in self.availability_zone_manager.list(
detailed=False)]
if availability_zone not in compute_zones:
LOG.info('Availability zone %s was not found in nova. %s',
availability_zone, compute_zones)
raise exceptions.InvalidSubresource(
resource='Nova availability zone', id=availability_zone)
except Exception as e:
LOG.exception('Nova reports a failure getting listing '
'availability zones: %s', e)
raise

View File

@ -31,7 +31,8 @@ class OctaviaBase(models.ModelBase):
# objects.
if obj.__class__.__name__ in ['Member', 'Pool', 'LoadBalancer',
'Listener', 'Amphora', 'L7Policy',
'L7Rule', 'Flavor', 'FlavorProfile']:
'L7Rule', 'Flavor', 'FlavorProfile',
'AvailabilityZoneProfile']:
return obj.__class__.__name__ + obj.id
if obj.__class__.__name__ in ['SessionPersistence', 'HealthMonitor']:
return obj.__class__.__name__ + obj.pool_id
@ -48,6 +49,8 @@ class OctaviaBase(models.ModelBase):
obj.listener_id + obj.tls_container_id)
if obj.__class__.__name__ in ['Quotas']:
return obj.__class__.__name__ + obj.project_id
if obj.__class__.__name__ in ['AvailabilityZone']:
return obj.__class__.__name__ + obj.name
raise NotImplementedError
def to_data_model(self, _graph_nodes=None):

View File

@ -0,0 +1,71 @@
# 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 availability_zone table
Revision ID: c761c8a71579
Revises: e37941b010db
Create Date: 2019-11-11 18:53:15.428386
"""
from alembic import op
import sqlalchemy as sa
from octavia.common import constants
# revision identifiers, used by Alembic.
revision = 'c761c8a71579'
down_revision = 'e37941b010db'
def upgrade():
azp_table = op.create_table(
u'availability_zone_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'availability_zone_data', sa.String(4096), nullable=False),
sa.PrimaryKeyConstraint(u'id'))
op.bulk_insert(
azp_table,
[
{'id': constants.NIL_UUID, 'name': 'DELETED-PLACEHOLDER',
'provider_name': 'DELETED', 'availability_zone_data': '{}'},
]
)
az_table = op.create_table(
u'availability_zone',
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'availability_zone_profile_id', sa.String(36),
nullable=False),
sa.ForeignKeyConstraint([u'availability_zone_profile_id'],
[u'availability_zone_profile.id'],
name=u'fk_az_az_profile_id'),
sa.PrimaryKeyConstraint(u'name'),)
op.bulk_insert(
az_table,
[
{'name': constants.NIL_UUID,
'description': 'Placeholder for DELETED LBs with DELETED '
'availability zones',
'enabled': False,
'availability_zone_profile_id': constants.NIL_UUID}
]
)

View File

@ -23,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 availability_zone_profile
from octavia.api.v2.types import availability_zones
from octavia.api.v2.types import flavor_profile
from octavia.api.v2.types import flavors
from octavia.api.v2.types import health_monitor
@ -782,6 +784,41 @@ class Flavor(base_models.BASE,
nullable=False)
class AvailabilityZoneProfile(base_models.BASE, base_models.IdMixin,
base_models.NameMixin):
__data_model__ = data_models.AvailabilityZoneProfile
__tablename__ = "availability_zone_profile"
__v2_wsme__ = availability_zone_profile.AvailabilityZoneProfileResponse
provider_name = sa.Column(sa.String(255), nullable=False)
availability_zone_data = sa.Column(sa.String(4096), nullable=False)
class AvailabilityZone(base_models.BASE,
base_models.NameMixin):
__data_model__ = data_models.AvailabilityZone
__tablename__ = "availability_zone"
__v2_wsme__ = availability_zones.AvailabilityZoneResponse
__table_args__ = (
sa.PrimaryKeyConstraint('name'),
)
description = sa.Column(sa.String(255), nullable=True)
enabled = sa.Column(sa.Boolean(), nullable=False)
availability_zone_profile_id = sa.Column(
sa.String(36),
sa.ForeignKey("availability_zone_profile.id",
name="fk_az_az_profile_id"),
nullable=False)
class ClientAuthenticationMode(base_models.BASE):
__tablename__ = "client_authentication_mode"

View File

@ -228,6 +228,8 @@ class Repositories(object):
self.flavor = FlavorRepository()
self.flavor_profile = FlavorProfileRepository()
self.spares_pool = SparesPoolRepository()
self.availability_zone = AvailabilityZoneRepository()
self.availability_zone_profile = AvailabilityZoneProfileRepository()
def create_load_balancer_and_vip(self, session, lb_dict, vip_dict):
"""Inserts load balancer and vip entities into the database.
@ -1828,7 +1830,10 @@ class _GetALLExceptDELETEDIdMixin(object):
if query_options:
query = query.options(query_options)
if hasattr(self.model_class, 'id'):
query = query.filter(self.model_class.id != consts.NIL_UUID)
else:
query = query.filter(self.model_class.name != consts.NIL_UUID)
if pagination_helper:
model_list, links = pagination_helper.apply(
@ -1898,3 +1903,69 @@ class SparesPoolRepository(BaseRepository):
"""
row = lock_session.query(models.SparesPool).with_for_update().one()
return row
class AvailabilityZoneRepository(_GetALLExceptDELETEDIdMixin, BaseRepository):
model_class = models.AvailabilityZone
def get_availability_zone_metadata_dict(self, session,
availability_zone_name):
with session.begin(subtransactions=True):
availability_zone_metadata_json = (
session.query(
models.AvailabilityZoneProfile.availability_zone_data)
.filter(models.AvailabilityZone.name == availability_zone_name)
.filter(models.AvailabilityZone.availability_zone_profile_id ==
models.AvailabilityZoneProfile.id)
.one()[0])
result_dict = (
{} if availability_zone_metadata_json is None
else jsonutils.loads(availability_zone_metadata_json))
return result_dict
def get_availability_zone_provider(self, session, availability_zone_name):
with session.begin(subtransactions=True):
return (session.query(models.AvailabilityZoneProfile.provider_name)
.filter(
models.AvailabilityZone.name == availability_zone_name)
.filter(
models.AvailabilityZone.availability_zone_profile_id ==
models.AvailabilityZoneProfile.id).one()[0])
def update(self, session, name, **model_kwargs):
"""Updates an entity in the database.
:param session: A Sql Alchemy database session.
:param model_kwargs: Entity attributes that should be updates.
:returns: octavia.common.data_model
"""
with session.begin(subtransactions=True):
session.query(self.model_class).filter_by(
name=name).update(model_kwargs)
def delete(self, serial_session, **filters):
"""Special delete method for availability_zone.
Sets DELETED LBs availability_zone_id to NIL_UUID, then removes the
availability_zone.
:param serial_session: A Sql Alchemy database transaction session.
:param filters: Filters to decide which entity should be deleted.
:returns: None
:raises: odb_exceptions.DBReferenceError
:raises: sqlalchemy.orm.exc.NoResultFound
"""
# TODO(sorrison): Uncomment this
# (serial_session.query(models.LoadBalancer).
# filter(models.LoadBalancer.availability_zone_id == filters['id']).
# filter(models.LoadBalancer.provisioning_status == consts.DELETED).
# update({models.LoadBalancer.availability_zone_id: consts.NIL_UUID},
# synchronize_session=False))
availability_zone = (
serial_session.query(self.model_class).filter_by(**filters).one())
serial_session.delete(availability_zone)
class AvailabilityZoneProfileRepository(_GetALLExceptDELETEDIdMixin,
BaseRepository):
model_class = models.AvailabilityZoneProfile

View File

@ -14,6 +14,8 @@
import itertools
from octavia.policies import amphora
from octavia.policies import availability_zone
from octavia.policies import availability_zone_profile
from octavia.policies import base
from octavia.policies import flavor
from octavia.policies import flavor_profile
@ -25,6 +27,7 @@ from octavia.policies import loadbalancer
from octavia.policies import member
from octavia.policies import pool
from octavia.policies import provider
from octavia.policies import provider_availability_zone
from octavia.policies import provider_flavor
from octavia.policies import quota
@ -34,6 +37,8 @@ def list_rules():
base.list_rules(),
flavor.list_rules(),
flavor_profile.list_rules(),
availability_zone.list_rules(),
availability_zone_profile.list_rules(),
healthmonitor.list_rules(),
l7policy.list_rules(),
l7rule.list_rules(),
@ -45,4 +50,5 @@ def list_rules():
quota.list_rules(),
amphora.list_rules(),
provider_flavor.list_rules(),
provider_availability_zone.list_rules(),
)

View File

@ -0,0 +1,62 @@
# Copyright 2019 Verizon Media
# 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_AVAILABILITY_ZONE,
action=constants.RBAC_GET_ALL),
constants.RULE_API_READ,
"List Availability Zones",
[{'method': 'GET', 'path': '/v2.0/lbaas/availabilityzones'}]
),
policy.DocumentedRuleDefault(
'{rbac_obj}{action}'.format(rbac_obj=constants.RBAC_AVAILABILITY_ZONE,
action=constants.RBAC_POST),
constants.RULE_API_ADMIN,
"Create an Availability Zone",
[{'method': 'POST', 'path': '/v2.0/lbaas/availabilityzones'}]
),
policy.DocumentedRuleDefault(
'{rbac_obj}{action}'.format(rbac_obj=constants.RBAC_AVAILABILITY_ZONE,
action=constants.RBAC_PUT),
constants.RULE_API_ADMIN,
"Update an Availability Zone",
[{'method': 'PUT',
'path': '/v2.0/lbaas/availabilityzones/{availability_zone_id}'}]
),
policy.DocumentedRuleDefault(
'{rbac_obj}{action}'.format(rbac_obj=constants.RBAC_AVAILABILITY_ZONE,
action=constants.RBAC_GET_ONE),
constants.RULE_API_READ,
"Show Availability Zone details",
[{'method': 'GET',
'path': '/v2.0/lbaas/availabilityzones/{availability_zone_id}'}]
),
policy.DocumentedRuleDefault(
'{rbac_obj}{action}'.format(rbac_obj=constants.RBAC_AVAILABILITY_ZONE,
action=constants.RBAC_DELETE),
constants.RULE_API_ADMIN,
"Remove an Availability Zone",
[{'method': 'DELETE',
'path': '/v2.0/lbaas/availabilityzones/{availability_zone_id}'}]
),
]
def list_rules():
return rules

View File

@ -0,0 +1,70 @@
# Copyright 2019 Verizon Media
# 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_AVAILABILITY_ZONE_PROFILE,
action=constants.RBAC_GET_ALL),
constants.RULE_API_ADMIN,
"List Availability Zones",
[{'method': 'GET', 'path': '/v2.0/lbaas/availabilityzoneprofiles'}]
),
policy.DocumentedRuleDefault(
'{rbac_obj}{action}'.format(
rbac_obj=constants.RBAC_AVAILABILITY_ZONE_PROFILE,
action=constants.RBAC_POST),
constants.RULE_API_ADMIN,
"Create an Availability Zone",
[{'method': 'POST', 'path': '/v2.0/lbaas/availabilityzoneprofiles'}]
),
policy.DocumentedRuleDefault(
'{rbac_obj}{action}'.format(
rbac_obj=constants.RBAC_AVAILABILITY_ZONE_PROFILE,
action=constants.RBAC_PUT),
constants.RULE_API_ADMIN,
"Update an Availability Zone",
[{'method': 'PUT',
'path': '/v2.0/lbaas/availabilityzoneprofiles/'
'{availability_zone_profile_id}'}]
),
policy.DocumentedRuleDefault(
'{rbac_obj}{action}'.format(
rbac_obj=constants.RBAC_AVAILABILITY_ZONE_PROFILE,
action=constants.RBAC_GET_ONE),
constants.RULE_API_ADMIN,
"Show Availability Zone details",
[{'method': 'GET',
'path': '/v2.0/lbaas/availabilityzoneprofiles/'
'{availability_zone_profile_id}'}]
),
policy.DocumentedRuleDefault(
'{rbac_obj}{action}'.format(
rbac_obj=constants.RBAC_AVAILABILITY_ZONE_PROFILE,
action=constants.RBAC_DELETE),
constants.RULE_API_ADMIN,
"Remove an Availability Zone",
[{'method': 'DELETE',
'path': '/v2.0/lbaas/availabilityzoneprofiles/'
'{availability_zone_profile_id}'}]
),
]
def list_rules():
return rules

View File

@ -50,7 +50,7 @@ rules = [
'{rbac_obj}{action}'.format(rbac_obj=constants.RBAC_FLAVOR,
action=constants.RBAC_DELETE),
constants.RULE_API_ADMIN,
"Remove a flavor",
"Remove a Flavor",
[{'method': 'DELETE',
'path': '/v2.0/lbaas/flavors/{flavor_id}'}]
),

View File

@ -21,21 +21,21 @@ rules = [
'{rbac_obj}{action}'.format(rbac_obj=constants.RBAC_FLAVOR_PROFILE,
action=constants.RBAC_GET_ALL),
constants.RULE_API_ADMIN,
"List Flavors",
"List Flavor Profiles",
[{'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",
"Create a Flavor Profile",
[{'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",
"Update a Flavor Profile",
[{'method': 'PUT',
'path': '/v2.0/lbaas/flavorprofiles/{flavor_profile_id}'}]
),
@ -43,7 +43,7 @@ rules = [
'{rbac_obj}{action}'.format(rbac_obj=constants.RBAC_FLAVOR_PROFILE,
action=constants.RBAC_GET_ONE),
constants.RULE_API_ADMIN,
"Show Flavor details",
"Show Flavor Profile details",
[{'method': 'GET',
'path': '/v2.0/lbaas/flavorprofiles/{flavor_profile_id}'}]
),
@ -51,7 +51,7 @@ rules = [
'{rbac_obj}{action}'.format(rbac_obj=constants.RBAC_FLAVOR_PROFILE,
action=constants.RBAC_DELETE),
constants.RULE_API_ADMIN,
"Remove a flavor",
"Remove a Flavor Profile",
[{'method': 'DELETE',
'path': '/v2.0/lbaas/flavorprofiles/{flavor_profile_id}'}]
),

View File

@ -0,0 +1,33 @@
# Copyright 2018 Rackspace, US 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_PROVIDER_AVAILABILITY_ZONE,
action=constants.RBAC_GET_ALL),
constants.RULE_API_ADMIN,
"List the provider availability zone capabilities.",
[{'method': 'GET',
'path': '/v2/lbaas/providers/{provider}/'
'availability_zone_capabilities'}]
),
]
def list_rules():
return rules

View File

@ -22,7 +22,7 @@ rules = [
constants.RULE_API_ADMIN,
"List the provider flavor capabilities.",
[{'method': 'GET',
'path': '/v2/lbaas/providers/{provider}/capabilities'}]
'path': '/v2/lbaas/providers/{provider}/flavor_capabilities'}]
),
]

View File

@ -43,7 +43,7 @@ class TestRootController(base_db_test.OctaviaDBTestBase):
def test_api_versions(self):
versions = self._get_versions_with_config()
version_ids = tuple(v.get('id') for v in versions)
self.assertEqual(14, len(version_ids))
self.assertEqual(15, len(version_ids))
self.assertIn('v2.0', version_ids)
self.assertIn('v2.1', version_ids)
self.assertIn('v2.2', version_ids)
@ -58,6 +58,7 @@ class TestRootController(base_db_test.OctaviaDBTestBase):
self.assertIn('v2.11', version_ids)
self.assertIn('v2.12', version_ids)
self.assertIn('v2.13', version_ids)
self.assertIn('v2.14', version_ids)
# Each version should have a 'self' 'href' to the API version URL
# [{u'rel': u'self', u'href': u'http://localhost/v2'}]

View File

@ -40,6 +40,14 @@ class BaseAPITest(base_db_test.OctaviaDBTestBase):
FPS_PATH = '/flavorprofiles'
FP_PATH = FPS_PATH + '/{fp_id}'
# /lbaas/availabilityzones
AZS_PATH = '/availabilityzones'
AZ_PATH = AZS_PATH + '/{az_name}'
# /lbaas/availabilityzoneprofiles
AZPS_PATH = '/availabilityzoneprofiles'
AZP_PATH = AZPS_PATH + '/{azp_id}'
# /lbaas/loadbalancers
LBS_PATH = '/lbaas/loadbalancers'
LB_PATH = LBS_PATH + '/{lb_id}'
@ -80,8 +88,10 @@ class BaseAPITest(base_db_test.OctaviaDBTestBase):
AMPHORA_CONFIG_PATH = AMPHORA_PATH + '/config'
PROVIDERS_PATH = '/lbaas/providers'
FLAVOR_CAPABILITIES_PATH = (PROVIDERS_PATH +
'/{provider}/flavor_capabilities')
FLAVOR_CAPABILITIES_PATH = (
PROVIDERS_PATH + '/{provider}/flavor_capabilities')
AVAILABILITY_ZONE_CAPABILITIES_PATH = (
PROVIDERS_PATH + '/{provider}/availability_zone_capabilities')
NOT_AUTHORIZED_BODY = {
'debuginfo': None, 'faultcode': 'Client',
@ -205,13 +215,31 @@ class BaseAPITest(base_db_test.OctaviaDBTestBase):
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,
def create_flavor_profile(self, name, provider_name, flavor_data):
req_dict = {'name': name, 'provider_name': provider_name,
constants.FLAVOR_DATA: flavor_data}
body = {'flavorprofile': req_dict}
response = self.post(self.FPS_PATH, body)
return response.json.get('flavorprofile')
def create_availability_zone(self, name, description,
availability_zone_profile_id, enabled):
req_dict = {
'name': name, 'description': description,
'availability_zone_profile_id': availability_zone_profile_id,
'enabled': enabled}
body = {'availability_zone': req_dict}
response = self.post(self.AZS_PATH, body)
return response.json.get('availability_zone')
def create_availability_zone_profile(self, name, provider_name,
availability_zone_data):
req_dict = {'name': name, 'provider_name': provider_name,
constants.AVAILABILITY_ZONE_DATA: availability_zone_data}
body = {'availability_zone_profile': req_dict}
response = self.post(self.AZPS_PATH, body)
return response.json.get('availability_zone_profile')
def create_load_balancer(self, vip_subnet_id,
**optionals):
req_dict = {'vip_subnet_id': vip_subnet_id,

View File

@ -0,0 +1,589 @@
# Copyright 2019 Verizon Media
#
# 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 TestAvailabilityZoneProfiles(base.BaseAPITest):
root_tag = 'availability_zone_profile'
root_tag_list = 'availability_zone_profiles'
root_tag_links = 'availability_zone_profile_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(constants.PROVIDER_NAME),
resp.get(constants.PROVIDER_NAME))
self.assertEqual(req.get(constants.AVAILABILITY_ZONE_DATA),
resp.get(constants.AVAILABILITY_ZONE_DATA))
def test_empty_list(self):
response = self.get(self.AZPS_PATH)
api_list = response.json.get(self.root_tag_list)
self.assertEqual([], api_list)
def test_create(self):
az_json = {'name': 'test1', constants.PROVIDER_NAME: 'noop_driver',
constants.AVAILABILITY_ZONE_DATA: '{"hello": "world"}'}
body = self._build_body(az_json)
response = self.post(self.AZPS_PATH, body)
api_azp = response.json.get(self.root_tag)
self._assert_request_matches_response(az_json, api_azp)
def test_create_with_missing_name(self):
az_json = {constants.PROVIDER_NAME: 'pr1',
constants.AVAILABILITY_ZONE_DATA: '{"x": "y"}'}
body = self._build_body(az_json)
response = self.post(self.AZPS_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):
az_json = {'name': 'xyz',
constants.AVAILABILITY_ZONE_DATA: '{"x": "y"}'}
body = self._build_body(az_json)
response = self.post(self.AZPS_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_availability_zone_data(self):
az_json = {'name': 'xyz', constants.PROVIDER_NAME: 'pr1'}
body = self._build_body(az_json)
response = self.post(self.AZPS_PATH, body, status=400)
err_msg = ("Invalid input for field/attribute availability_zone_data. "
"Value: 'None'. Mandatory field missing.")
self.assertEqual(err_msg, response.json.get('faultstring'))
def test_create_with_empty_availability_zone_data(self):
az_json = {'name': 'test1', constants.PROVIDER_NAME: 'noop_driver',
constants.AVAILABILITY_ZONE_DATA: '{}'}
body = self._build_body(az_json)
response = self.post(self.AZPS_PATH, body)
api_azp = response.json.get(self.root_tag)
self._assert_request_matches_response(az_json, api_azp)
def test_create_with_long_name(self):
az_json = {'name': 'n' * 256, constants.PROVIDER_NAME: 'test1',
constants.AVAILABILITY_ZONE_DATA: '{"hello": "world"}'}
body = self._build_body(az_json)
self.post(self.AZPS_PATH, body, status=400)
def test_create_with_long_provider(self):
az_json = {'name': 'name1', constants.PROVIDER_NAME: 'n' * 256,
constants.AVAILABILITY_ZONE_DATA: '{"hello": "world"}'}
body = self._build_body(az_json)
self.post(self.AZPS_PATH, body, status=400)
def test_create_with_long_availability_zone_data(self):
az_json = {'name': 'name1', constants.PROVIDER_NAME: 'amp',
constants.AVAILABILITY_ZONE_DATA: 'n' * 4097}
body = self._build_body(az_json)
self.post(self.AZPS_PATH, body, status=400)
def test_create_authorized(self):
az_json = {'name': 'test1', constants.PROVIDER_NAME: 'noop_driver',
constants.AVAILABILITY_ZONE_DATA: '{"hello": "world"}'}
body = self._build_body(az_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.AZPS_PATH, body)
self.conf.config(group='api_settings', auth_strategy=auth_strategy)
api_azp = response.json.get(self.root_tag)
self._assert_request_matches_response(az_json, api_azp)
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)
az_json = {'name': 'name',
constants.PROVIDER_NAME: 'xyz',
constants.AVAILABILITY_ZONE_DATA: '{"x": "y"}'}
body = self._build_body(az_json)
response = self.post(self.AZPS_PATH, body, status=403)
api_azp = response.json
self.conf.config(group='api_settings', auth_strategy=auth_strategy)
self.assertEqual(self.NOT_AUTHORIZED_BODY, api_azp)
def test_create_db_failure(self):
az_json = {'name': 'test1', constants.PROVIDER_NAME: 'noop_driver',
constants.AVAILABILITY_ZONE_DATA: '{"hello": "world"}'}
body = self._build_body(az_json)
with mock.patch(
"octavia.db.repositories.AvailabilityZoneProfileRepository."
"create") as mock_create:
mock_create.side_effect = Exception
self.post(self.AZPS_PATH, body, status=500)
mock_create.side_effect = odb_exceptions.DBDuplicateEntry
self.post(self.AZPS_PATH, body, status=409)
def test_create_with_invalid_json(self):
az_json = {'name': 'test1', constants.PROVIDER_NAME: 'noop_driver',
constants.AVAILABILITY_ZONE_DATA: '{hello: "world"}'}
body = self._build_body(az_json)
self.post(self.AZPS_PATH, body, status=400)
def test_get(self):
azp = self.create_availability_zone_profile(
'name', 'noop_driver', '{"x": "y"}')
self.assertTrue(uuidutils.is_uuid_like(azp.get('id')))
response = self.get(
self.AZP_PATH.format(
azp_id=azp.get('id'))).json.get(self.root_tag)
self.assertEqual('name', response.get('name'))
self.assertEqual(azp.get('id'), response.get('id'))
def test_get_one_deleted_id(self):
response = self.get(self.AZP_PATH.format(azp_id=constants.NIL_UUID),
status=404)
self.assertEqual('Availability Zone Profile {} not found.'.format(
constants.NIL_UUID), response.json.get('faultstring'))
def test_get_one_fields_filter(self):
azp = self.create_availability_zone_profile(
'name', 'noop_driver', '{"x": "y"}')
self.assertTrue(uuidutils.is_uuid_like(azp.get('id')))
response = self.get(
self.AZP_PATH.format(azp_id=azp.get('id')), params={
'fields': ['id', constants.PROVIDER_NAME]}
).json.get(self.root_tag)
self.assertEqual(azp.get('id'), response.get('id'))
self.assertIn(u'id', response)
self.assertIn(constants.PROVIDER_NAME, response)
self.assertNotIn(u'name', response)
self.assertNotIn(constants.AVAILABILITY_ZONE_DATA, response)
def test_get_authorized(self):
azp = self.create_availability_zone_profile(
'name', 'noop_driver', '{"x": "y"}')
self.assertTrue(uuidutils.is_uuid_like(azp.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.AZP_PATH.format(
azp_id=azp.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(azp.get('id'), response.get('id'))
def test_get_not_authorized(self):
azp = self.create_availability_zone_profile(
'name', 'noop_driver', '{"x": "y"}')
self.assertTrue(uuidutils.is_uuid_like(azp.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.AZP_PATH.format(azp_id=azp.get('id')), status=403)
self.conf.config(group='api_settings', auth_strategy=auth_strategy)
def test_get_all(self):
fp1 = self.create_availability_zone_profile(
'test1', 'noop_driver', '{"compute_zone": "my_az_1"}')
ref_fp_1 = {u'availability_zone_data': u'{"compute_zone": "my_az_1"}',
u'id': fp1.get('id'), u'name': u'test1',
constants.PROVIDER_NAME: u'noop_driver'}
self.assertTrue(uuidutils.is_uuid_like(fp1.get('id')))
fp2 = self.create_availability_zone_profile(
'test2', 'noop_driver-alt', '{"compute_zone": "my_az_1"}')
ref_fp_2 = {u'availability_zone_data': u'{"compute_zone": "my_az_1"}',
u'id': fp2.get('id'), u'name': u'test2',
constants.PROVIDER_NAME: u'noop_driver-alt'}
self.assertTrue(uuidutils.is_uuid_like(fp2.get('id')))
response = self.get(self.AZPS_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_availability_zone_profile(
'test1', 'noop_driver', '{"compute_zone": "my_az_1"}')
self.assertTrue(uuidutils.is_uuid_like(fp1.get('id')))
fp2 = self.create_availability_zone_profile(
'test2', 'noop_driver-alt', '{"compute_zone": "my_az_1"}')
self.assertTrue(uuidutils.is_uuid_like(fp2.get('id')))
response = self.get(self.AZPS_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(constants.PROVIDER_NAME, profile)
self.assertNotIn(constants.AVAILABILITY_ZONE_DATA, profile)
def test_get_all_authorized(self):
fp1 = self.create_availability_zone_profile(
'test1', 'noop_driver', '{"compute_zone": "my_az_1"}')
self.assertTrue(uuidutils.is_uuid_like(fp1.get('id')))
fp2 = self.create_availability_zone_profile(
'test2', 'noop_driver-alt', '{"compute_zone": "my_az_1"}')
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 = {
'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.AZPS_PATH)
self.conf.config(group='api_settings', auth_strategy=auth_strategy)
api_list = response.json.get(self.root_tag_list)
self.assertEqual(2, len(api_list))
def test_get_all_not_authorized(self):
fp1 = self.create_availability_zone_profile(
'test1', 'noop_driver', '{"compute_zone": "my_az_1"}')
self.assertTrue(uuidutils.is_uuid_like(fp1.get('id')))
fp2 = self.create_availability_zone_profile(
'test2', 'noop_driver-alt', '{"compute_zone": "my_az_1"}')
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)
self.get(self.AZPS_PATH, status=403)
self.conf.config(group='api_settings', auth_strategy=auth_strategy)
def test_update(self):
azp = self.create_availability_zone_profile(
'test_profile', 'noop_driver', '{"x": "y"}')
update_data = {'name': 'the_profile',
constants.PROVIDER_NAME: 'noop_driver-alt',
constants.AVAILABILITY_ZONE_DATA: '{"hello": "world"}'}
body = self._build_body(update_data)
self.put(self.AZP_PATH.format(azp_id=azp.get('id')), body)
response = self.get(
self.AZP_PATH.format(azp_id=azp.get('id'))).json.get(self.root_tag)
self.assertEqual('the_profile', response.get('name'))
self.assertEqual('noop_driver-alt',
response.get(constants.PROVIDER_NAME))
self.assertEqual('{"hello": "world"}',
response.get(constants.AVAILABILITY_ZONE_DATA))
def test_update_deleted_id(self):
update_data = {'name': 'fake_profile'}
body = self._build_body(update_data)
response = self.put(self.AZP_PATH.format(azp_id=constants.NIL_UUID),
body, status=404)
self.assertEqual('Availability Zone Profile {} not found.'.format(
constants.NIL_UUID), response.json.get('faultstring'))
def test_update_nothing(self):
azp = self.create_availability_zone_profile(
'test_profile', 'noop_driver', '{"x": "y"}')
body = self._build_body({})
self.put(self.AZP_PATH.format(azp_id=azp.get('id')), body)
response = self.get(
self.AZP_PATH.format(azp_id=azp.get('id'))).json.get(self.root_tag)
self.assertEqual('test_profile', response.get('name'))
self.assertEqual('noop_driver', response.get(constants.PROVIDER_NAME))
self.assertEqual('{"x": "y"}',
response.get(constants.AVAILABILITY_ZONE_DATA))
def test_update_name_none(self):
self._test_update_param_none(constants.NAME)
def test_update_provider_name_none(self):
self._test_update_param_none(constants.PROVIDER_NAME)
def test_update_availability_zone_data_none(self):
self._test_update_param_none(constants.AVAILABILITY_ZONE_DATA)
def _test_update_param_none(self, param_name):
azp = self.create_availability_zone_profile(
'test_profile', 'noop_driver', '{"x": "y"}')
expect_error_msg = ("None is not a valid option for %s" %
param_name)
body = self._build_body({param_name: None})
response = self.put(self.AZP_PATH.format(azp_id=azp.get('id')), body,
status=400)
self.assertEqual(expect_error_msg, response.json['faultstring'])
def test_update_no_availability_zone_data(self):
azp = self.create_availability_zone_profile(
'test_profile', 'noop_driver', '{"x": "y"}')
update_data = {'name': 'the_profile',
constants.PROVIDER_NAME: 'noop_driver-alt'}
body = self._build_body(update_data)
response = self.put(self.AZP_PATH.format(azp_id=azp.get('id')), body)
response = self.get(
self.AZP_PATH.format(azp_id=azp.get('id'))).json.get(self.root_tag)
self.assertEqual('the_profile', response.get('name'))
self.assertEqual('noop_driver-alt',
response.get(constants.PROVIDER_NAME))
self.assertEqual('{"x": "y"}',
response.get(constants.AVAILABILITY_ZONE_DATA))
def test_update_authorized(self):
azp = self.create_availability_zone_profile(
'test_profile', 'noop_driver', '{"x": "y"}')
update_data = {'name': 'the_profile',
constants.PROVIDER_NAME: 'noop_driver-alt',
constants.AVAILABILITY_ZONE_DATA: '{"hello": "world"}'}
body = self._build_body(update_data)
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.put(self.AZP_PATH.format(azp_id=azp.get('id')),
body)
self.conf.config(group='api_settings', auth_strategy=auth_strategy)
response = self.get(
self.AZP_PATH.format(azp_id=azp.get('id'))).json.get(self.root_tag)
self.assertEqual('the_profile', response.get('name'))
self.assertEqual('noop_driver-alt',
response.get(constants.PROVIDER_NAME))
self.assertEqual('{"hello": "world"}',
response.get(constants.AVAILABILITY_ZONE_DATA))
def test_update_not_authorized(self):
azp = self.create_availability_zone_profile(
'test_profile', 'noop_driver', '{"x": "y"}')
update_data = {'name': 'the_profile', constants.PROVIDER_NAME: 'amp',
constants.AVAILABILITY_ZONE_DATA: '{"hello": "world"}'}
body = self._build_body(update_data)
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)
response = self.put(self.AZP_PATH.format(azp_id=azp.get('id')),
body, status=403)
self.conf.config(group='api_settings', auth_strategy=auth_strategy)
response = self.get(
self.AZP_PATH.format(azp_id=azp.get('id'))).json.get(self.root_tag)
self.assertEqual('test_profile', response.get('name'))
self.assertEqual('noop_driver', response.get(constants.PROVIDER_NAME))
self.assertEqual('{"x": "y"}',
response.get(constants.AVAILABILITY_ZONE_DATA))
def test_update_in_use(self):
azp = self.create_availability_zone_profile(
'test_profile', 'noop_driver', '{"x": "y"}')
self.create_availability_zone(
'name1', 'description', azp.get('id'), True)
# Test updating provider while in use is not allowed
update_data = {'name': 'the_profile',
constants.PROVIDER_NAME: 'noop_driver-alt'}
body = self._build_body(update_data)
response = self.put(self.AZP_PATH.format(azp_id=azp.get('id')), body,
status=409)
err_msg = ("Availability Zone Profile {} is in use and cannot be "
"modified.".format(azp.get('id')))
self.assertEqual(err_msg, response.json.get('faultstring'))
response = self.get(
self.AZP_PATH.format(azp_id=azp.get('id'))).json.get(self.root_tag)
self.assertEqual('test_profile', response.get('name'))
self.assertEqual('noop_driver', response.get(constants.PROVIDER_NAME))
self.assertEqual('{"x": "y"}',
response.get(constants.AVAILABILITY_ZONE_DATA))
# Test updating availability zone data while in use is not allowed
update_data = {'name': 'the_profile',
constants.AVAILABILITY_ZONE_DATA: '{"hello": "world"}'}
body = self._build_body(update_data)
response = self.put(self.AZP_PATH.format(azp_id=azp.get('id')), body,
status=409)
err_msg = ("Availability Zone Profile {} is in use and cannot be "
"modified.".format(azp.get('id')))
self.assertEqual(err_msg, response.json.get('faultstring'))
response = self.get(
self.AZP_PATH.format(azp_id=azp.get('id'))).json.get(self.root_tag)
self.assertEqual('test_profile', response.get('name'))
self.assertEqual('noop_driver', response.get(constants.PROVIDER_NAME))
self.assertEqual('{"x": "y"}',
response.get(constants.AVAILABILITY_ZONE_DATA))
# Test that you can still update the name when in use
update_data = {'name': 'the_profile'}
body = self._build_body(update_data)
response = self.put(self.AZP_PATH.format(azp_id=azp.get('id')), body)
response = self.get(
self.AZP_PATH.format(azp_id=azp.get('id'))).json.get(self.root_tag)
self.assertEqual('the_profile', response.get('name'))
self.assertEqual('noop_driver', response.get(constants.PROVIDER_NAME))
self.assertEqual('{"x": "y"}',
response.get(constants.AVAILABILITY_ZONE_DATA))
def test_delete(self):
azp = self.create_availability_zone_profile(
'test1', 'noop_driver', '{"compute_zone": "my_az_1"}')
self.assertTrue(uuidutils.is_uuid_like(azp.get('id')))
self.delete(self.AZP_PATH.format(azp_id=azp.get('id')))
response = self.get(self.AZP_PATH.format(
azp_id=azp.get('id')), status=404)
err_msg = "Availability Zone Profile %s not found." % azp.get('id')
self.assertEqual(err_msg, response.json.get('faultstring'))
def test_delete_deleted_id(self):
response = self.delete(self.AZP_PATH.format(azp_id=constants.NIL_UUID),
status=404)
self.assertEqual('Availability Zone Profile {} not found.'.format(
constants.NIL_UUID), response.json.get('faultstring'))
def test_delete_nonexistent_id(self):
response = self.delete(self.AZP_PATH.format(azp_id='bogus_id'),
status=404)
self.assertEqual('Availability Zone Profile bogus_id not found.',
response.json.get('faultstring'))
def test_delete_authorized(self):
azp = self.create_availability_zone_profile(
'test1', 'noop_driver', '{"compute_zone": "my_az_1"}')
self.assertTrue(uuidutils.is_uuid_like(azp.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):
self.delete(self.AZP_PATH.format(azp_id=azp.get('id')))
self.conf.config(group='api_settings', auth_strategy=auth_strategy)
response = self.get(self.AZP_PATH.format(
azp_id=azp.get('id')), status=404)
err_msg = "Availability Zone Profile %s not found." % azp.get('id')
self.assertEqual(err_msg, response.json.get('faultstring'))
def test_delete_not_authorized(self):
azp = self.create_availability_zone_profile(
'test1', 'noop_driver', '{"compute_zone": "my_az_1"}')
self.assertTrue(uuidutils.is_uuid_like(azp.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)
response = self.delete(self.AZP_PATH.format(
azp_id=azp.get('id')), status=403)
api_azp = response.json
self.conf.config(group='api_settings', auth_strategy=auth_strategy)
self.assertEqual(self.NOT_AUTHORIZED_BODY, api_azp)
response = self.get(
self.AZP_PATH.format(azp_id=azp.get('id'))).json.get(self.root_tag)
self.assertEqual('test1', response.get('name'))
def test_delete_in_use(self):
azp = self.create_availability_zone_profile(
'test1', 'noop_driver', '{"compute_zone": "my_az_1"}')
self.create_availability_zone(
'name1', 'description', azp.get('id'), True)
response = self.delete(self.AZP_PATH.format(azp_id=azp.get('id')),
status=409)
err_msg = ("Availability Zone Profile {} is in use and cannot be "
"modified.".format(azp.get('id')))
self.assertEqual(err_msg, response.json.get('faultstring'))
response = self.get(
self.AZP_PATH.format(azp_id=azp.get('id'))).json.get(self.root_tag)
self.assertEqual('test1', response.get('name'))

View File

@ -0,0 +1,581 @@
# Copyright 2019 Verizon Media
#
# 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_utils import uuidutils
from oslo_config import cfg
from oslo_config import fixture as oslo_fixture
from octavia.common import constants
import octavia.common.context
from octavia.common import exceptions
from octavia.tests.functional.api.v2 import base
class TestAvailabilityZones(base.BaseAPITest):
root_tag = 'availability_zone'
root_tag_list = 'availability_zones'
root_tag_links = 'availability_zones_links'
def setUp(self):
super(TestAvailabilityZones, self).setUp()
self.azp = self.create_availability_zone_profile(
'test1', 'noop_driver', '{"compute_zone": "my_az_1"}')
def _assert_request_matches_response(self, req, resp, **optionals):
self.assertTrue('id' not in resp) # AZs do not expose an ID
req_description = req.get('description')
self.assertEqual(req.get('name'), resp.get('name'))
if not req_description:
self.assertEqual('', resp.get('description'))
else:
self.assertEqual(req.get('description'), resp.get('description'))
self.assertEqual(req.get('availability_zone_profile_id'),
resp.get('availability_zone_profile_id'))
self.assertEqual(req.get('enabled', True),
resp.get('enabled'))
def test_empty_list(self):
response = self.get(self.AZS_PATH)
api_list = response.json.get(self.root_tag_list)
self.assertEqual([], api_list)
def test_create(self):
az_json = {'name': 'test1',
'availability_zone_profile_id': self.azp.get('id')}
body = self._build_body(az_json)
response = self.post(self.AZS_PATH, body)
api_az = response.json.get(self.root_tag)
self._assert_request_matches_response(az_json, api_az)
def test_create_with_missing_name(self):
az_json = {'availability_zone_profile_id': self.azp.get('id')}
body = self._build_body(az_json)
response = self.post(self.AZS_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_long_name(self):
az_json = {'name': 'n' * 256,
'availability_zone_profile_id': self.azp.get('id')}
body = self._build_body(az_json)
self.post(self.AZS_PATH, body, status=400)
def test_create_with_long_description(self):
az_json = {'name': 'test-az',
'description': 'n' * 256,
'availability_zone_profile_id': self.azp.get('id')}
body = self._build_body(az_json)
self.post(self.AZS_PATH, body, status=400)
def test_create_with_missing_availability_zone_profile(self):
az_json = {'name': 'xyz'}
body = self._build_body(az_json)
response = self.post(self.AZS_PATH, body, status=400)
err_msg = (
"Invalid input for field/attribute availability_zone_profile_id. "
"Value: 'None'. Mandatory field missing.")
self.assertEqual(err_msg, response.json.get('faultstring'))
def test_create_with_bad_availability_zone_profile(self):
az_json = {'name': 'xyz', 'availability_zone_profile_id': 'bogus'}
body = self._build_body(az_json)
response = self.post(self.AZS_PATH, body, status=400)
err_msg = (
"Invalid input for field/attribute availability_zone_profile_id. "
"Value: 'bogus'. Value should be UUID format")
self.assertEqual(err_msg, response.json.get('faultstring'))
def test_create_duplicate_names(self):
self.create_availability_zone(
'name', 'description', self.azp.get('id'), True)
az_json = {'name': 'name',
'availability_zone_profile_id': self.azp.get('id')}
body = self._build_body(az_json)
response = self.post(self.AZS_PATH, body, status=409)
err_msg = "A availability zone of name already exists."
self.assertEqual(err_msg, response.json.get('faultstring'))
def test_create_authorized(self):
az_json = {'name': 'test1',
'availability_zone_profile_id': self.azp.get('id')}
body = self._build_body(az_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.AZS_PATH, body)
api_az = response.json.get(self.root_tag)
self.conf.config(group='api_settings', auth_strategy=auth_strategy)
self._assert_request_matches_response(az_json, api_az)
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)
az_json = {'name': 'name',
'availability_zone_profile_id': self.azp.get('id')}
body = self._build_body(az_json)
response = self.post(self.AZS_PATH, body, status=403)
api_az = response.json
self.conf.config(group='api_settings', auth_strategy=auth_strategy)
self.assertEqual(self.NOT_AUTHORIZED_BODY, api_az)
def test_create_db_failure(self):
az_json = {'name': 'test1',
'availability_zone_profile_id': self.azp.get('id')}
body = self._build_body(az_json)
with mock.patch("octavia.db.repositories.AvailabilityZoneRepository."
"create") as mock_create:
mock_create.side_effect = Exception
self.post(self.AZS_PATH, body, status=500)
def test_get(self):
az = self.create_availability_zone(
'name', 'description', self.azp.get('id'), True)
response = self.get(
self.AZ_PATH.format(
az_name=az.get('name'))).json.get(self.root_tag)
self.assertEqual('name', response.get('name'))
self.assertEqual('description', response.get('description'))
self.assertEqual(az.get('name'), response.get('name'))
self.assertEqual(self.azp.get('id'),
response.get('availability_zone_profile_id'))
self.assertTrue(response.get('enabled'))
def test_get_one_fields_filter(self):
az = self.create_availability_zone(
'name', 'description', self.azp.get('id'), True)
response = self.get(
self.AZ_PATH.format(az_name=az.get('name')), params={
'fields': ['name', 'availability_zone_profile_id']}
).json.get(self.root_tag)
self.assertEqual(az.get('name'), response.get('name'))
self.assertEqual(self.azp.get('id'),
response.get('availability_zone_profile_id'))
self.assertIn(u'availability_zone_profile_id', response)
self.assertNotIn(u'description', response)
self.assertNotIn(u'enabled', response)
def test_get_one_deleted_name(self):
response = self.get(
self.AZ_PATH.format(az_name=constants.NIL_UUID), status=404)
self.assertEqual(
'Availability Zone {} not found.'.format(constants.NIL_UUID),
response.json.get('faultstring'))
def test_get_authorized(self):
az = self.create_availability_zone(
'name', 'description', self.azp.get('id'), True)
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': False,
'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.AZ_PATH.format(
az_name=az.get('name'))).json.get(self.root_tag)
self.conf.config(group='api_settings', auth_strategy=auth_strategy)
self.assertEqual('name', response.get('name'))
self.assertEqual('description', response.get('description'))
self.assertEqual(self.azp.get('id'),
response.get('availability_zone_profile_id'))
self.assertTrue(response.get('enabled'))
def test_get_not_authorized(self):
az = self.create_availability_zone(
'name', 'description', self.azp.get('id'), True)
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)
response = self.get(self.AZ_PATH.format(
az_name=az.get('name')), status=403).json
self.conf.config(group='api_settings', auth_strategy=auth_strategy)
self.assertEqual(self.NOT_AUTHORIZED_BODY, response)
def test_get_all(self):
self.create_availability_zone(
'name1', 'description', self.azp.get('id'), True)
ref_az_1 = {
u'description': u'description', u'enabled': True,
u'availability_zone_profile_id': self.azp.get('id'),
u'name': u'name1'}
self.create_availability_zone(
'name2', 'description', self.azp.get('id'), True)
ref_az_2 = {
u'description': u'description', u'enabled': True,
u'availability_zone_profile_id': self.azp.get('id'),
u'name': u'name2'}
response = self.get(self.AZS_PATH)
api_list = response.json.get(self.root_tag_list)
self.assertEqual(2, len(api_list))
self.assertIn(ref_az_1, api_list)
self.assertIn(ref_az_2, api_list)
def test_get_all_fields_filter(self):
self.create_availability_zone(
'name1', 'description', self.azp.get('id'), True)
self.create_availability_zone(
'name2', 'description', self.azp.get('id'), True)
response = self.get(self.AZS_PATH, params={
'fields': ['id', 'name']})
api_list = response.json.get(self.root_tag_list)
self.assertEqual(2, len(api_list))
for az in api_list:
self.assertIn(u'name', az)
self.assertNotIn(u'availability_zone_profile_id', az)
self.assertNotIn(u'description', az)
self.assertNotIn(u'enabled', az)
def test_get_all_authorized(self):
self.create_availability_zone(
'name1', 'description', self.azp.get('id'), True)
self.create_availability_zone(
'name2', 'description', self.azp.get('id'), True)
response = self.get(self.AZS_PATH)
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': False,
'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):
api_list = response.json.get(self.root_tag_list)
self.conf.config(group='api_settings', auth_strategy=auth_strategy)
self.assertEqual(2, len(api_list))
def test_get_all_not_authorized(self):
self.create_availability_zone(
'name1', 'description', self.azp.get('id'), True)
self.create_availability_zone(
'name2', 'description', self.azp.get('id'), True)
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)
response = self.get(self.AZS_PATH, status=403).json
self.conf.config(group='api_settings', auth_strategy=auth_strategy)
self.assertEqual(self.NOT_AUTHORIZED_BODY, response)
def test_update(self):
az_json = {'name': 'Fancy_Availability_Zone',
'description': 'A great az. Pick me!',
'availability_zone_profile_id': self.azp.get('id')}
body = self._build_body(az_json)
response = self.post(self.AZS_PATH, body)
api_az = response.json.get(self.root_tag)
availability_zone_name = api_az.get('name')
az_json = {'description': 'An even better az. Pick me!',
'enabled': False}
body = self._build_body(az_json)
self.put(self.AZ_PATH.format(az_name=availability_zone_name), body)
updated_az = self.get(self.AZ_PATH.format(
az_name=availability_zone_name)).json.get(self.root_tag)
self.assertEqual('An even better az. Pick me!',
updated_az.get('description'))
self.assertEqual(availability_zone_name, updated_az.get('name'))
self.assertEqual(self.azp.get('id'),
updated_az.get('availability_zone_profile_id'))
self.assertFalse(updated_az.get('enabled'))
def test_update_deleted_name(self):
update_json = {'description': 'fake_desc'}
body = self._build_body(update_json)
response = self.put(
self.AZ_PATH.format(az_name=constants.NIL_UUID), body,
status=404)
self.assertEqual(
'Availability Zone {} not found.'.format(constants.NIL_UUID),
response.json.get('faultstring'))
def test_update_none(self):
az_json = {'name': 'Fancy_Availability_Zone',
'description': 'A great az. Pick me!',
'availability_zone_profile_id': self.azp.get('id')}
body = self._build_body(az_json)
response = self.post(self.AZS_PATH, body)
api_az = response.json.get(self.root_tag)
availability_zone_name = api_az.get('name')
az_json = {}
body = self._build_body(az_json)
self.put(self.AZ_PATH.format(az_name=availability_zone_name), body)
updated_az = self.get(self.AZ_PATH.format(
az_name=availability_zone_name)).json.get(self.root_tag)
self.assertEqual('Fancy_Availability_Zone', updated_az.get('name'))
self.assertEqual('A great az. Pick me!',
updated_az.get('description'))
self.assertEqual(availability_zone_name, updated_az.get('name'))
self.assertEqual(self.azp.get('id'),
updated_az.get('availability_zone_profile_id'))
self.assertTrue(updated_az.get('enabled'))
def test_update_availability_zone_profile_id(self):
az_json = {'name': 'Fancy_Availability_Zone',
'description': 'A great az. Pick me!',
'availability_zone_profile_id': self.azp.get('id')}
body = self._build_body(az_json)
response = self.post(self.AZS_PATH, body)
api_az = response.json.get(self.root_tag)
availability_zone_name = api_az.get('name')
az_json = {'availability_zone_profile_id': uuidutils.generate_uuid()}
body = self._build_body(az_json)
self.put(self.AZ_PATH.format(az_name=availability_zone_name),
body, status=400)
updated_az = self.get(self.AZ_PATH.format(
az_name=availability_zone_name)).json.get(self.root_tag)
self.assertEqual(self.azp.get('id'),
updated_az.get('availability_zone_profile_id'))
def test_update_authorized(self):
az_json = {'name': 'Fancy_Availability_Zone',
'description': 'A great az. Pick me!',
'availability_zone_profile_id': self.azp.get('id')}
body = self._build_body(az_json)
response = self.post(self.AZS_PATH, body)
api_az = response.json.get(self.root_tag)
availability_zone_name = api_az.get('name')
az_json = {'description': 'An even better az. Pick me!',
'enabled': False}
body = self._build_body(az_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):
self.put(self.AZ_PATH.format(az_name=availability_zone_name),
body)
self.conf.config(group='api_settings', auth_strategy=auth_strategy)
updated_az = self.get(self.AZ_PATH.format(
az_name=availability_zone_name)).json.get(self.root_tag)
self.assertEqual('An even better az. Pick me!',
updated_az.get('description'))
self.assertEqual(availability_zone_name, updated_az.get('name'))
self.assertEqual(self.azp.get('id'),
updated_az.get('availability_zone_profile_id'))
self.assertFalse(updated_az.get('enabled'))
def test_update_not_authorized(self):
az_json = {'name': 'Fancy_Availability_Zone',
'description': 'A great az. Pick me!',
'availability_zone_profile_id': self.azp.get('id')}
body = self._build_body(az_json)
response = self.post(self.AZS_PATH, body)
api_az = response.json.get(self.root_tag)
availability_zone_name = api_az.get('name')
az_json = {'description': 'An even better az. Pick me!',
'enabled': False}
body = self._build_body(az_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)
self.put(self.AZ_PATH.format(az_name=availability_zone_name),
body, status=403)
self.conf.config(group='api_settings', auth_strategy=auth_strategy)
updated_az = self.get(self.AZ_PATH.format(
az_name=availability_zone_name)).json.get(self.root_tag)
self.assertEqual('A great az. Pick me!',
updated_az.get('description'))
self.assertEqual(availability_zone_name, updated_az.get('name'))
self.assertEqual(self.azp.get('id'),
updated_az.get('availability_zone_profile_id'))
self.assertTrue(updated_az.get('enabled'))
@mock.patch('octavia.db.repositories.AvailabilityZoneRepository.update')
def test_update_exception(self, mock_update):
mock_update.side_effect = [exceptions.OctaviaException()]
update_json = {'description': 'Some availability zone.'}
body = self._build_body(update_json)
response = self.put(self.AZ_PATH.format(az_name='bogus'), body,
status=500)
self.assertEqual('An unknown exception occurred.',
response.json.get('faultstring'))
def test_delete(self):
az = self.create_availability_zone(
'name1', 'description', self.azp.get('id'), True)
self.delete(self.AZ_PATH.format(az_name=az.get('name')))
response = self.get(self.AZ_PATH.format(az_name=az.get('name')),
status=404)
err_msg = "Availability Zone %s not found." % az.get('name')
self.assertEqual(err_msg, response.json.get('faultstring'))
def test_delete_nonexistent_name(self):
response = self.delete(
self.AZ_PATH.format(az_name='bogus_name'), status=404)
self.assertEqual('Availability Zone bogus_name not found.',
response.json.get('faultstring'))
def test_delete_deleted_name(self):
response = self.delete(
self.AZ_PATH.format(az_name=constants.NIL_UUID), status=404)
self.assertEqual(
'Availability Zone {} not found.'.format(constants.NIL_UUID),
response.json.get('faultstring'))
def test_delete_authorized(self):
az = self.create_availability_zone(
'name1', 'description', self.azp.get('id'), True)
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):
self.delete(
self.AZ_PATH.format(az_name=az.get('name')))
self.conf.config(group='api_settings', auth_strategy=auth_strategy)
response = self.get(self.AZ_PATH.format(az_name=az.get('name')),
status=404)
err_msg = "Availability Zone %s not found." % az.get('name')
self.assertEqual(err_msg, response.json.get('faultstring'))
def test_delete_not_authorized(self):
az = self.create_availability_zone(
'name1', 'description', self.azp.get('id'), True)
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)
response = self.delete(self.AZ_PATH.format(az_name=az.get('name')),
status=403)
api_az = response.json
self.conf.config(group='api_settings', auth_strategy=auth_strategy)
self.assertEqual(self.NOT_AUTHORIZED_BODY, api_az)
response = self.get(self.AZ_PATH.format(
az_name=az.get('name'))).json.get(self.root_tag)
self.assertEqual('name1', response.get('name'))
def test_delete_in_use(self):
# TODO(sorrison): Enable this test
self.skipTest("Enable in next patch when LB can use AZ")
az = self.create_availability_zone(
'name1', 'description', self.azp.get('id'), True)
project_id = uuidutils.generate_uuid()
lb_id = uuidutils.generate_uuid()
self.create_load_balancer(lb_id, name='lb1',
project_id=project_id,
description='desc1',
availability_zone_name=az.get('name'),
admin_state_up=False)
self.delete(self.AZ_PATH.format(az_name=az.get('name')),
status=409)
response = self.get(self.AZ_PATH.format(
az_name=az.get('name'))).json.get(self.root_tag)
self.assertEqual('name1', response.get('name'))
@mock.patch('octavia.db.repositories.AvailabilityZoneRepository.delete')
def test_delete_exception(self, mock_delete):
mock_delete.side_effect = [exceptions.OctaviaException()]
response = self.delete(self.AZ_PATH.format(az_name='bogus'),
status=500)
self.assertEqual('An unknown exception occurred.',
response.json.get('faultstring'))

View File

@ -32,8 +32,8 @@ class TestFlavorProfiles(base.BaseAPITest):
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.PROVIDER_NAME),
resp.get(constants.PROVIDER_NAME))
self.assertEqual(req.get(constants.FLAVOR_DATA),
resp.get(constants.FLAVOR_DATA))
@ -43,7 +43,7 @@ class TestFlavorProfiles(base.BaseAPITest):
self.assertEqual([], api_list)
def test_create(self):
fp_json = {'name': 'test1', 'provider_name': 'noop_driver',
fp_json = {'name': 'test1', constants.PROVIDER_NAME: 'noop_driver',
constants.FLAVOR_DATA: '{"hello": "world"}'}
body = self._build_body(fp_json)
response = self.post(self.FPS_PATH, body)
@ -51,7 +51,8 @@ class TestFlavorProfiles(base.BaseAPITest):
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"}'}
fp_json = {constants.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: "
@ -67,7 +68,7 @@ class TestFlavorProfiles(base.BaseAPITest):
self.assertEqual(err_msg, response.json.get('faultstring'))
def test_create_with_missing_flavor_data(self):
fp_json = {'name': 'xyz', 'provider_name': 'pr1'}
fp_json = {'name': 'xyz', constants.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. "
@ -75,7 +76,7 @@ class TestFlavorProfiles(base.BaseAPITest):
self.assertEqual(err_msg, response.json.get('faultstring'))
def test_create_with_empty_flavor_data(self):
fp_json = {'name': 'test1', 'provider_name': 'noop_driver',
fp_json = {'name': 'test1', constants.PROVIDER_NAME: 'noop_driver',
constants.FLAVOR_DATA: '{}'}
body = self._build_body(fp_json)
response = self.post(self.FPS_PATH, body)
@ -83,25 +84,25 @@ class TestFlavorProfiles(base.BaseAPITest):
self._assert_request_matches_response(fp_json, api_fp)
def test_create_with_long_name(self):
fp_json = {'name': 'n' * 256, 'provider_name': 'test1',
fp_json = {'name': 'n' * 256, constants.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,
fp_json = {'name': 'name1', constants.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',
fp_json = {'name': 'name1', constants.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',
fp_json = {'name': 'test1', constants.PROVIDER_NAME: 'noop_driver',
constants.FLAVOR_DATA: '{"hello": "world"}'}
body = self._build_body(fp_json)
self.conf = self.useFixture(oslo_fixture.Config(cfg.CONF))
@ -136,7 +137,8 @@ class TestFlavorProfiles(base.BaseAPITest):
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"}'}
constants.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
@ -144,7 +146,7 @@ class TestFlavorProfiles(base.BaseAPITest):
self.assertEqual(self.NOT_AUTHORIZED_BODY, api_fp)
def test_create_db_failure(self):
fp_json = {'name': 'test1', 'provider_name': 'noop_driver',
fp_json = {'name': 'test1', constants.PROVIDER_NAME: 'noop_driver',
constants.FLAVOR_DATA: '{"hello": "world"}'}
body = self._build_body(fp_json)
with mock.patch("octavia.db.repositories.FlavorProfileRepository."
@ -156,7 +158,7 @@ class TestFlavorProfiles(base.BaseAPITest):
self.post(self.FPS_PATH, body, status=409)
def test_create_with_invalid_json(self):
fp_json = {'name': 'test1', 'provider_name': 'noop_driver',
fp_json = {'name': 'test1', constants.PROVIDER_NAME: 'noop_driver',
constants.FLAVOR_DATA: '{hello: "world"}'}
body = self._build_body(fp_json)
self.post(self.FPS_PATH, body, status=400)
@ -183,10 +185,11 @@ class TestFlavorProfiles(base.BaseAPITest):
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)
'fields': ['id', constants.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.assertIn(constants.PROVIDER_NAME, response)
self.assertNotIn(u'name', response)
self.assertNotIn(constants.FLAVOR_DATA, response)
@ -238,13 +241,13 @@ class TestFlavorProfiles(base.BaseAPITest):
'{"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'}
constants.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'}
constants.PROVIDER_NAME: u'noop_driver-alt'}
self.assertTrue(uuidutils.is_uuid_like(fp2.get('id')))
response = self.get(self.FPS_PATH)
@ -268,7 +271,7 @@ class TestFlavorProfiles(base.BaseAPITest):
for profile in api_list:
self.assertIn(u'id', profile)
self.assertIn(u'name', profile)
self.assertNotIn(u'provider_name', profile)
self.assertNotIn(constants.PROVIDER_NAME, profile)
self.assertNotIn(constants.FLAVOR_DATA, profile)
def test_get_all_authorized(self):
@ -323,14 +326,15 @@ class TestFlavorProfiles(base.BaseAPITest):
fp = self.create_flavor_profile('test_profile', 'noop_driver',
'{"x": "y"}')
update_data = {'name': 'the_profile',
'provider_name': 'noop_driver-alt',
constants.PROVIDER_NAME: 'noop_driver-alt',
constants.FLAVOR_DATA: '{"hello": "world"}'}
body = self._build_body(update_data)
response = self.put(self.FP_PATH.format(fp_id=fp.get('id')), body)
response = self.get(
self.FP_PATH.format(fp_id=fp.get('id'))).json.get(self.root_tag)
self.assertEqual('the_profile', response.get('name'))
self.assertEqual('noop_driver-alt', response.get('provider_name'))
self.assertEqual('noop_driver-alt',
response.get(constants.PROVIDER_NAME))
self.assertEqual('{"hello": "world"}',
response.get(constants.FLAVOR_DATA))
@ -350,7 +354,7 @@ class TestFlavorProfiles(base.BaseAPITest):
response = self.get(
self.FP_PATH.format(fp_id=fp.get('id'))).json.get(self.root_tag)
self.assertEqual('test_profile', response.get('name'))
self.assertEqual('noop_driver', response.get('provider_name'))
self.assertEqual('noop_driver', response.get(constants.PROVIDER_NAME))
self.assertEqual('{"x": "y"}',
response.get(constants.FLAVOR_DATA))
@ -377,20 +381,21 @@ class TestFlavorProfiles(base.BaseAPITest):
fp = self.create_flavor_profile('test_profile', 'noop_driver',
'{"x": "y"}')
update_data = {'name': 'the_profile',
'provider_name': 'noop_driver-alt'}
constants.PROVIDER_NAME: 'noop_driver-alt'}
body = self._build_body(update_data)
response = self.put(self.FP_PATH.format(fp_id=fp.get('id')), body)
response = self.get(
self.FP_PATH.format(fp_id=fp.get('id'))).json.get(self.root_tag)
self.assertEqual('the_profile', response.get('name'))
self.assertEqual('noop_driver-alt', response.get('provider_name'))
self.assertEqual('noop_driver-alt',
response.get(constants.PROVIDER_NAME))
self.assertEqual('{"x": "y"}', response.get(constants.FLAVOR_DATA))
def test_update_authorized(self):
fp = self.create_flavor_profile('test_profile', 'noop_driver',
'{"x": "y"}')
update_data = {'name': 'the_profile',
'provider_name': 'noop_driver-alt',
constants.PROVIDER_NAME: 'noop_driver-alt',
constants.FLAVOR_DATA: '{"hello": "world"}'}
body = self._build_body(update_data)
self.conf = self.useFixture(oslo_fixture.Config(cfg.CONF))
@ -421,14 +426,15 @@ class TestFlavorProfiles(base.BaseAPITest):
response = self.get(
self.FP_PATH.format(fp_id=fp.get('id'))).json.get(self.root_tag)
self.assertEqual('the_profile', response.get('name'))
self.assertEqual('noop_driver-alt', response.get('provider_name'))
self.assertEqual('noop_driver-alt',
response.get(constants.PROVIDER_NAME))
self.assertEqual('{"hello": "world"}',
response.get(constants.FLAVOR_DATA))
def test_update_not_authorized(self):
fp = self.create_flavor_profile('test_profile', 'noop_driver',
'{"x": "y"}')
update_data = {'name': 'the_profile', 'provider_name': 'amp',
update_data = {'name': 'the_profile', constants.PROVIDER_NAME: 'amp',
constants.FLAVOR_DATA: '{"hello": "world"}'}
body = self._build_body(update_data)
self.conf = self.useFixture(oslo_fixture.Config(cfg.CONF))
@ -440,7 +446,7 @@ class TestFlavorProfiles(base.BaseAPITest):
response = self.get(
self.FP_PATH.format(fp_id=fp.get('id'))).json.get(self.root_tag)
self.assertEqual('test_profile', response.get('name'))
self.assertEqual('noop_driver', response.get('provider_name'))
self.assertEqual('noop_driver', response.get(constants.PROVIDER_NAME))
self.assertEqual('{"x": "y"}',
response.get(constants.FLAVOR_DATA))
@ -451,7 +457,7 @@ class TestFlavorProfiles(base.BaseAPITest):
# Test updating provider while in use is not allowed
update_data = {'name': 'the_profile',
'provider_name': 'noop_driver-alt'}
constants.PROVIDER_NAME: 'noop_driver-alt'}
body = self._build_body(update_data)
response = self.put(self.FP_PATH.format(fp_id=fp.get('id')), body,
status=409)
@ -461,7 +467,7 @@ class TestFlavorProfiles(base.BaseAPITest):
response = self.get(
self.FP_PATH.format(fp_id=fp.get('id'))).json.get(self.root_tag)
self.assertEqual('test_profile', response.get('name'))
self.assertEqual('noop_driver', response.get('provider_name'))
self.assertEqual('noop_driver', response.get(constants.PROVIDER_NAME))
self.assertEqual('{"x": "y"}', response.get(constants.FLAVOR_DATA))
# Test updating flavor data while in use is not allowed
@ -476,7 +482,7 @@ class TestFlavorProfiles(base.BaseAPITest):
response = self.get(
self.FP_PATH.format(fp_id=fp.get('id'))).json.get(self.root_tag)
self.assertEqual('test_profile', response.get('name'))
self.assertEqual('noop_driver', response.get('provider_name'))
self.assertEqual('noop_driver', response.get(constants.PROVIDER_NAME))
self.assertEqual('{"x": "y"}', response.get(constants.FLAVOR_DATA))
# Test that you can still update the name when in use
@ -486,7 +492,7 @@ class TestFlavorProfiles(base.BaseAPITest):
response = self.get(
self.FP_PATH.format(fp_id=fp.get('id'))).json.get(self.root_tag)
self.assertEqual('the_profile', response.get('name'))
self.assertEqual('noop_driver', response.get('provider_name'))
self.assertEqual('noop_driver', response.get(constants.PROVIDER_NAME))
self.assertEqual('{"x": "y"}', response.get(constants.FLAVOR_DATA))
def test_delete(self):

View File

@ -245,11 +245,6 @@ class TestFlavors(base.BaseAPITest):
self.assertEqual(self.NOT_AUTHORIZED_BODY, response)
def test_get_all(self):
ref_flavor_1 = {
u'description': u'description', u'enabled': True,
u'flavor_profile_id': u'd21bf20d-c323-4004-bf67-f90591ceced9',
u'id': u'172ccb10-a3b7-4c73-aee8-bdb77fb51ed5',
u'name': u'name1'}
flavor1 = self.create_flavor('name1', 'description', self.fp.get('id'),
True)
self.assertTrue(uuidutils.is_uuid_like(flavor1.get('id')))

View File

@ -171,3 +171,144 @@ class TestFlavorCapabilities(base.BaseAPITest):
self.assertEqual(1, len(capabilities))
self.assertEqual(1, len(capabilities[0]))
self.assertEqual('compute_flavor', capabilities[0][constants.NAME])
class TestAvailabilityZoneCapabilities(base.BaseAPITest):
root_tag = 'availability_zone_capabilities'
def setUp(self):
super(TestAvailabilityZoneCapabilities, self).setUp()
def test_nonexistent_provider(self):
self.get(self.AVAILABILITY_ZONE_CAPABILITIES_PATH.format(
provider='bogus'), status=400)
def test_noop_provider(self):
ref_capabilities = [{'description': 'The compute availability zone to '
'use for this loadbalancer.',
'name': constants.COMPUTE_ZONE}]
result = self.get(
self.AVAILABILITY_ZONE_CAPABILITIES_PATH.format(
provider='noop_driver'))
self.assertEqual(ref_capabilities, result.json.get(self.root_tag))
def test_amphora_driver(self):
ref_description1 = 'The compute availability zone.'
ref_description2 = 'The management network ID for the amphora.'
result = self.get(
self.AVAILABILITY_ZONE_CAPABILITIES_PATH.format(
provider='amphora'))
capabilities = result.json.get(self.root_tag)
capability_dict = [i for i in capabilities if
i['name'] == constants.COMPUTE_ZONE][0]
self.assertEqual(ref_description1,
capability_dict['description'])
capability_dict = [i for i in capabilities if
i['name'] == constants.MANAGEMENT_NETWORK][0]
self.assertEqual(ref_description2,
capability_dict['description'])
# Some drivers might not have implemented this yet, test that case
@mock.patch('octavia.api.drivers.noop_driver.driver.NoopProviderDriver.'
'get_supported_availability_zone_metadata')
def test_not_implemented(self, mock_get_metadata):
mock_get_metadata.side_effect = exceptions.NotImplementedError()
self.get(self.AVAILABILITY_ZONE_CAPABILITIES_PATH.format(
provider='noop_driver'), status=501)
def test_authorized(self):
ref_capabilities = [{'description': 'The compute availability zone to '
'use for this loadbalancer.',
'name': constants.COMPUTE_ZONE}]
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):
result = self.get(
self.AVAILABILITY_ZONE_CAPABILITIES_PATH.format(
provider='noop_driver'))
self.conf.config(group='api_settings', auth_strategy=auth_strategy)
self.assertEqual(ref_capabilities, result.json.get(self.root_tag))
def test_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)
self.get(self.AVAILABILITY_ZONE_CAPABILITIES_PATH.format(
provider='noop_driver'), status=403)
self.conf.config(group='api_settings', auth_strategy=auth_strategy)
def test_amphora_driver_one_filter(self):
ref_description = 'The compute availability zone.'
result = self.get(
self.AVAILABILITY_ZONE_CAPABILITIES_PATH.format(
provider=constants.AMPHORA),
params={constants.NAME: constants.COMPUTE_ZONE})
capabilities = result.json.get(self.root_tag)
self.assertEqual(1, len(capabilities))
self.assertEqual(2, len(capabilities[0]))
self.assertEqual(ref_description,
capabilities[0][constants.DESCRIPTION])
ref_description = 'The management network ID for the amphora.'
result = self.get(
self.AVAILABILITY_ZONE_CAPABILITIES_PATH.format(
provider=constants.AMPHORA),
params={constants.NAME: constants.MANAGEMENT_NETWORK})
capabilities = result.json.get(self.root_tag)
self.assertEqual(1, len(capabilities))
self.assertEqual(2, len(capabilities[0]))
self.assertEqual(ref_description,
capabilities[0][constants.DESCRIPTION])
def test_amphora_driver_two_filters(self):
ref_description = 'The compute availability zone.'
result = self.get(
self.AVAILABILITY_ZONE_CAPABILITIES_PATH.format(
provider=constants.AMPHORA),
params={constants.NAME: constants.COMPUTE_ZONE,
constants.DESCRIPTION: ref_description})
capabilities = result.json.get(self.root_tag)
self.assertEqual(1, len(capabilities))
self.assertEqual(ref_description,
capabilities[0][constants.DESCRIPTION])
def test_amphora_driver_filter_no_match(self):
result = self.get(
self.AVAILABILITY_ZONE_CAPABILITIES_PATH.format(
provider=constants.AMPHORA),
params={constants.NAME: 'bogus'})
capabilities = result.json.get(self.root_tag)
self.assertEqual([], capabilities)
def test_amphora_driver_one_filter_one_field(self):
result = self.get(
self.AVAILABILITY_ZONE_CAPABILITIES_PATH.format(
provider=constants.AMPHORA),
params={constants.NAME: constants.COMPUTE_ZONE,
constants.FIELDS: constants.NAME})
capabilities = result.json.get(self.root_tag)
self.assertEqual(1, len(capabilities))
self.assertEqual(1, len(capabilities[0]))
self.assertEqual(constants.COMPUTE_ZONE,
capabilities[0][constants.NAME])

View File

@ -108,6 +108,18 @@ class OctaviaDBTestBase(test_base.DbTestCase):
description='Placeholder for DELETED LBs with DELETED flavors')
session.add(deleted_flavor)
session.flush()
deleted_az_profile = models.AvailabilityZoneProfile(
id=constants.NIL_UUID, name='DELETED-PLACEHOLDER',
provider_name=constants.DELETED, availability_zone_data='{}')
session.add(deleted_az_profile)
session.flush()
deleted_az = models.AvailabilityZone(
availability_zone_profile_id=constants.NIL_UUID,
name=constants.NIL_UUID, enabled=False,
description='Placeholder for DELETED LBs with DELETED '
'availability zones')
session.add(deleted_az)
session.flush()
def _seed_lookup_table(self, session, name_list, model_cls):
for name in name_list:

View File

@ -121,7 +121,8 @@ class AllRepositoriesTest(base.OctaviaDBTestBase):
'amphorahealth', 'vrrpgroup', 'l7rule', 'l7policy',
'amp_build_slots', 'amp_build_req', 'quotas',
'flavor', 'flavor_profile', 'spares_pool',
'listener_cidr')
'listener_cidr', 'availability_zone',
'availability_zone_profile')
for repo_attr in repo_attr_names:
single_repo = getattr(self.repos, repo_attr, None)
message = ("Class Repositories should have %s instance"

View File

@ -648,3 +648,45 @@ class TestAmphoraDriver(base.TestRpc):
'SUPPORTED_FLAVOR_SCHEMA', 'bogus'):
self.assertRaises(exceptions.DriverError,
self.amp_driver.validate_flavor, 'bogus')
# Availability Zone
def test_get_supported_availability_zone_metadata(self):
test_schema = {
"properties": {
"test_name": {"description": "Test description"},
"test_name2": {"description": "Another description"}}}
ref_dict = {"test_name": "Test description",
"test_name2": "Another description"}
# mock out the supported_availability_zone_metadata
with mock.patch('octavia.api.drivers.amphora_driver.'
'availability_zone_schema.'
'SUPPORTED_AVAILABILITY_ZONE_SCHEMA', test_schema):
result = self.amp_driver.get_supported_availability_zone_metadata()
self.assertEqual(ref_dict, result)
# Test for bad schema
with mock.patch('octavia.api.drivers.amphora_driver.'
'availability_zone_schema.'
'SUPPORTED_AVAILABILITY_ZONE_SCHEMA', 'bogus'):
self.assertRaises(
exceptions.DriverError,
self.amp_driver.get_supported_availability_zone_metadata)
def test_validate_availability_zone(self):
ref_dict = {consts.COMPUTE_ZONE: 'my_compute_zone'}
self.amp_driver.validate_availability_zone(ref_dict)
# Test bad availability zone metadata key
ref_dict = {'bogus': 'bogus'}
self.assertRaises(exceptions.UnsupportedOptionError,
self.amp_driver.validate_availability_zone,
ref_dict)
# Test for bad schema
with mock.patch('octavia.api.drivers.amphora_driver.'
'availability_zone_schema.'
'SUPPORTED_AVAILABILITY_ZONE_SCHEMA', 'bogus'):
self.assertRaises(exceptions.DriverError,
self.amp_driver.validate_availability_zone,
'bogus')

View File

@ -648,3 +648,45 @@ class TestAmphoraDriver(base.TestRpc):
'SUPPORTED_FLAVOR_SCHEMA', 'bogus'):
self.assertRaises(exceptions.DriverError,
self.amp_driver.validate_flavor, 'bogus')
# Availability Zone
def test_get_supported_availability_zone_metadata(self):
test_schema = {
"properties": {
"test_name": {"description": "Test description"},
"test_name2": {"description": "Another description"}}}
ref_dict = {"test_name": "Test description",
"test_name2": "Another description"}
# mock out the supported_availability_zone_metadata
with mock.patch('octavia.api.drivers.amphora_driver.'
'availability_zone_schema.'
'SUPPORTED_AVAILABILITY_ZONE_SCHEMA', test_schema):
result = self.amp_driver.get_supported_availability_zone_metadata()
self.assertEqual(ref_dict, result)
# Test for bad schema
with mock.patch('octavia.api.drivers.amphora_driver.'
'availability_zone_schema.'
'SUPPORTED_AVAILABILITY_ZONE_SCHEMA', 'bogus'):
self.assertRaises(
exceptions.DriverError,
self.amp_driver.get_supported_availability_zone_metadata)
def test_validate_availability_zone(self):
ref_dict = {consts.COMPUTE_ZONE: 'my_compute_zone'}
self.amp_driver.validate_availability_zone(ref_dict)
# Test bad availability zone metadata key
ref_dict = {'bogus': 'bogus'}
self.assertRaises(exceptions.UnsupportedOptionError,
self.amp_driver.validate_availability_zone,
ref_dict)
# Test for bad schema
with mock.patch('octavia.api.drivers.amphora_driver.'
'availability_zone_schema.'
'SUPPORTED_AVAILABILITY_ZONE_SCHEMA', 'bogus'):
self.assertRaises(exceptions.DriverError,
self.amp_driver.validate_availability_zone,
'bogus')

View File

@ -143,6 +143,9 @@ class TestNoopProviderDriver(base.TestCase):
self.ref_flavor_metadata = {"amp_image_tag": "The glance image tag "
"to use for this load balancer."}
self.ref_availability_zone_metadata = {
"compute_zone": "The compute availability zone to use for this "
"loadbalancer."}
def test_create_vip_port(self):
vip_dict = self.driver.create_vip_port(self.loadbalancer_id,
@ -304,3 +307,17 @@ class TestNoopProviderDriver(base.TestCase):
flavor_hash = hash(frozenset(self.ref_flavor_metadata))
self.assertEqual((self.ref_flavor_metadata, 'validate_flavor'),
self.driver.driver.driverconfig[flavor_hash])
def test_get_supported_availability_zone_metadata(self):
metadata = self.driver.get_supported_availability_zone_metadata()
self.assertEqual(self.ref_availability_zone_metadata, metadata)
def test_validate_availability_zone(self):
self.driver.validate_availability_zone(
self.ref_availability_zone_metadata)
az_hash = hash(frozenset(self.ref_availability_zone_metadata))
self.assertEqual((self.ref_availability_zone_metadata,
'validate_availability_zone'),
self.driver.driver.driverconfig[az_hash])

View File

@ -0,0 +1,70 @@
# Copyright 2019 Verizon Media
#
# 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 exc
from wsme.rest import json as wsme_json
from octavia.api.v2.types import availability_zone_profile as azp_type
from octavia.common import constants
from octavia.tests.unit.api.common import base
class TestAvailabilityZoneProfile(object):
_type = None
def test_availability_zone_profile(self):
body = {"name": "test_name", "provider_name": "test1",
constants.AVAILABILITY_ZONE_DATA: '{"hello": "world"}'}
availability_zone = wsme_json.fromjson(self._type, body)
self.assertEqual(availability_zone.name, body["name"])
def test_invalid_name(self):
body = {"name": 0}
self.assertRaises(exc.InvalidInput, wsme_json.fromjson, self._type,
body)
def test_name_length(self):
body = {"name": "x" * 256}
self.assertRaises(exc.InvalidInput, wsme_json.fromjson, self._type,
body)
def test_provider_name_length(self):
body = {"name": "x" * 250,
"provider_name": "X" * 256}
self.assertRaises(exc.InvalidInput, wsme_json.fromjson,
self._type, body)
def test_name_mandatory(self):
body = {"provider_name": "test1",
constants.AVAILABILITY_ZONE_DATA: '{"hello": "world"}'}
self.assertRaises(exc.InvalidInput, wsme_json.fromjson, self._type,
body)
def test_provider_name_mandatory(self):
body = {"name": "test_name",
constants.AVAILABILITY_ZONE_DATA: '{"hello": "world"}'}
self.assertRaises(exc.InvalidInput, wsme_json.fromjson, self._type,
body)
def test_meta_mandatory(self):
body = {"name": "test_name", "provider_name": "test1"}
self.assertRaises(exc.InvalidInput, wsme_json.fromjson, self._type,
body)
class TestAvailabilityZoneProfilePOST(base.BaseTypesTest,
TestAvailabilityZoneProfile):
_type = azp_type.AvailabilityZoneProfilePOST

View File

@ -0,0 +1,87 @@
# 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_utils import uuidutils
from wsme import exc
from wsme.rest import json as wsme_json
from octavia.api.v2.types import availability_zones as availability_zone_type
from octavia.tests.unit.api.common import base
class TestAvailabilityZone(object):
_type = None
def test_availability_zone(self):
body = {"name": "test_name", "description": "test_description",
"availability_zone_profile_id": uuidutils.generate_uuid()}
availability_zone = wsme_json.fromjson(self._type, body)
self.assertTrue(availability_zone.enabled)
def test_invalid_name(self):
body = {"name": 0,
"availability_zone_profile_id": uuidutils.generate_uuid()}
self.assertRaises(exc.InvalidInput, wsme_json.fromjson, self._type,
body)
def test_name_length(self):
body = {"name": "x" * 256,
"availability_zone_profile_id": uuidutils.generate_uuid()}
self.assertRaises(exc.InvalidInput, wsme_json.fromjson, self._type,
body)
def test_invalid_description(self):
body = {"availability_zone_profile_id": uuidutils.generate_uuid(),
"description": 0, "name": "test"}
self.assertRaises(exc.InvalidInput, wsme_json.fromjson, self._type,
body)
def test_description_length(self):
body = {"name": "x" * 250,
"availability_zone_profile_id": uuidutils.generate_uuid(),
"description": "0" * 256}
self.assertRaises(exc.InvalidInput, wsme_json.fromjson, self._type,
body)
def test_invalid_enabled(self):
body = {"name": "test_name",
"availability_zone_profile_id": uuidutils.generate_uuid(),
"enabled": "notvalid"}
self.assertRaises(ValueError, wsme_json.fromjson, self._type,
body)
def test_name_mandatory(self):
body = {"description": "xyz",
"availability_zone_profile_id": uuidutils.generate_uuid(),
"enabled": True}
self.assertRaises(exc.InvalidInput, wsme_json.fromjson, self._type,
body)
def test_availability_zone_profile_id_mandatory(self):
body = {"name": "test_name"}
self.assertRaises(exc.InvalidInput, wsme_json.fromjson, self._type,
body)
class TestAvailabilityZonePOST(base.BaseTypesTest, TestAvailabilityZone):
_type = availability_zone_type.AvailabilityZonePOST
def test_non_uuid_project_id(self):
body = {"name": "test_name", "description": "test_description",
"availability_zone_profile_id": uuidutils.generate_uuid()}
lb = wsme_json.fromjson(self._type, body)
self.assertEqual(lb.availability_zone_profile_id,
body['availability_zone_profile_id'])

View File

@ -49,6 +49,8 @@ class TestNoopComputeDriver(base.TestCase):
self.port_id = 88
self.network_id = uuidutils.generate_uuid()
self.ip_address = "192.0.2.2"
self.flavor_id = uuidutils.generate_uuid()
self.availability_zone = 'my_test_az'
def test_build(self):
self.driver.build(self.name, self.amphora_flavor,
@ -120,3 +122,14 @@ class TestNoopComputeDriver(base.TestCase):
'detach_port'),
self.driver.driver.computeconfig[(
self.amphora_id, self.port_id)])
def test_validate_flavor(self):
self.driver.validate_flavor(self.flavor_id)
self.assertEqual((self.flavor_id, 'validate_flavor'),
self.driver.driver.computeconfig[self.flavor_id])
def test_validate_availability_zone(self):
self.driver.validate_availability_zone(self.availability_zone)
self.assertEqual(
(self.availability_zone, 'validate_availability_zone'),
self.driver.driver.computeconfig[self.availability_zone])

View File

@ -128,7 +128,7 @@ class TestNovaClient(base.TestCase):
self.manager.server_groups = mock.MagicMock()
self.manager._nova_client = mock.MagicMock()
self.manager.flavor_manager = mock.MagicMock()
self.manager.flavor_manager.get = mock.MagicMock()
self.manager.availability_zone_manager = mock.MagicMock()
self.nova_response.interface_list.side_effect = [[self.interface_list]]
self.manager.manager.get.return_value = self.nova_response
@ -156,6 +156,7 @@ class TestNovaClient(base.TestCase):
self.compute_id = uuidutils.generate_uuid()
self.network_id = uuidutils.generate_uuid()
self.flavor_id = uuidutils.generate_uuid()
self.availability_zone = 'my_test_az'
super(TestNovaClient, self).setUp()
@ -444,3 +445,17 @@ class TestNovaClient(base.TestCase):
self.assertRaises(exceptions.OctaviaException,
self.manager.validate_flavor,
"bogus")
def test_validate_availability_zone(self):
mock_az = mock.Mock()
mock_az.zoneName = self.availability_zone
self.manager.availability_zone_manager.list.return_value = [mock_az]
self.manager.validate_availability_zone(self.availability_zone)
self.manager.availability_zone_manager.list.assert_called_with(
detailed=False)
def test_validate_availability_zone_with_exception(self):
self.manager.availability_zone_manager.list.return_value = []
self.assertRaises(exceptions.InvalidSubresource,
self.manager.validate_availability_zone,
"bogus")

View File

@ -0,0 +1,6 @@
---
features:
- |
Add an API for allowing administrators to manage Octavia Availability
Zones and Availability Zone Profiles, which behave nearly identically
to Flavors and Flavor Profiles.