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:
parent
4a5c24ef6f
commit
8ae6bc3697
@ -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
|
||||
|
290
api-ref/source/v2/availabilityzone.inc
Normal file
290
api-ref/source/v2/availabilityzone.inc
Normal 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.
|
297
api-ref/source/v2/availabilityzoneprofile.inc
Normal file
297
api-ref/source/v2/availabilityzoneprofile.inc
Normal 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.
|
1
api-ref/source/v2/examples/availabilityzone-create-curl
Normal file
1
api-ref/source/v2/examples/availabilityzone-create-curl
Normal 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
|
@ -0,0 +1,8 @@
|
||||
{
|
||||
"availability_zone": {
|
||||
"name": "my_az",
|
||||
"description": "My availability zone.",
|
||||
"enabled": true,
|
||||
"availability_zone_profile_id": "5712097e-0092-45dc-bff0-ab68b61ad51a"
|
||||
}
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
{
|
||||
"availability_zone": {
|
||||
"name": "my_az",
|
||||
"description": "My availability zone.",
|
||||
"enabled": true,
|
||||
"availability_zone_profile_id": "5712097e-0092-45dc-bff0-ab68b61ad51a"
|
||||
}
|
||||
}
|
1
api-ref/source/v2/examples/availabilityzone-delete-curl
Normal file
1
api-ref/source/v2/examples/availabilityzone-delete-curl
Normal file
@ -0,0 +1 @@
|
||||
curl -X DELETE -H "X-Auth-Token: <token>" http://198.51.100.10:9876/v2.0/lbaas/availabilityzones/my_az
|
1
api-ref/source/v2/examples/availabilityzone-list-curl
Normal file
1
api-ref/source/v2/examples/availabilityzone-list-curl
Normal file
@ -0,0 +1 @@
|
||||
curl -X GET -H "X-Auth-Token: <token>" http://198.51.100.10:9876/v2.0/lbaas/availabilityzones
|
@ -0,0 +1,10 @@
|
||||
{
|
||||
"availability_zones": [
|
||||
{
|
||||
"name": "my_az",
|
||||
"description": "My availability zone.",
|
||||
"enabled": true,
|
||||
"availability_zone_profile_id": "5712097e-0092-45dc-bff0-ab68b61ad51a"
|
||||
}
|
||||
]
|
||||
}
|
1
api-ref/source/v2/examples/availabilityzone-show-curl
Normal file
1
api-ref/source/v2/examples/availabilityzone-show-curl
Normal file
@ -0,0 +1 @@
|
||||
curl -X GET -H "X-Auth-Token: <token>" http://198.51.100.10:9876/v2.0/lbaas/availabilityzones/my_az
|
@ -0,0 +1,8 @@
|
||||
{
|
||||
"availability_zone": {
|
||||
"name": "my_az",
|
||||
"description": "My availability zone.",
|
||||
"enabled": true,
|
||||
"availability_zone_profile_id": "5712097e-0092-45dc-bff0-ab68b61ad51a"
|
||||
}
|
||||
}
|
1
api-ref/source/v2/examples/availabilityzone-update-curl
Normal file
1
api-ref/source/v2/examples/availabilityzone-update-curl
Normal 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
|
@ -0,0 +1,6 @@
|
||||
{
|
||||
"availability_zone": {
|
||||
"description": "My availability zone.",
|
||||
"enabled": false
|
||||
}
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
{
|
||||
"availability_zone": {
|
||||
"name": "my_az",
|
||||
"description": "My availability zone.",
|
||||
"enabled": false,
|
||||
"availability_zone_profile_id": "5712097e-0092-45dc-bff0-ab68b61ad51a"
|
||||
}
|
||||
}
|
@ -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
|
@ -0,0 +1,8 @@
|
||||
{
|
||||
"availability_zone_profile":
|
||||
{
|
||||
"name": "some_az",
|
||||
"provider_name": "amphora",
|
||||
"availability_zone_data": "{\"compute_zone\": \"az1\"}"
|
||||
}
|
||||
}
|
@ -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\"}"
|
||||
}
|
||||
}
|
@ -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
|
@ -0,0 +1 @@
|
||||
curl -X GET -H "X-Auth-Token: <token>" http://198.51.100.10:9876/v2.0/lbaas/availabilityzoneprofiles
|
@ -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\"}"
|
||||
}
|
||||
]
|
||||
}
|
@ -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
|
@ -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\"}"
|
||||
}
|
||||
}
|
@ -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
|
@ -0,0 +1,8 @@
|
||||
{
|
||||
"availability_zone_profile":
|
||||
{
|
||||
"name": "other_az",
|
||||
"provider_name": "amphora",
|
||||
"availability_zone_data": "{\"compute_zone\": \"az2\"}"
|
||||
}
|
||||
}
|
@ -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\"}"
|
||||
}
|
||||
}
|
@ -0,0 +1 @@
|
||||
curl -X GET -H "X-Auth-Token: <token>" http://198.51.100.10:9876/v2/lbaas/providers/amphora/availability_zone_capabilities
|
@ -0,0 +1,8 @@
|
||||
{
|
||||
"availability_zone_capabilities": [
|
||||
{
|
||||
"name": "compute_zone",
|
||||
"description": "The compute availability zone."
|
||||
}
|
||||
]
|
||||
}
|
@ -97,7 +97,7 @@ Request
|
||||
- name: name
|
||||
|
||||
Request Example
|
||||
----------------
|
||||
---------------
|
||||
|
||||
.. literalinclude:: examples/flavor-create-request.json
|
||||
:language: javascript
|
||||
|
@ -99,7 +99,7 @@ Request
|
||||
- provider_name: provider-name
|
||||
|
||||
Request Example
|
||||
----------------
|
||||
---------------
|
||||
|
||||
.. literalinclude:: examples/flavorprofile-create-request.json
|
||||
:language: javascript
|
||||
|
@ -66,6 +66,16 @@ Flavor Profiles
|
||||
---------------
|
||||
.. include:: flavorprofile.inc
|
||||
|
||||
------------------
|
||||
Availability Zones
|
||||
------------------
|
||||
.. include:: availabilityzone.inc
|
||||
|
||||
--------------------------
|
||||
Availability Zone Profiles
|
||||
--------------------------
|
||||
.. include:: availabilityzoneprofile.inc
|
||||
|
||||
--------
|
||||
Amphorae
|
||||
--------
|
||||
|
@ -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
|
||||
|
@ -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."
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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}
|
||||
|
@ -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):
|
||||
|
237
octavia/api/v2/controllers/availability_zone_profiles.py
Normal file
237
octavia/api/v2/controllers/availability_zone_profiles.py
Normal 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)
|
176
octavia/api/v2/controllers/availability_zones.py
Normal file
176
octavia/api/v2/controllers/availability_zones.py
Normal 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()
|
@ -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,
|
||||
|
@ -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)
|
||||
|
71
octavia/api/v2/types/availability_zone_profile.py
Normal file
71
octavia/api/v2/types/availability_zone_profile.py
Normal 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)
|
69
octavia/api/v2/types/availability_zones.py
Normal file
69
octavia/api/v2/types/availability_zones.py
Normal 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)
|
@ -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])
|
||||
|
@ -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'
|
||||
|
@ -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):
|
||||
|
@ -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
|
||||
"""
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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):
|
||||
|
@ -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}
|
||||
]
|
||||
)
|
@ -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"
|
||||
|
@ -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
|
||||
|
@ -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(),
|
||||
)
|
||||
|
62
octavia/policies/availability_zone.py
Normal file
62
octavia/policies/availability_zone.py
Normal 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
|
70
octavia/policies/availability_zone_profile.py
Normal file
70
octavia/policies/availability_zone_profile.py
Normal 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
|
@ -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}'}]
|
||||
),
|
||||
|
@ -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}'}]
|
||||
),
|
||||
|
33
octavia/policies/provider_availability_zone.py
Normal file
33
octavia/policies/provider_availability_zone.py
Normal 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
|
@ -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'}]
|
||||
),
|
||||
]
|
||||
|
||||
|
@ -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'}]
|
||||
|
@ -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,
|
||||
|
@ -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'))
|
581
octavia/tests/functional/api/v2/test_availability_zones.py
Normal file
581
octavia/tests/functional/api/v2/test_availability_zones.py
Normal 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'))
|
@ -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):
|
||||
|
@ -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')))
|
||||
|
@ -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])
|
||||
|
@ -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:
|
||||
|
@ -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"
|
||||
|
@ -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')
|
||||
|
@ -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')
|
||||
|
@ -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])
|
||||
|
@ -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
|
87
octavia/tests/unit/api/v2/types/test_availability_zones.py
Normal file
87
octavia/tests/unit/api/v2/types/test_availability_zones.py
Normal 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'])
|
@ -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])
|
||||
|
@ -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")
|
||||
|
@ -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.
|
Loading…
Reference in New Issue
Block a user