Merge "Attach Manila shares via virtiofs (API)"
This commit is contained in:
@@ -31,6 +31,7 @@ the `API guide <https://docs.openstack.org/api-guide/compute/index.html>`_.
|
|||||||
.. include:: os-instance-actions.inc
|
.. include:: os-instance-actions.inc
|
||||||
.. include:: os-interface.inc
|
.. include:: os-interface.inc
|
||||||
.. include:: os-server-password.inc
|
.. include:: os-server-password.inc
|
||||||
|
.. include:: os-server-shares.inc
|
||||||
.. include:: os-volume-attachments.inc
|
.. include:: os-volume-attachments.inc
|
||||||
.. include:: flavors.inc
|
.. include:: flavors.inc
|
||||||
.. include:: os-flavor-access.inc
|
.. include:: os-flavor-access.inc
|
||||||
|
|||||||
@@ -0,0 +1,163 @@
|
|||||||
|
.. -*- rst -*-
|
||||||
|
|
||||||
|
===================================================================
|
||||||
|
Servers with shares attachments (servers, shares)
|
||||||
|
===================================================================
|
||||||
|
|
||||||
|
Attaches shares that are created through the Manila share API to server
|
||||||
|
instances. Also, lists share attachments for a server, shows
|
||||||
|
details for a share attachment, and detaches a share (New in version 2.97).
|
||||||
|
|
||||||
|
List share attachments for an instance
|
||||||
|
=======================================
|
||||||
|
|
||||||
|
.. rest_method:: GET /servers/{server_id}/shares
|
||||||
|
|
||||||
|
List share attachments for an instance.
|
||||||
|
|
||||||
|
Normal response codes: 200
|
||||||
|
|
||||||
|
Error response codes: badrequest(400), forbidden(403), itemNotFound(404)
|
||||||
|
|
||||||
|
Request
|
||||||
|
-------
|
||||||
|
|
||||||
|
.. rest_parameters:: parameters.yaml
|
||||||
|
|
||||||
|
- server_id: server_id_path
|
||||||
|
|
||||||
|
Response
|
||||||
|
--------
|
||||||
|
|
||||||
|
.. rest_parameters:: parameters.yaml
|
||||||
|
|
||||||
|
- shares: shares_body
|
||||||
|
- share_id: share_id_body
|
||||||
|
- status: share_status_body
|
||||||
|
- tag: share_tag_body
|
||||||
|
|
||||||
|
|
||||||
|
**Example List share attachments for an instance: JSON response**
|
||||||
|
|
||||||
|
.. literalinclude:: ../../doc/api_samples/os-server-shares/v2.97/server-shares-list-resp.json
|
||||||
|
:language: javascript
|
||||||
|
|
||||||
|
|
||||||
|
Attach a share to an instance
|
||||||
|
==============================
|
||||||
|
|
||||||
|
.. rest_method:: POST /servers/{server_id}/shares
|
||||||
|
|
||||||
|
Attach a share to an instance.
|
||||||
|
|
||||||
|
Normal response codes: 201
|
||||||
|
|
||||||
|
Error response codes: badRequest(400), forbidden(403), itemNotFound(404), conflict(409)
|
||||||
|
|
||||||
|
.. note:: This action is only valid when the server is in ``STOPPED`` state.
|
||||||
|
|
||||||
|
.. note:: This action also needs specific configurations, check the documentation requirements to configure
|
||||||
|
your environment and support this feature.
|
||||||
|
|
||||||
|
Request
|
||||||
|
-------
|
||||||
|
|
||||||
|
.. rest_parameters:: parameters.yaml
|
||||||
|
|
||||||
|
- server_id: server_id_path
|
||||||
|
- share_id: share_id_body
|
||||||
|
- tag: share_tag_body
|
||||||
|
|
||||||
|
**Example Attach a share to an instance: JSON request**
|
||||||
|
|
||||||
|
.. literalinclude:: ../../doc/api_samples/os-server-shares/v2.97/server-shares-create-req.json
|
||||||
|
:language: javascript
|
||||||
|
|
||||||
|
|
||||||
|
Response
|
||||||
|
--------
|
||||||
|
|
||||||
|
.. rest_parameters:: parameters.yaml
|
||||||
|
|
||||||
|
- shares: shares_body
|
||||||
|
- share_id: share_id_body
|
||||||
|
- status: share_status_body
|
||||||
|
- tag: share_tag_body
|
||||||
|
|
||||||
|
**Example Attach a share to an instance: JSON response**
|
||||||
|
|
||||||
|
.. literalinclude:: ../../doc/api_samples/os-server-shares/v2.97/server-shares-create-resp.json
|
||||||
|
:language: javascript
|
||||||
|
|
||||||
|
|
||||||
|
Show a detail of a share attachment
|
||||||
|
====================================
|
||||||
|
|
||||||
|
.. rest_method:: GET /servers/{server_id}/shares/{share_id}
|
||||||
|
|
||||||
|
Show a detail of a share attachment.
|
||||||
|
|
||||||
|
Normal response codes: 200
|
||||||
|
|
||||||
|
Error response codes: badRequest(400), forbidden(403), itemNotFound(404)
|
||||||
|
|
||||||
|
|
||||||
|
Request
|
||||||
|
-------
|
||||||
|
|
||||||
|
.. rest_parameters:: parameters.yaml
|
||||||
|
|
||||||
|
- server_id: server_id_path
|
||||||
|
- share_id: share_id_path
|
||||||
|
|
||||||
|
Response
|
||||||
|
--------
|
||||||
|
|
||||||
|
.. rest_parameters:: parameters.yaml
|
||||||
|
|
||||||
|
- share: share_body
|
||||||
|
- uuid: share_uuid_body
|
||||||
|
- share_id: share_id_body
|
||||||
|
- status: share_status_body
|
||||||
|
- tag: share_tag_body
|
||||||
|
- export_location: share_export_location_body
|
||||||
|
|
||||||
|
.. note:: Optional fields can only be seen by admins.
|
||||||
|
|
||||||
|
**Example Show a detail of a share attachment: JSON response**
|
||||||
|
|
||||||
|
.. literalinclude:: ../../doc/api_samples/os-server-shares/v2.97/server-shares-show-resp.json
|
||||||
|
:language: javascript
|
||||||
|
|
||||||
|
**Example Show a detail of a share attachment with admin rights: JSON response**
|
||||||
|
|
||||||
|
.. literalinclude:: ../../doc/api_samples/os-server-shares/v2.97/server-shares-admin-show-resp.json
|
||||||
|
:language: javascript
|
||||||
|
|
||||||
|
|
||||||
|
Detach a share from an instance
|
||||||
|
================================
|
||||||
|
|
||||||
|
.. rest_method:: DELETE /servers/{server_id}/shares/{share_id}
|
||||||
|
|
||||||
|
Detach a share from an instance.
|
||||||
|
|
||||||
|
Normal response codes: 200
|
||||||
|
|
||||||
|
Error response codes: badRequest(400), forbidden(403), itemNotFound(404), conflict(409)
|
||||||
|
|
||||||
|
.. note:: This action is only valid when the server is in ``STOPPED`` or ``ERROR`` state.
|
||||||
|
|
||||||
|
|
||||||
|
Request
|
||||||
|
-------
|
||||||
|
|
||||||
|
.. rest_parameters:: parameters.yaml
|
||||||
|
|
||||||
|
- server_id: server_id_path
|
||||||
|
- share_id: share_id_path
|
||||||
|
|
||||||
|
Response
|
||||||
|
--------
|
||||||
|
|
||||||
|
No body is returned on successful request.
|
||||||
@@ -311,6 +311,12 @@ service_id_path_2_53_no_version:
|
|||||||
in: path
|
in: path
|
||||||
required: true
|
required: true
|
||||||
type: string
|
type: string
|
||||||
|
share_id_path:
|
||||||
|
description: |
|
||||||
|
The UUID of the attached share.
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
snapshot_id_path:
|
snapshot_id_path:
|
||||||
description: |
|
description: |
|
||||||
The UUID of the snapshot.
|
The UUID of the snapshot.
|
||||||
@@ -3742,13 +3748,13 @@ hosts.availability_zone_none:
|
|||||||
type: none
|
type: none
|
||||||
hours:
|
hours:
|
||||||
description: |
|
description: |
|
||||||
The duration that the server exists (in hours).
|
The duration that the server exists (in hours).
|
||||||
in: body
|
in: body
|
||||||
required: true
|
required: true
|
||||||
type: float
|
type: float
|
||||||
hours_optional:
|
hours_optional:
|
||||||
description: |
|
description: |
|
||||||
The duration that the server exists (in hours).
|
The duration that the server exists (in hours).
|
||||||
in: body
|
in: body
|
||||||
required: false
|
required: false
|
||||||
type: float
|
type: float
|
||||||
@@ -6809,6 +6815,56 @@ set_metadata:
|
|||||||
in: body
|
in: body
|
||||||
required: true
|
required: true
|
||||||
type: object
|
type: object
|
||||||
|
share_body:
|
||||||
|
description: |
|
||||||
|
A dictionary representation of a share attachment containing the fields
|
||||||
|
``uuid``, ``serverId``, ``status``, ``tag`` and ``export_location``.
|
||||||
|
in: body
|
||||||
|
required: true
|
||||||
|
type: object
|
||||||
|
share_export_location_body:
|
||||||
|
description: |
|
||||||
|
The export location used to attach the share to the underlying host.
|
||||||
|
in: body
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
share_id_body:
|
||||||
|
description: |
|
||||||
|
The UUID of the attached share.
|
||||||
|
in: body
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
share_status_body:
|
||||||
|
description: |
|
||||||
|
Status of the Share:
|
||||||
|
|
||||||
|
- attaching: The share is being attached to the VM by the compute node.
|
||||||
|
- detaching: The share is being detached from the VM by the compute node.
|
||||||
|
- inactive: The share is attached but inactive because the VM is stopped.
|
||||||
|
- active: The share is attached, and the VM is running.
|
||||||
|
- error: The share is in an error state.
|
||||||
|
in: body
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
share_tag_body:
|
||||||
|
description: |
|
||||||
|
The device tag to be used by users to mount the share within the instance,
|
||||||
|
if not provided then the share UUID will be used automatically.
|
||||||
|
in: body
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
share_uuid_body:
|
||||||
|
description: |
|
||||||
|
The UUID of the share attachment.
|
||||||
|
in: body
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
shares_body:
|
||||||
|
description: |
|
||||||
|
The list of share attachments.
|
||||||
|
in: body
|
||||||
|
required: true
|
||||||
|
type: array
|
||||||
shelve:
|
shelve:
|
||||||
description: |
|
description: |
|
||||||
The action.
|
The action.
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"share": {
|
||||||
|
"uuid": "68ba1762-fd6d-4221-8311-f3193dd93404",
|
||||||
|
"share_id": "e8debdc0-447a-4376-a10a-4cd9122d7986",
|
||||||
|
"status": "attaching",
|
||||||
|
"export_location": "10.0.0.50:/mnt/foo",
|
||||||
|
"tag": "e8debdc0-447a-4376-a10a-4cd9122d7986"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"share": {
|
||||||
|
"uuid": "68ba1762-fd6d-4221-8311-f3193dd93404",
|
||||||
|
"share_id": "e8debdc0-447a-4376-a10a-4cd9122d7986",
|
||||||
|
"status": "inactive",
|
||||||
|
"export_location": "10.0.0.50:/mnt/foo",
|
||||||
|
"tag": "e8debdc0-447a-4376-a10a-4cd9122d7986"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"share": {
|
||||||
|
"share_id": "3cdf5132-64f2-4584-876a-bd296ae7eabd",
|
||||||
|
"tag": "my-share"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"share": {
|
||||||
|
"share_id": "e8debdc0-447a-4376-a10a-4cd9122d7986",
|
||||||
|
"status": "attaching",
|
||||||
|
"tag": "e8debdc0-447a-4376-a10a-4cd9122d7986"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"shares": [
|
||||||
|
{
|
||||||
|
"share_id": "e8debdc0-447a-4376-a10a-4cd9122d7986",
|
||||||
|
"status": "inactive",
|
||||||
|
"tag": "e8debdc0-447a-4376-a10a-4cd9122d7986"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"share": {
|
||||||
|
"share_id": "e8debdc0-447a-4376-a10a-4cd9122d7986",
|
||||||
|
"status": "inactive",
|
||||||
|
"tag": "e8debdc0-447a-4376-a10a-4cd9122d7986"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"status": "CURRENT",
|
"status": "CURRENT",
|
||||||
"version": "2.96",
|
"version": "2.97",
|
||||||
"min_version": "2.1",
|
"min_version": "2.1",
|
||||||
"updated": "2013-07-23T11:33:21Z"
|
"updated": "2013-07-23T11:33:21Z"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"status": "CURRENT",
|
"status": "CURRENT",
|
||||||
"version": "2.96",
|
"version": "2.97",
|
||||||
"min_version": "2.1",
|
"min_version": "2.1",
|
||||||
"updated": "2013-07-23T11:33:21Z"
|
"updated": "2013-07-23T11:33:21Z"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -257,6 +257,14 @@ REST_API_VERSION_HISTORY = """REST API Version History:
|
|||||||
* 2.95 - Evacuate will now stop instance at destination.
|
* 2.95 - Evacuate will now stop instance at destination.
|
||||||
* 2.96 - Add support for returning pinned_availability_zone in
|
* 2.96 - Add support for returning pinned_availability_zone in
|
||||||
``server show`` and ``server list --long`` responses.
|
``server show`` and ``server list --long`` responses.
|
||||||
|
* 2.97 - Adds new API ``GET /servers/{server_id}/shares`` which shows
|
||||||
|
shares attachments of a given server.
|
||||||
|
``GET /servers/{server_id}/shares/{share_id} which gives details
|
||||||
|
about a share attachment.
|
||||||
|
``POST /servers/{server_id}/shares/{share_id} which create an
|
||||||
|
attachment.
|
||||||
|
``DELETE /servers/{server_id}/shares/{share_id} which delete an
|
||||||
|
attachment.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# The minimum and maximum versions of the API supported
|
# The minimum and maximum versions of the API supported
|
||||||
@@ -265,7 +273,7 @@ REST_API_VERSION_HISTORY = """REST API Version History:
|
|||||||
# Note(cyeoh): This only applies for the v2.1 API once microversions
|
# Note(cyeoh): This only applies for the v2.1 API once microversions
|
||||||
# support is fully merged. It does not affect the V2 API.
|
# support is fully merged. It does not affect the V2 API.
|
||||||
_MIN_API_VERSION = '2.1'
|
_MIN_API_VERSION = '2.1'
|
||||||
_MAX_API_VERSION = '2.96'
|
_MAX_API_VERSION = '2.97'
|
||||||
DEFAULT_API_VERSION = _MIN_API_VERSION
|
DEFAULT_API_VERSION = _MIN_API_VERSION
|
||||||
|
|
||||||
# Almost all proxy APIs which are related to network, images and baremetal
|
# Almost all proxy APIs which are related to network, images and baremetal
|
||||||
|
|||||||
@@ -161,6 +161,10 @@ class EvacuateController(wsgi.Controller):
|
|||||||
raise exc.HTTPBadRequest(explanation=e.format_message())
|
raise exc.HTTPBadRequest(explanation=e.format_message())
|
||||||
except exception.UnsupportedRPCVersion as e:
|
except exception.UnsupportedRPCVersion as e:
|
||||||
raise exc.HTTPConflict(explanation=e.format_message())
|
raise exc.HTTPConflict(explanation=e.format_message())
|
||||||
|
except (
|
||||||
|
exception.ForbiddenSharesNotSupported,
|
||||||
|
exception.ForbiddenWithShare) as e:
|
||||||
|
raise exc.HTTPConflict(explanation=e.format_message())
|
||||||
|
|
||||||
if (not api_version_request.is_supported(req, min_version='2.14') and
|
if (not api_version_request.is_supported(req, min_version='2.14') and
|
||||||
CONF.api.enable_instance_password):
|
CONF.api.enable_instance_password):
|
||||||
|
|||||||
@@ -81,6 +81,11 @@ class MigrateServerController(wsgi.Controller):
|
|||||||
exception.ExtendedResourceRequestOldCompute,
|
exception.ExtendedResourceRequestOldCompute,
|
||||||
) as e:
|
) as e:
|
||||||
raise exc.HTTPBadRequest(explanation=e.format_message())
|
raise exc.HTTPBadRequest(explanation=e.format_message())
|
||||||
|
except (
|
||||||
|
exception.ForbiddenSharesNotSupported,
|
||||||
|
exception.ForbiddenWithShare,
|
||||||
|
) as e:
|
||||||
|
raise exc.HTTPConflict(explanation=e.format_message())
|
||||||
|
|
||||||
@wsgi.response(202)
|
@wsgi.response(202)
|
||||||
@wsgi.expected_errors((400, 403, 404, 409))
|
@wsgi.expected_errors((400, 403, 404, 409))
|
||||||
@@ -156,6 +161,11 @@ class MigrateServerController(wsgi.Controller):
|
|||||||
except exception.InstanceInvalidState as state_error:
|
except exception.InstanceInvalidState as state_error:
|
||||||
common.raise_http_conflict_for_instance_invalid_state(state_error,
|
common.raise_http_conflict_for_instance_invalid_state(state_error,
|
||||||
'os-migrateLive', id)
|
'os-migrateLive', id)
|
||||||
|
except (
|
||||||
|
exception.ForbiddenSharesNotSupported,
|
||||||
|
exception.ForbiddenWithShare,
|
||||||
|
) as e:
|
||||||
|
raise exc.HTTPConflict(explanation=e.format_message())
|
||||||
|
|
||||||
def _get_force_param_for_live_migration(self, body, host):
|
def _get_force_param_for_live_migration(self, body, host):
|
||||||
force = body["os-migrateLive"].get("force", False)
|
force = body["os-migrateLive"].get("force", False)
|
||||||
|
|||||||
@@ -1255,3 +1255,16 @@ behavior.
|
|||||||
|
|
||||||
The ``server show`` and ``server list --long`` responses now include the
|
The ``server show`` and ``server list --long`` responses now include the
|
||||||
pinned availability zone as well.
|
pinned availability zone as well.
|
||||||
|
|
||||||
|
.. _microversion 2.97:
|
||||||
|
|
||||||
|
2.97
|
||||||
|
----
|
||||||
|
|
||||||
|
This microversion introduces the new Manila Share Attachment feature,
|
||||||
|
streamlining the process of attaching and mounting Manila file shares to
|
||||||
|
instances. It includes a new set of APIs to easily add, remove, list, and
|
||||||
|
display shares. For detailed insights and usage instructions, please refer
|
||||||
|
to the `manage-shares documentation`_.
|
||||||
|
|
||||||
|
.. _manage-shares documentation: https://docs.openstack.org/nova/latest/admin/manage-shares.html
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ from nova.api.openstack.compute import server_groups
|
|||||||
from nova.api.openstack.compute import server_metadata
|
from nova.api.openstack.compute import server_metadata
|
||||||
from nova.api.openstack.compute import server_migrations
|
from nova.api.openstack.compute import server_migrations
|
||||||
from nova.api.openstack.compute import server_password
|
from nova.api.openstack.compute import server_password
|
||||||
|
from nova.api.openstack.compute import server_shares
|
||||||
from nova.api.openstack.compute import server_tags
|
from nova.api.openstack.compute import server_tags
|
||||||
from nova.api.openstack.compute import server_topology
|
from nova.api.openstack.compute import server_topology
|
||||||
from nova.api.openstack.compute import servers
|
from nova.api.openstack.compute import servers
|
||||||
@@ -310,6 +311,8 @@ server_remote_consoles_controller = functools.partial(_create_controller,
|
|||||||
server_security_groups_controller = functools.partial(_create_controller,
|
server_security_groups_controller = functools.partial(_create_controller,
|
||||||
security_groups.ServerSecurityGroupController, [])
|
security_groups.ServerSecurityGroupController, [])
|
||||||
|
|
||||||
|
server_shares_controller = functools.partial(_create_controller,
|
||||||
|
server_shares.ServerSharesController, [])
|
||||||
|
|
||||||
server_tags_controller = functools.partial(_create_controller,
|
server_tags_controller = functools.partial(_create_controller,
|
||||||
server_tags.ServerTagsController, [])
|
server_tags.ServerTagsController, [])
|
||||||
@@ -825,6 +828,14 @@ ROUTE_LIST = (
|
|||||||
('/servers/{server_id}/os-security-groups', {
|
('/servers/{server_id}/os-security-groups', {
|
||||||
'GET': [server_security_groups_controller, 'index']
|
'GET': [server_security_groups_controller, 'index']
|
||||||
}),
|
}),
|
||||||
|
('/servers/{server_id}/shares', {
|
||||||
|
'GET': [server_shares_controller, 'index'],
|
||||||
|
'POST': [server_shares_controller, 'create'],
|
||||||
|
}),
|
||||||
|
('/servers/{server_id}/shares/{id}', {
|
||||||
|
'GET': [server_shares_controller, 'show'],
|
||||||
|
'DELETE': [server_shares_controller, 'delete'],
|
||||||
|
}),
|
||||||
('/servers/{server_id}/tags', {
|
('/servers/{server_id}/tags', {
|
||||||
'GET': [server_tags_controller, 'index'],
|
'GET': [server_tags_controller, 'index'],
|
||||||
'PUT': [server_tags_controller, 'update_all'],
|
'PUT': [server_tags_controller, 'update_all'],
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
# 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 nova.api.validation import parameter_types
|
||||||
|
|
||||||
|
create = {
|
||||||
|
'title': 'Server shares',
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'share': {
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'share_id': parameter_types.share_id,
|
||||||
|
'tag': parameter_types.share_tag,
|
||||||
|
},
|
||||||
|
'required': ['share_id'],
|
||||||
|
'additionalProperties': False
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'required': ['share'],
|
||||||
|
'additionalProperties': False
|
||||||
|
}
|
||||||
|
|
||||||
|
index_query = {
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {},
|
||||||
|
'additionalProperties': False
|
||||||
|
}
|
||||||
|
|
||||||
|
show_query = {
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {},
|
||||||
|
'additionalProperties': False
|
||||||
|
}
|
||||||
|
|
||||||
|
# "share": {
|
||||||
|
# "uuid": "68ba1762-fd6d-4221-8311-f3193dd93404",
|
||||||
|
# "export_location": "10.0.0.50:/mnt/foo",
|
||||||
|
# "share_id": "e8debdc0-447a-4376-a10a-4cd9122d7986",
|
||||||
|
# "status": "inactive",
|
||||||
|
# "tag": "e8debdc0-447a-4376-a10a-4cd9122d7986"
|
||||||
|
# }
|
||||||
|
|
||||||
|
share_response = {
|
||||||
|
'title': 'Server share',
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'share': {
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'uuid': parameter_types.share_id,
|
||||||
|
'export_location': parameter_types.share_export_location,
|
||||||
|
'share_id': parameter_types.share_id,
|
||||||
|
'status': parameter_types.share_status,
|
||||||
|
'tag': parameter_types.share_tag,
|
||||||
|
},
|
||||||
|
'required': ['share_id', 'status', 'tag'],
|
||||||
|
'additionalProperties': False
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'required': ['share'],
|
||||||
|
'additionalProperties': False
|
||||||
|
}
|
||||||
|
|
||||||
|
share_list_response = {
|
||||||
|
'title': 'Server shares',
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'shares': {
|
||||||
|
'type': 'array',
|
||||||
|
'items': {
|
||||||
|
'properties': {
|
||||||
|
'uuid': parameter_types.share_id,
|
||||||
|
'export_location': parameter_types.share_export_location,
|
||||||
|
'share_id': parameter_types.share_id,
|
||||||
|
'status': parameter_types.share_status,
|
||||||
|
'tag': parameter_types.share_tag,
|
||||||
|
},
|
||||||
|
'required': ['share_id', 'status', 'tag'],
|
||||||
|
'additionalProperties': False
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'required': ['shares'],
|
||||||
|
'additionalProperties': False
|
||||||
|
}
|
||||||
@@ -0,0 +1,262 @@
|
|||||||
|
# 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 webob
|
||||||
|
|
||||||
|
from oslo_db import exception as db_exc
|
||||||
|
from oslo_utils import uuidutils
|
||||||
|
|
||||||
|
from nova.api.openstack import common
|
||||||
|
from nova.api.openstack.compute.schemas import server_shares as schema
|
||||||
|
from nova.api.openstack.compute.views import server_shares
|
||||||
|
from nova.api.openstack import wsgi
|
||||||
|
from nova.api import validation
|
||||||
|
from nova.compute import api as compute
|
||||||
|
from nova.compute import vm_states
|
||||||
|
from nova import context as nova_context
|
||||||
|
from nova import exception
|
||||||
|
from nova import objects
|
||||||
|
from nova.objects import fields
|
||||||
|
from nova.policies import server_shares as ss_policies
|
||||||
|
from nova.share import manila
|
||||||
|
from nova.virt import hardware as hw
|
||||||
|
|
||||||
|
|
||||||
|
def _get_instance_mapping(context, server_id):
|
||||||
|
try:
|
||||||
|
return objects.InstanceMapping.get_by_instance_uuid(context, server_id)
|
||||||
|
except exception.InstanceMappingNotFound as e:
|
||||||
|
raise webob.exc.HTTPNotFound(explanation=e.format_message())
|
||||||
|
|
||||||
|
|
||||||
|
class ServerSharesController(wsgi.Controller):
|
||||||
|
_view_builder_class = server_shares.ViewBuilder
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super(ServerSharesController, self).__init__()
|
||||||
|
self.compute_api = compute.API()
|
||||||
|
self.manila = manila.API()
|
||||||
|
|
||||||
|
def _get_instance_from_server_uuid(self, context, server_id):
|
||||||
|
instance = common.get_instance(self.compute_api, context, server_id)
|
||||||
|
return instance
|
||||||
|
|
||||||
|
def _check_instance_in_valid_state(self, context, server_id, action):
|
||||||
|
instance = self._get_instance_from_server_uuid(context, server_id)
|
||||||
|
if (
|
||||||
|
(action == "create share" and
|
||||||
|
instance.vm_state not in vm_states.STOPPED) or
|
||||||
|
(action == "delete share" and
|
||||||
|
instance.vm_state not in vm_states.STOPPED and
|
||||||
|
instance.vm_state not in vm_states.ERROR)
|
||||||
|
):
|
||||||
|
exc = exception.InstanceInvalidState(
|
||||||
|
attr="vm_state",
|
||||||
|
instance_uuid=instance.uuid,
|
||||||
|
state=instance.vm_state,
|
||||||
|
method=action,
|
||||||
|
)
|
||||||
|
common.raise_http_conflict_for_instance_invalid_state(
|
||||||
|
exc, action, server_id
|
||||||
|
)
|
||||||
|
return instance
|
||||||
|
|
||||||
|
@wsgi.Controller.api_version("2.97")
|
||||||
|
@wsgi.response(200)
|
||||||
|
@wsgi.expected_errors((400, 403, 404))
|
||||||
|
@validation.query_schema(schema.index_query)
|
||||||
|
@validation.response_body_schema(schema.share_list_response)
|
||||||
|
def index(self, req, server_id):
|
||||||
|
context = req.environ["nova.context"]
|
||||||
|
# Get instance mapping to query the required cell database
|
||||||
|
im = _get_instance_mapping(context, server_id)
|
||||||
|
context.can(ss_policies.POLICY_ROOT % 'index',
|
||||||
|
target={'project_id': im.project_id})
|
||||||
|
|
||||||
|
with nova_context.target_cell(context, im.cell_mapping) as cctxt:
|
||||||
|
# Ensure the instance exists
|
||||||
|
self._get_instance_from_server_uuid(cctxt, server_id)
|
||||||
|
db_shares = objects.ShareMappingList.get_by_instance_uuid(
|
||||||
|
cctxt, server_id
|
||||||
|
)
|
||||||
|
|
||||||
|
return self._view_builder._list_view(db_shares)
|
||||||
|
|
||||||
|
@wsgi.Controller.api_version("2.97")
|
||||||
|
@wsgi.response(201)
|
||||||
|
@wsgi.expected_errors((400, 403, 404, 409))
|
||||||
|
@validation.schema(schema.create, min_version='2.97')
|
||||||
|
@validation.response_body_schema(schema.share_response)
|
||||||
|
def create(self, req, server_id, body):
|
||||||
|
def _try_create_share_mapping(context, share_mapping):
|
||||||
|
"""Block the request if the share is already created.
|
||||||
|
Prevent race conditions of requests that would hit the
|
||||||
|
share_mapping.create() almost at the same time.
|
||||||
|
Prevent user from using the same tag twice on the same instance.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
objects.ShareMapping.get_by_instance_uuid_and_share_id(context,
|
||||||
|
share_mapping.instance_uuid, share_mapping.share_id
|
||||||
|
)
|
||||||
|
raise exception.ShareMappingAlreadyExists(
|
||||||
|
share_id=share_mapping.share_id, tag=share_mapping.tag
|
||||||
|
)
|
||||||
|
except exception.ShareNotFound:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
share_mapping.create()
|
||||||
|
except db_exc.DBDuplicateEntry:
|
||||||
|
raise exception.ShareMappingAlreadyExists(
|
||||||
|
share_id=share_mapping.share_id, tag=share_mapping.tag
|
||||||
|
)
|
||||||
|
|
||||||
|
def _check_manila_share(manila_share_data):
|
||||||
|
"""Check that the targeted share in manila has
|
||||||
|
correct export location, status 'available' and a supported
|
||||||
|
protocol.
|
||||||
|
"""
|
||||||
|
if manila_share_data.status != 'available':
|
||||||
|
raise exception.ShareStatusIncorect(
|
||||||
|
share_id=share_id, status=manila_share_data.status
|
||||||
|
)
|
||||||
|
|
||||||
|
if manila_share_data.export_location is None:
|
||||||
|
raise exception.ShareMissingExportLocation(share_id=share_id)
|
||||||
|
|
||||||
|
if (
|
||||||
|
manila_share_data.share_proto
|
||||||
|
not in fields.ShareMappingProto.ALL
|
||||||
|
):
|
||||||
|
raise exception.ShareProtocolNotSupported(
|
||||||
|
share_proto=manila_share_data.share_proto
|
||||||
|
)
|
||||||
|
|
||||||
|
context = req.environ["nova.context"]
|
||||||
|
# Get instance mapping to query the required cell database
|
||||||
|
im = _get_instance_mapping(context, server_id)
|
||||||
|
context.can(
|
||||||
|
ss_policies.POLICY_ROOT % 'create',
|
||||||
|
target={'project_id': im.project_id}
|
||||||
|
)
|
||||||
|
|
||||||
|
share_dict = body['share']
|
||||||
|
share_id = share_dict.get('share_id')
|
||||||
|
with nova_context.target_cell(context, im.cell_mapping) as cctxt:
|
||||||
|
instance = self._check_instance_in_valid_state(
|
||||||
|
cctxt,
|
||||||
|
server_id,
|
||||||
|
"create share"
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
hw.check_shares_supported(cctxt, instance)
|
||||||
|
|
||||||
|
manila_share_data = self.manila.get(cctxt, share_id)
|
||||||
|
_check_manila_share(manila_share_data)
|
||||||
|
|
||||||
|
share_mapping = objects.ShareMapping(cctxt)
|
||||||
|
share_mapping.uuid = uuidutils.generate_uuid()
|
||||||
|
share_mapping.instance_uuid = server_id
|
||||||
|
share_mapping.share_id = manila_share_data.id
|
||||||
|
share_mapping.status = fields.ShareMappingStatus.ATTACHING
|
||||||
|
share_mapping.tag = share_dict.get('tag', manila_share_data.id)
|
||||||
|
share_mapping.export_location = (
|
||||||
|
manila_share_data.export_location)
|
||||||
|
share_mapping.share_proto = manila_share_data.share_proto
|
||||||
|
|
||||||
|
_try_create_share_mapping(cctxt, share_mapping)
|
||||||
|
self.compute_api.allow_share(cctxt, instance, share_mapping)
|
||||||
|
|
||||||
|
view = self._view_builder._show_view(cctxt, share_mapping)
|
||||||
|
|
||||||
|
except (exception.ShareNotFound) as e:
|
||||||
|
raise webob.exc.HTTPNotFound(explanation=e.format_message())
|
||||||
|
except (exception.ShareStatusIncorect) as e:
|
||||||
|
raise webob.exc.HTTPConflict(explanation=e.format_message())
|
||||||
|
except (exception.ShareMissingExportLocation) as e:
|
||||||
|
raise webob.exc.HTTPConflict(explanation=e.format_message())
|
||||||
|
except (exception.ShareProtocolNotSupported) as e:
|
||||||
|
raise webob.exc.HTTPConflict(explanation=e.format_message())
|
||||||
|
except (exception.ShareMappingAlreadyExists) as e:
|
||||||
|
raise webob.exc.HTTPConflict(explanation=e.format_message())
|
||||||
|
except (exception.ForbiddenSharesNotSupported) as e:
|
||||||
|
raise webob.exc.HTTPForbidden(explanation=e.format_message())
|
||||||
|
except (exception.ForbiddenSharesNotConfiguredCorrectly) as e:
|
||||||
|
raise webob.exc.HTTPConflict(explanation=e.format_message())
|
||||||
|
|
||||||
|
return view
|
||||||
|
|
||||||
|
@wsgi.Controller.api_version("2.97")
|
||||||
|
@wsgi.response(200)
|
||||||
|
@wsgi.expected_errors((400, 403, 404))
|
||||||
|
@validation.query_schema(schema.show_query)
|
||||||
|
@validation.response_body_schema(schema.share_response)
|
||||||
|
def show(self, req, server_id, id):
|
||||||
|
context = req.environ["nova.context"]
|
||||||
|
# Get instance mapping to query the required cell database
|
||||||
|
im = _get_instance_mapping(context, server_id)
|
||||||
|
context.can(
|
||||||
|
ss_policies.POLICY_ROOT % 'show',
|
||||||
|
target={'project_id': im.project_id}
|
||||||
|
)
|
||||||
|
|
||||||
|
with nova_context.target_cell(context, im.cell_mapping) as cctxt:
|
||||||
|
try:
|
||||||
|
# Ensure the instance exists
|
||||||
|
self._get_instance_from_server_uuid(cctxt, server_id)
|
||||||
|
share = objects.ShareMapping.get_by_instance_uuid_and_share_id(
|
||||||
|
cctxt,
|
||||||
|
server_id,
|
||||||
|
id
|
||||||
|
)
|
||||||
|
|
||||||
|
view = self._view_builder._show_view(cctxt, share)
|
||||||
|
|
||||||
|
except (exception.ShareNotFound) as e:
|
||||||
|
raise webob.exc.HTTPNotFound(explanation=e.format_message())
|
||||||
|
|
||||||
|
return view
|
||||||
|
|
||||||
|
@wsgi.Controller.api_version("2.97")
|
||||||
|
@wsgi.response(200)
|
||||||
|
@wsgi.expected_errors((400, 403, 404, 409))
|
||||||
|
def delete(self, req, server_id, id):
|
||||||
|
context = req.environ["nova.context"]
|
||||||
|
# Get instance mapping to query the required cell database
|
||||||
|
im = _get_instance_mapping(context, server_id)
|
||||||
|
context.can(
|
||||||
|
ss_policies.POLICY_ROOT % 'delete',
|
||||||
|
target={'project_id': im.project_id}
|
||||||
|
)
|
||||||
|
|
||||||
|
with nova_context.target_cell(context, im.cell_mapping) as cctxt:
|
||||||
|
instance = self._check_instance_in_valid_state(
|
||||||
|
cctxt,
|
||||||
|
server_id,
|
||||||
|
"delete share"
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
# Ensure the instance exists
|
||||||
|
self._get_instance_from_server_uuid(cctxt, server_id)
|
||||||
|
share_mapping = (
|
||||||
|
objects.ShareMapping.get_by_instance_uuid_and_share_id(
|
||||||
|
cctxt, server_id, id
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
share_mapping.status = fields.ShareMappingStatus.DETACHING
|
||||||
|
share_mapping.save()
|
||||||
|
self.compute_api.deny_share(cctxt, instance, share_mapping)
|
||||||
|
|
||||||
|
except (exception.ShareNotFound) as e:
|
||||||
|
raise webob.exc.HTTPNotFound(explanation=e.format_message())
|
||||||
@@ -1099,6 +1099,10 @@ class ServersController(wsgi.Controller):
|
|||||||
except exception.Invalid:
|
except exception.Invalid:
|
||||||
msg = _("Invalid instance image.")
|
msg = _("Invalid instance image.")
|
||||||
raise exc.HTTPBadRequest(explanation=msg)
|
raise exc.HTTPBadRequest(explanation=msg)
|
||||||
|
except (
|
||||||
|
exception.ForbiddenSharesNotSupported,
|
||||||
|
exception.ForbiddenWithShare) as e:
|
||||||
|
raise exc.HTTPConflict(explanation=e.format_message())
|
||||||
|
|
||||||
@wsgi.response(204)
|
@wsgi.response(204)
|
||||||
@wsgi.expected_errors((404, 409))
|
@wsgi.expected_errors((404, 409))
|
||||||
|
|||||||
@@ -59,6 +59,10 @@ class ShelveController(wsgi.Controller):
|
|||||||
'shelve', id)
|
'shelve', id)
|
||||||
except exception.ForbiddenPortsWithAccelerator as e:
|
except exception.ForbiddenPortsWithAccelerator as e:
|
||||||
raise exc.HTTPBadRequest(explanation=e.format_message())
|
raise exc.HTTPBadRequest(explanation=e.format_message())
|
||||||
|
except (
|
||||||
|
exception.ForbiddenSharesNotSupported,
|
||||||
|
exception.ForbiddenWithShare) as e:
|
||||||
|
raise exc.HTTPConflict(explanation=e.format_message())
|
||||||
|
|
||||||
@wsgi.response(202)
|
@wsgi.response(202)
|
||||||
@wsgi.expected_errors((400, 404, 409))
|
@wsgi.expected_errors((400, 404, 409))
|
||||||
|
|||||||
@@ -49,6 +49,10 @@ class SuspendServerController(wsgi.Controller):
|
|||||||
'suspend', id)
|
'suspend', id)
|
||||||
except exception.ForbiddenPortsWithAccelerator as e:
|
except exception.ForbiddenPortsWithAccelerator as e:
|
||||||
raise exc.HTTPBadRequest(explanation=e.format_message())
|
raise exc.HTTPBadRequest(explanation=e.format_message())
|
||||||
|
except (
|
||||||
|
exception.ForbiddenSharesNotSupported,
|
||||||
|
exception.ForbiddenWithShare) as e:
|
||||||
|
raise exc.HTTPConflict(explanation=e.format_message())
|
||||||
|
|
||||||
@wsgi.response(202)
|
@wsgi.response(202)
|
||||||
@wsgi.expected_errors((404, 409))
|
@wsgi.expected_errors((404, 409))
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
# 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 nova.api.openstack import common
|
||||||
|
from nova.api.openstack.compute.views import servers
|
||||||
|
|
||||||
|
|
||||||
|
class ViewBuilder(common.ViewBuilder):
|
||||||
|
_collection_name = 'shares'
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super(ViewBuilder, self).__init__()
|
||||||
|
self._server_builder = servers.ViewBuilder()
|
||||||
|
|
||||||
|
def _list_view(self, db_shares):
|
||||||
|
shares = {'shares': []}
|
||||||
|
for db_share in db_shares:
|
||||||
|
share = {
|
||||||
|
'share_id': db_share.share_id,
|
||||||
|
'status': db_share.status,
|
||||||
|
'tag': db_share.tag,
|
||||||
|
}
|
||||||
|
shares['shares'].append(share)
|
||||||
|
return shares
|
||||||
|
|
||||||
|
def _show_view(self, context, db_share):
|
||||||
|
share = {'share': {
|
||||||
|
'share_id': db_share.share_id,
|
||||||
|
'status': db_share.status,
|
||||||
|
'tag': db_share.tag,
|
||||||
|
}}
|
||||||
|
|
||||||
|
if context.is_admin:
|
||||||
|
share['share']['export_location'] = db_share.export_location
|
||||||
|
share['share']['uuid'] = db_share.uuid
|
||||||
|
|
||||||
|
return share
|
||||||
@@ -357,6 +357,23 @@ image_id = {
|
|||||||
'type': 'string', 'format': 'uuid'
|
'type': 'string', 'format': 'uuid'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
share_id = {
|
||||||
|
'type': 'string', 'format': 'uuid'
|
||||||
|
}
|
||||||
|
|
||||||
|
share_tag = {
|
||||||
|
'type': 'string', 'minLength': 1, 'maxLength': 255,
|
||||||
|
'pattern': '^[a-zA-Z0-9-]*$'
|
||||||
|
}
|
||||||
|
|
||||||
|
share_export_location = {
|
||||||
|
'type': 'string'
|
||||||
|
}
|
||||||
|
|
||||||
|
share_status = {
|
||||||
|
'type': 'string',
|
||||||
|
'enum': ['active', 'inactive', 'attaching', 'detaching', 'error']
|
||||||
|
}
|
||||||
|
|
||||||
image_id_or_empty_string = {
|
image_id_or_empty_string = {
|
||||||
'oneOf': [
|
'oneOf': [
|
||||||
|
|||||||
+11
-1
@@ -721,14 +721,24 @@ class ShareNotFound(NotFound):
|
|||||||
msg_fmt = _("Share %(share_id)s could not be found.")
|
msg_fmt = _("Share %(share_id)s could not be found.")
|
||||||
|
|
||||||
|
|
||||||
|
class ShareStatusIncorect(NotFound):
|
||||||
|
msg_fmt = _("Share %(share_id)s is in '%(status)s' instead of "
|
||||||
|
"'available' status.")
|
||||||
|
|
||||||
|
|
||||||
class ShareMappingAlreadyExists(NotFound):
|
class ShareMappingAlreadyExists(NotFound):
|
||||||
msg_fmt = _("Share %(share_id)s already associated to this server.")
|
msg_fmt = _("Share '%(share_id)s' or tag '%(tag)s' already associated "
|
||||||
|
"to this server.")
|
||||||
|
|
||||||
|
|
||||||
class ShareProtocolNotSupported(NotFound):
|
class ShareProtocolNotSupported(NotFound):
|
||||||
msg_fmt = _("Share protocol %(share_proto)s is not supported.")
|
msg_fmt = _("Share protocol %(share_proto)s is not supported.")
|
||||||
|
|
||||||
|
|
||||||
|
class ShareMissingExportLocation(NotFound):
|
||||||
|
msg_fmt = _("Share %(share_id)s export location is missing.")
|
||||||
|
|
||||||
|
|
||||||
class ShareError(NovaException):
|
class ShareError(NovaException):
|
||||||
msg_fmt = _("Share %(share_id)s used by instance %(instance_uuid)s "
|
msg_fmt = _("Share %(share_id)s used by instance %(instance_uuid)s "
|
||||||
"is in error state.")
|
"is in error state.")
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ from nova.policies import server_external_events
|
|||||||
from nova.policies import server_groups
|
from nova.policies import server_groups
|
||||||
from nova.policies import server_metadata
|
from nova.policies import server_metadata
|
||||||
from nova.policies import server_password
|
from nova.policies import server_password
|
||||||
|
from nova.policies import server_shares
|
||||||
from nova.policies import server_tags
|
from nova.policies import server_tags
|
||||||
from nova.policies import server_topology
|
from nova.policies import server_topology
|
||||||
from nova.policies import servers
|
from nova.policies import servers
|
||||||
@@ -114,6 +115,7 @@ def list_rules():
|
|||||||
server_groups.list_rules(),
|
server_groups.list_rules(),
|
||||||
server_metadata.list_rules(),
|
server_metadata.list_rules(),
|
||||||
server_password.list_rules(),
|
server_password.list_rules(),
|
||||||
|
server_shares.list_rules(),
|
||||||
server_tags.list_rules(),
|
server_tags.list_rules(),
|
||||||
server_topology.list_rules(),
|
server_topology.list_rules(),
|
||||||
servers.list_rules(),
|
servers.list_rules(),
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
# 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 nova.policies import base
|
||||||
|
|
||||||
|
|
||||||
|
POLICY_ROOT = 'os_compute_api:os-server-shares:%s'
|
||||||
|
|
||||||
|
|
||||||
|
server_shares_policies = [
|
||||||
|
policy.DocumentedRuleDefault(
|
||||||
|
name=POLICY_ROOT % 'index',
|
||||||
|
check_str=base.PROJECT_READER,
|
||||||
|
description="List all shares for given server",
|
||||||
|
operations=[
|
||||||
|
{
|
||||||
|
'method': 'GET',
|
||||||
|
'path': '/servers/{server_id}/shares'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
scope_types=['project']),
|
||||||
|
policy.DocumentedRuleDefault(
|
||||||
|
name=POLICY_ROOT % 'create',
|
||||||
|
check_str=base.PROJECT_MEMBER,
|
||||||
|
description="Attach a share to the specified server",
|
||||||
|
operations=[
|
||||||
|
{
|
||||||
|
'method': 'POST',
|
||||||
|
'path': '/servers/{server_id}/shares'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
scope_types=['project']),
|
||||||
|
policy.DocumentedRuleDefault(
|
||||||
|
name=POLICY_ROOT % 'show',
|
||||||
|
check_str=base.PROJECT_READER,
|
||||||
|
description="Show a share configured for the specified server",
|
||||||
|
operations=[
|
||||||
|
{
|
||||||
|
'method': 'GET',
|
||||||
|
'path': '/servers/{server_id}/shares/{share_id}'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
scope_types=['project']),
|
||||||
|
policy.DocumentedRuleDefault(
|
||||||
|
name=POLICY_ROOT % 'delete',
|
||||||
|
check_str=base.PROJECT_MEMBER,
|
||||||
|
description="Detach a share to the specified server",
|
||||||
|
operations=[
|
||||||
|
{
|
||||||
|
'method': 'DELETE',
|
||||||
|
'path': '/servers/{server_id}/shares/{share_id}'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
scope_types=['project']),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def list_rules():
|
||||||
|
return server_shares_policies
|
||||||
@@ -203,12 +203,15 @@ class API(object):
|
|||||||
def filter_export_locations(export_locations):
|
def filter_export_locations(export_locations):
|
||||||
# Return the preferred path otherwise choose the first one
|
# Return the preferred path otherwise choose the first one
|
||||||
paths = []
|
paths = []
|
||||||
for export_location in export_locations:
|
try:
|
||||||
if export_location.is_preferred:
|
for export_location in export_locations:
|
||||||
return export_location.path
|
if export_location.is_preferred:
|
||||||
else:
|
return export_location.path
|
||||||
paths.append(export_location.path)
|
else:
|
||||||
return paths[0]
|
paths.append(export_location.path)
|
||||||
|
return paths[0]
|
||||||
|
except (IndexError, NameError):
|
||||||
|
return None
|
||||||
|
|
||||||
client = _manilaclient(context, admin=False)
|
client = _manilaclient(context, admin=False)
|
||||||
LOG.debug("Get share id:'%s' data from manila", share_id)
|
LOG.debug("Get share id:'%s' data from manila", share_id)
|
||||||
|
|||||||
Vendored
+23
@@ -80,6 +80,29 @@ class ManilaFixture(fixtures.Fixture):
|
|||||||
manila_share, export_location
|
manila_share, export_location
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def fake_get_share_status_error(self, context, share_id):
|
||||||
|
manila_share = ManilaShare(share_id)
|
||||||
|
manila_share.status = "error"
|
||||||
|
export_location = "10.0.0.50:/mnt/foo"
|
||||||
|
return nova.share.manila.Share.from_manila_share(
|
||||||
|
manila_share, export_location
|
||||||
|
)
|
||||||
|
|
||||||
|
def fake_get_share_export_location_missing(self, context, share_id):
|
||||||
|
manila_share = ManilaShare(share_id)
|
||||||
|
export_location = None
|
||||||
|
return nova.share.manila.Share.from_manila_share(
|
||||||
|
manila_share, export_location
|
||||||
|
)
|
||||||
|
|
||||||
|
def fake_get_share_unknown_protocol(self, context, share_id):
|
||||||
|
manila_share = ManilaShare(share_id)
|
||||||
|
manila_share.share_protocol = "CIFS"
|
||||||
|
export_location = "10.0.0.50:/mnt/foo"
|
||||||
|
return nova.share.manila.Share.from_manila_share(
|
||||||
|
manila_share, export_location
|
||||||
|
)
|
||||||
|
|
||||||
def fake_get_cephfs(self, context, share_id):
|
def fake_get_cephfs(self, context, share_id):
|
||||||
manila_share = ManilaShare(share_id, "CEPHFS")
|
manila_share = ManilaShare(share_id, "CEPHFS")
|
||||||
export_location = "10.0.0.50:/mnt/foo"
|
export_location = "10.0.0.50:/mnt/foo"
|
||||||
|
|||||||
+10
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"share":
|
||||||
|
{
|
||||||
|
"uuid": "%(uuid)s",
|
||||||
|
"share_id": "%(share_id)s",
|
||||||
|
"status": "attaching",
|
||||||
|
"export_location": "10.0.0.50:/mnt/foo",
|
||||||
|
"tag": "%(share_id)s"
|
||||||
|
}
|
||||||
|
}
|
||||||
+10
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"share":
|
||||||
|
{
|
||||||
|
"uuid": "%(uuid)s",
|
||||||
|
"share_id": "%(share_id)s",
|
||||||
|
"status": "inactive",
|
||||||
|
"export_location": "10.0.0.50:/mnt/foo",
|
||||||
|
"tag": "%(share_id)s"
|
||||||
|
}
|
||||||
|
}
|
||||||
+5
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"share": {
|
||||||
|
"share_id": "%(share_id)s"
|
||||||
|
}
|
||||||
|
}
|
||||||
+8
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"share":
|
||||||
|
{
|
||||||
|
"share_id": "%(share_id)s",
|
||||||
|
"status": "attaching",
|
||||||
|
"tag": "%(share_id)s"
|
||||||
|
}
|
||||||
|
}
|
||||||
+6
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"share": {
|
||||||
|
"share_id": "%(share_id)s",
|
||||||
|
"tag": "%(tag)s"
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
server-shares-create-req.json.tpl
|
||||||
+9
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"shares": [
|
||||||
|
{
|
||||||
|
"share_id": "e8debdc0-447a-4376-a10a-4cd9122d7986",
|
||||||
|
"status": "inactive",
|
||||||
|
"tag": "e8debdc0-447a-4376-a10a-4cd9122d7986"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
+8
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"share":
|
||||||
|
{
|
||||||
|
"share_id": "%(share_id)s",
|
||||||
|
"status": "inactive",
|
||||||
|
"tag": "%(share_id)s"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,482 @@
|
|||||||
|
# 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 nova.compute import api as compute
|
||||||
|
from nova import exception
|
||||||
|
from nova.tests import fixtures as nova_fixtures
|
||||||
|
from nova.tests.functional.api import client
|
||||||
|
from nova.tests.functional.api_sample_tests import test_servers
|
||||||
|
from oslo_utils.fixture import uuidsentinel
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
|
||||||
|
class ServerSharesBase(test_servers.ServersSampleBase):
|
||||||
|
sample_dir = 'os-server-shares'
|
||||||
|
microversion = '2.97'
|
||||||
|
scenarios = [('v2_97', {'api_major_version': 'v2.1'})]
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(ServerSharesBase, self).setUp()
|
||||||
|
self.manila_fixture = self.useFixture(nova_fixtures.ManilaFixture())
|
||||||
|
self.compute_api = compute.API()
|
||||||
|
|
||||||
|
def _get_create_subs(self):
|
||||||
|
return {'share_id': 'e8debdc0-447a-4376-a10a-4cd9122d7986',
|
||||||
|
'uuid': '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}'
|
||||||
|
'-[0-9a-f]{4}-[0-9a-f]{12}',
|
||||||
|
}
|
||||||
|
|
||||||
|
def create_server_ok(self, requested_flavor=None):
|
||||||
|
flavor = self._create_flavor(extra_spec=requested_flavor)
|
||||||
|
server = self._create_server(networks='auto', flavor_id=flavor)
|
||||||
|
self._stop_server(server)
|
||||||
|
return server['id']
|
||||||
|
|
||||||
|
def create_server_not_stopped(self):
|
||||||
|
server = self._create_server(networks='auto')
|
||||||
|
return server['id']
|
||||||
|
|
||||||
|
def _post_server_shares(self):
|
||||||
|
"""Verify the response status and returns the UUID of the
|
||||||
|
newly created server with shares.
|
||||||
|
"""
|
||||||
|
uuid = self.create_server_ok()
|
||||||
|
subs = self._get_create_subs()
|
||||||
|
response = self._do_post(
|
||||||
|
"servers/%s/shares" % uuid, "server-shares-create-req", subs
|
||||||
|
)
|
||||||
|
|
||||||
|
self._verify_response(
|
||||||
|
'server-shares-create-resp', subs, response, 201)
|
||||||
|
|
||||||
|
return uuid
|
||||||
|
|
||||||
|
|
||||||
|
class ServerSharesJsonTest(ServerSharesBase):
|
||||||
|
def test_server_shares_create(self):
|
||||||
|
"""Verify we can create a share mapping.
|
||||||
|
"""
|
||||||
|
self._post_server_shares()
|
||||||
|
|
||||||
|
def test_server_shares_create_fails_if_already_created(self):
|
||||||
|
"""Verify we cannot create a share mapping already created.
|
||||||
|
"""
|
||||||
|
uuid = self._post_server_shares()
|
||||||
|
# Following mock simulate a race condition between 2 requests that
|
||||||
|
# would hit the share_mapping.create() almost at the same time.
|
||||||
|
with mock.patch(
|
||||||
|
"nova.db.main.api.share_mapping_get_by_instance_uuid_and_share_id"
|
||||||
|
) as mock_db:
|
||||||
|
mock_db.return_value = None
|
||||||
|
subs = self._get_create_subs()
|
||||||
|
response = self._do_post(
|
||||||
|
"servers/%s/shares" % uuid, "server-shares-create-req", subs
|
||||||
|
)
|
||||||
|
self.assertEqual(409, response.status_code)
|
||||||
|
self.assertIn('already associated to this server', response.text)
|
||||||
|
|
||||||
|
def test_server_shares_create_with_tag_fails_if_already_created(self):
|
||||||
|
"""Verify we cannot create a share mapping with a new tag if it is
|
||||||
|
already created.
|
||||||
|
"""
|
||||||
|
uuid = self._post_server_shares()
|
||||||
|
subs = self._get_create_subs()
|
||||||
|
subs['tag'] = "my-tag"
|
||||||
|
response = self._do_post(
|
||||||
|
"servers/%s/shares" % uuid, "server-shares-create-tag-req", subs
|
||||||
|
)
|
||||||
|
self.assertEqual(409, response.status_code)
|
||||||
|
self.assertIn(
|
||||||
|
"Share 'e8debdc0-447a-4376-a10a-4cd9122d7986' or "
|
||||||
|
"tag 'my-tag' already associated to this server.",
|
||||||
|
response.text,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_server_shares_create_fails_instance_not_stopped(self):
|
||||||
|
"""Verify we cannot create a share if instance is not stopped.
|
||||||
|
"""
|
||||||
|
uuid = self.create_server_not_stopped()
|
||||||
|
subs = self._get_create_subs()
|
||||||
|
response = self._do_post(
|
||||||
|
"servers/%s/shares" % uuid, "server-shares-create-req", subs
|
||||||
|
)
|
||||||
|
self.assertEqual(409, response.status_code)
|
||||||
|
self.assertIn('while it is in vm_state active', response.text)
|
||||||
|
|
||||||
|
def test_server_shares_create_fails_incorrect_configuration(self):
|
||||||
|
"""Verify we cannot create a share we don't have the
|
||||||
|
appropriate configuration.
|
||||||
|
"""
|
||||||
|
with mock.patch.dict(self.compute.driver.capabilities,
|
||||||
|
supports_mem_backing_file=False):
|
||||||
|
self.compute.stop()
|
||||||
|
self.compute.start()
|
||||||
|
uuid = self.create_server_ok()
|
||||||
|
subs = self._get_create_subs()
|
||||||
|
response = self._do_post('servers/%s/shares' % uuid,
|
||||||
|
'server-shares-create-req', subs)
|
||||||
|
self.assertEqual(409, response.status_code)
|
||||||
|
self.assertIn(
|
||||||
|
'Feature not supported because either compute or '
|
||||||
|
'instance are not configured correctly.', response.text
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_server_shares_create_fails_cannot_allow_policy(self):
|
||||||
|
"""Verify we raise an exception if we get a timeout to apply policy"""
|
||||||
|
uuid = self.create_server_ok()
|
||||||
|
subs = self._get_create_subs()
|
||||||
|
# simulate that manila does not set the requested access in time and
|
||||||
|
# nova times out waiting for it.
|
||||||
|
self.manila_fixture.mock_get_access.return_value = None
|
||||||
|
self.manila_fixture.mock_get_access.side_effect = None
|
||||||
|
self.flags(share_apply_policy_timeout=2, group='manila')
|
||||||
|
|
||||||
|
# Here we are using CastAsCallFixture so we got an exception from
|
||||||
|
# nova api. This should not happen without the fixture.
|
||||||
|
response = self._do_post(
|
||||||
|
"servers/%s/shares" % uuid, "server-shares-create-req", subs
|
||||||
|
)
|
||||||
|
self.assertEqual(500, response.status_code)
|
||||||
|
self.assertIn(
|
||||||
|
"nova.exception.ShareAccessGrantError",
|
||||||
|
response.text,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_server_shares_create_with_alternative_flavor(self):
|
||||||
|
"""Verify we can create a share with the proper flavor.
|
||||||
|
"""
|
||||||
|
with mock.patch.dict(self.compute.driver.capabilities,
|
||||||
|
supports_mem_backing_file=False):
|
||||||
|
self.compute.stop()
|
||||||
|
self.compute.start()
|
||||||
|
uuid = self.create_server_ok(
|
||||||
|
requested_flavor={"hw:mem_page_size": "large"}
|
||||||
|
)
|
||||||
|
subs = self._get_create_subs()
|
||||||
|
response = self._do_post(
|
||||||
|
"servers/%s/shares" % uuid, "server-shares-create-req", subs
|
||||||
|
)
|
||||||
|
self.assertEqual(201, response.status_code)
|
||||||
|
|
||||||
|
def test_server_shares_create_fails_share_not_found(self):
|
||||||
|
"""Verify we can not create a share if the share does not
|
||||||
|
exists.
|
||||||
|
"""
|
||||||
|
self.manila_fixture.mock_get.side_effect = exception.ShareNotFound(
|
||||||
|
share_id='fake_uuid')
|
||||||
|
uuid = self.create_server_ok()
|
||||||
|
subs = self._get_create_subs()
|
||||||
|
response = self._do_post(
|
||||||
|
"servers/%s/shares" % uuid, "server-shares-create-req", subs
|
||||||
|
)
|
||||||
|
self.assertEqual(404, response.status_code)
|
||||||
|
self.assertIn("Share fake_uuid could not be found", response.text)
|
||||||
|
|
||||||
|
def test_server_shares_create_unknown_instance(self):
|
||||||
|
"""Verify creating a share on an unknown instance reports an error.
|
||||||
|
"""
|
||||||
|
self.create_server_ok()
|
||||||
|
subs = self._get_create_subs()
|
||||||
|
response = self._do_post(
|
||||||
|
"servers/%s/shares" % uuidsentinel.fake_uuid,
|
||||||
|
"server-shares-create-req",
|
||||||
|
subs,
|
||||||
|
)
|
||||||
|
self.assertEqual(404, response.status_code)
|
||||||
|
self.assertIn("could not be found", response.text)
|
||||||
|
|
||||||
|
def test_server_shares_create_fails_share_in_error(self):
|
||||||
|
"""Verify creating a share which is in error reports an error.
|
||||||
|
"""
|
||||||
|
uuid = self.create_server_ok()
|
||||||
|
subs = self._get_create_subs()
|
||||||
|
self.manila_fixture.mock_get.side_effect = (
|
||||||
|
self.manila_fixture.fake_get_share_status_error
|
||||||
|
)
|
||||||
|
|
||||||
|
response = self._do_post(
|
||||||
|
"servers/%s/shares" % uuid, "server-shares-create-req", subs
|
||||||
|
)
|
||||||
|
self.assertEqual(409, response.status_code)
|
||||||
|
self.assertIn(
|
||||||
|
"Share e8debdc0-447a-4376-a10a-4cd9122d7986 is in 'error' "
|
||||||
|
"instead of 'available' status.",
|
||||||
|
response.text,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_server_shares_create_fails_export_location_missing(self):
|
||||||
|
"""Verify creating a share without export location reports an error.
|
||||||
|
"""
|
||||||
|
uuid = self.create_server_ok()
|
||||||
|
subs = self._get_create_subs()
|
||||||
|
self.manila_fixture.mock_get.side_effect = (
|
||||||
|
self.manila_fixture.fake_get_share_export_location_missing
|
||||||
|
)
|
||||||
|
|
||||||
|
response = self._do_post(
|
||||||
|
"servers/%s/shares" % uuid, "server-shares-create-req", subs
|
||||||
|
)
|
||||||
|
self.assertEqual(409, response.status_code)
|
||||||
|
self.assertIn(
|
||||||
|
"Share e8debdc0-447a-4376-a10a-4cd9122d7986 export location is "
|
||||||
|
"missing.",
|
||||||
|
response.text,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_server_shares_create_fails_unknown_protocol(self):
|
||||||
|
"""Verify creating a share with an unknown protocol reports an error.
|
||||||
|
"""
|
||||||
|
uuid = self.create_server_ok()
|
||||||
|
subs = self._get_create_subs()
|
||||||
|
self.manila_fixture.mock_get.side_effect = (
|
||||||
|
self.manila_fixture.fake_get_share_unknown_protocol
|
||||||
|
)
|
||||||
|
|
||||||
|
response = self._do_post(
|
||||||
|
"servers/%s/shares" % uuid, "server-shares-create-req", subs
|
||||||
|
)
|
||||||
|
self.assertEqual(409, response.status_code)
|
||||||
|
self.assertIn("Share protocol CIFS is not supported.", response.text)
|
||||||
|
|
||||||
|
def test_server_shares_index(self):
|
||||||
|
"""Verify we can list shares.
|
||||||
|
"""
|
||||||
|
uuid = self._post_server_shares()
|
||||||
|
subs = self._get_create_subs()
|
||||||
|
response = self._do_get("servers/%s/shares" % uuid)
|
||||||
|
self._verify_response("server-shares-list-resp", subs, response, 200)
|
||||||
|
|
||||||
|
def test_server_shares_index_unknown_instance(self):
|
||||||
|
"""Verify getting shares on an unknown instance reports an error.
|
||||||
|
"""
|
||||||
|
response = self._do_get('servers/%s/shares' % uuidsentinel.fake_uuid)
|
||||||
|
self.assertEqual(404, response.status_code)
|
||||||
|
self.assertIn(
|
||||||
|
"could not be found",
|
||||||
|
response.text
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_server_shares_show(self):
|
||||||
|
"""Verify we can show a share.
|
||||||
|
"""
|
||||||
|
uuid = self._post_server_shares()
|
||||||
|
subs = self._get_create_subs()
|
||||||
|
response = self._do_get(
|
||||||
|
"servers/%s/shares/%s" % (uuid, subs["share_id"])
|
||||||
|
)
|
||||||
|
self._verify_response("server-shares-show-resp", subs, response, 200)
|
||||||
|
|
||||||
|
def test_server_shares_show_fails_share_not_found(self):
|
||||||
|
"""Verify we can not show a share if the share does not
|
||||||
|
exists.
|
||||||
|
"""
|
||||||
|
uuid = self.create_server_ok()
|
||||||
|
subs = self._get_create_subs()
|
||||||
|
response = self._do_get(
|
||||||
|
"servers/%s/shares/%s" % (uuid, subs["share_id"])
|
||||||
|
)
|
||||||
|
self.assertEqual(404, response.status_code)
|
||||||
|
self.assertIn(
|
||||||
|
"Share e8debdc0-447a-4376-a10a-4cd9122d7986 could not be found",
|
||||||
|
response.text
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_server_shares_show_unknown_instance(self):
|
||||||
|
"""Verify showing a share on an unknown instance reports an error.
|
||||||
|
"""
|
||||||
|
self._post_server_shares()
|
||||||
|
subs = self._get_create_subs()
|
||||||
|
response = self._do_get(
|
||||||
|
"servers/%s/shares/%s" % (uuidsentinel.fake_uuid, subs["share_id"])
|
||||||
|
)
|
||||||
|
self.assertEqual(404, response.status_code)
|
||||||
|
self.assertIn(
|
||||||
|
"could not be found",
|
||||||
|
response.text
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_server_shares_delete(self):
|
||||||
|
"""Verify we can delete share.
|
||||||
|
"""
|
||||||
|
uuid = self._post_server_shares()
|
||||||
|
subs = self._get_create_subs()
|
||||||
|
response = self._do_delete(
|
||||||
|
"servers/%s/shares/%s" % (uuid, subs["share_id"])
|
||||||
|
)
|
||||||
|
self.assertEqual(200, response.status_code)
|
||||||
|
|
||||||
|
# Check share is not anymore available
|
||||||
|
response = self._do_get(
|
||||||
|
"servers/%s/shares/%s" % (uuid, subs["share_id"])
|
||||||
|
)
|
||||||
|
self.assertEqual(404, response.status_code)
|
||||||
|
|
||||||
|
def test_server_shares_delete_instance(self):
|
||||||
|
"""Verify we can delete an instance and its associated share is
|
||||||
|
deleted as well.
|
||||||
|
"""
|
||||||
|
uuid = self._post_server_shares()
|
||||||
|
subs = self._get_create_subs()
|
||||||
|
|
||||||
|
# Check share is created
|
||||||
|
response = self._do_get(
|
||||||
|
"servers/%s/shares/%s" % (uuid, subs["share_id"])
|
||||||
|
)
|
||||||
|
self._verify_response("server-shares-show-resp", subs, response, 200)
|
||||||
|
|
||||||
|
# Delete the instance
|
||||||
|
response = self._do_delete(
|
||||||
|
"servers/%s" % (uuid)
|
||||||
|
)
|
||||||
|
self.assertEqual(204, response.status_code)
|
||||||
|
|
||||||
|
# Check share is not anymore available
|
||||||
|
response = self._do_get(
|
||||||
|
"servers/%s/shares/%s" % (uuid, subs["share_id"])
|
||||||
|
)
|
||||||
|
self.assertEqual(404, response.status_code)
|
||||||
|
|
||||||
|
def test_server_shares_delete_fails_share_not_found(self):
|
||||||
|
"""Verify we have an error if we want to remove an unknown share.
|
||||||
|
"""
|
||||||
|
uuid = self._post_server_shares()
|
||||||
|
response = self._do_delete(
|
||||||
|
"servers/%s/shares/%s" % (uuid, uuidsentinel.wrong_share_id)
|
||||||
|
)
|
||||||
|
self.assertEqual(404, response.status_code)
|
||||||
|
|
||||||
|
def test_server_shares_delete_fails_instance_not_stopped(self):
|
||||||
|
"""Verify we cannot remove a share if the instance is not stopped.
|
||||||
|
"""
|
||||||
|
uuid = self._post_server()
|
||||||
|
subs = self._get_create_subs()
|
||||||
|
response = self._do_post(
|
||||||
|
"servers/%s/shares" % uuid, "server-shares-delete-req", subs
|
||||||
|
)
|
||||||
|
response = self._do_delete(
|
||||||
|
"servers/%s/shares/%s" % (uuid, subs["share_id"])
|
||||||
|
)
|
||||||
|
self.assertEqual(409, response.status_code)
|
||||||
|
|
||||||
|
def test_server_shares_delete_unknown_instance(self):
|
||||||
|
"""Verify deleting a share on an unknown instance reports an error.
|
||||||
|
"""
|
||||||
|
uuid = self._post_server_shares()
|
||||||
|
subs = self._get_create_subs()
|
||||||
|
response = self._do_post(
|
||||||
|
"servers/%s/shares" % uuid, "server-shares-delete-req", subs
|
||||||
|
)
|
||||||
|
response = self._do_delete(
|
||||||
|
"servers/%s/shares/%s" % (uuidsentinel.fake_uuid, subs["share_id"])
|
||||||
|
)
|
||||||
|
self.assertEqual(404, response.status_code)
|
||||||
|
self.assertIn(
|
||||||
|
"could not be found",
|
||||||
|
response.text
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_server_shares_delete_fails_cannot_deny_policy(self):
|
||||||
|
"""Verify we raise an exception if we cannot deny the policy.
|
||||||
|
"""
|
||||||
|
uuid = self._post_server_shares()
|
||||||
|
subs = self._get_create_subs()
|
||||||
|
self.manila_fixture.mock_deny.return_value = None
|
||||||
|
self.manila_fixture.mock_deny.side_effect = (
|
||||||
|
exception.ShareAccessRemovalError(
|
||||||
|
share_id=subs["share_id"],
|
||||||
|
reason="Resource could not be found.",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Here we are using CastAsCallFixture so we got an exception from
|
||||||
|
# nova api. This should not happen without the fixture.
|
||||||
|
response = self._do_delete(
|
||||||
|
"servers/%s/shares/%s" % (uuid, subs["share_id"])
|
||||||
|
)
|
||||||
|
self.assertEqual(500, response.status_code)
|
||||||
|
self.assertIn('nova.exception.ShareAccessRemovalError', response.text)
|
||||||
|
|
||||||
|
|
||||||
|
class ServerSharesJsonAdminTest(ServerSharesBase):
|
||||||
|
ADMIN_API = True
|
||||||
|
|
||||||
|
def _post_server_shares(self):
|
||||||
|
"""Verify the response status and returns the UUID of the
|
||||||
|
newly created server with shares.
|
||||||
|
"""
|
||||||
|
uuid = self.create_server_ok()
|
||||||
|
subs = self._get_create_subs()
|
||||||
|
response = self._do_post(
|
||||||
|
"servers/%s/shares" % uuid, "server-shares-create-req", subs
|
||||||
|
)
|
||||||
|
self._verify_response(
|
||||||
|
'server-shares-admin-create-resp', subs, response, 201)
|
||||||
|
|
||||||
|
return uuid
|
||||||
|
|
||||||
|
def test_server_shares_create(self):
|
||||||
|
"""Verify we can create a share mapping.
|
||||||
|
"""
|
||||||
|
self._post_server_shares()
|
||||||
|
|
||||||
|
def test_server_shares_show(self):
|
||||||
|
"""Verify we can show a share as admin and thus have more
|
||||||
|
information.
|
||||||
|
"""
|
||||||
|
uuid = self._post_server_shares()
|
||||||
|
subs = self._get_create_subs()
|
||||||
|
response = self._do_get(
|
||||||
|
"servers/%s/shares/%s" % (uuid, subs["share_id"])
|
||||||
|
)
|
||||||
|
self._verify_response(
|
||||||
|
"server-shares-admin-show-resp", subs, response, 200
|
||||||
|
)
|
||||||
|
|
||||||
|
def _block_action(self, body):
|
||||||
|
uuid = self._post_server_shares()
|
||||||
|
|
||||||
|
ex = self.assertRaises(
|
||||||
|
client.OpenStackApiException,
|
||||||
|
self.api.post_server_action,
|
||||||
|
uuid,
|
||||||
|
body
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(409, ex.response.status_code)
|
||||||
|
self.assertIn(
|
||||||
|
"Feature not supported with instances that have shares.",
|
||||||
|
ex.response.text
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_shelve_server_with_share_fails(self):
|
||||||
|
self._block_action({"shelve": None})
|
||||||
|
|
||||||
|
def test_suspend_server_with_share_fails(self):
|
||||||
|
self._block_action({"suspend": None})
|
||||||
|
|
||||||
|
def test_evacuate_server_with_share_fails(self):
|
||||||
|
self._block_action({"evacuate": {}})
|
||||||
|
|
||||||
|
def test_resize_server_with_share_fails(self):
|
||||||
|
self._block_action({"resize": {"flavorRef": "2"}})
|
||||||
|
|
||||||
|
def test_migrate_server_with_share_fails(self):
|
||||||
|
self._block_action({"migrate": None})
|
||||||
|
|
||||||
|
def test_live_migrate_server_with_share_fails(self):
|
||||||
|
self._block_action(
|
||||||
|
{"os-migrateLive": {
|
||||||
|
"host": None,
|
||||||
|
"block_migration": "auto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
@@ -0,0 +1,411 @@
|
|||||||
|
# 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 webob
|
||||||
|
|
||||||
|
from nova.api.openstack.compute import server_shares
|
||||||
|
from nova.compute import vm_states
|
||||||
|
from nova import context
|
||||||
|
from nova.db.main import models
|
||||||
|
from nova import objects
|
||||||
|
from nova.tests.unit.api.openstack import fakes
|
||||||
|
from nova.tests.unit.compute.test_compute import BaseTestCase
|
||||||
|
from nova.tests.unit import fake_instance
|
||||||
|
|
||||||
|
from nova.tests import fixtures as nova_fixtures
|
||||||
|
from oslo_utils import timeutils
|
||||||
|
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
UUID = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'
|
||||||
|
NON_EXISTING_UUID = '123'
|
||||||
|
|
||||||
|
|
||||||
|
def return_server(compute_api, context, instance_id, expected_attrs=None):
|
||||||
|
return fake_instance.fake_instance_obj(context, vm_state=vm_states.ACTIVE)
|
||||||
|
|
||||||
|
|
||||||
|
def return_invalid_server(compute_api, context, instance_id,
|
||||||
|
expected_attrs=None):
|
||||||
|
return fake_instance.fake_instance_obj(context,
|
||||||
|
vm_state=vm_states.BUILDING)
|
||||||
|
|
||||||
|
|
||||||
|
class ServerSharesTest(BaseTestCase):
|
||||||
|
wsgi_api_version = '2.97'
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(ServerSharesTest, self).setUp()
|
||||||
|
self.controller = server_shares.ServerSharesController()
|
||||||
|
inst_map = objects.InstanceMapping(
|
||||||
|
project_id=fakes.FAKE_PROJECT_ID,
|
||||||
|
user_id=fakes.FAKE_USER_ID,
|
||||||
|
cell_mapping=objects.CellMappingList.get_all(
|
||||||
|
context.get_admin_context())[1])
|
||||||
|
self.stub_out('nova.objects.InstanceMapping.get_by_instance_uuid',
|
||||||
|
lambda s, c, u: inst_map)
|
||||||
|
self.req = fakes.HTTPRequest.blank(
|
||||||
|
'/servers/%s/shares' % (UUID),
|
||||||
|
use_admin_context=False, version=self.wsgi_api_version)
|
||||||
|
self.manila_fixture = self.useFixture(nova_fixtures.ManilaFixture())
|
||||||
|
|
||||||
|
def fake_get_instance(self):
|
||||||
|
ctxt = self.req.environ['nova.context']
|
||||||
|
return fake_instance.fake_instance_obj(
|
||||||
|
ctxt,
|
||||||
|
uuid=fakes.FAKE_UUID,
|
||||||
|
flavor = objects.Flavor(id=1, name='flavor1',
|
||||||
|
memory_mb=256, vcpus=1,
|
||||||
|
root_gb=1, ephemeral_gb=1,
|
||||||
|
flavorid='1',
|
||||||
|
swap=0, rxtx_factor=1.0,
|
||||||
|
vcpu_weight=1,
|
||||||
|
disabled=False,
|
||||||
|
is_public=True,
|
||||||
|
extra_specs={
|
||||||
|
'virtiofs': 'required',
|
||||||
|
'mem_backing_file': 'required'
|
||||||
|
},
|
||||||
|
projects=[]),
|
||||||
|
vm_state=vm_states.STOPPED)
|
||||||
|
|
||||||
|
@mock.patch(
|
||||||
|
'nova.virt.hardware.check_shares_supported', return_value=None
|
||||||
|
)
|
||||||
|
@mock.patch('nova.db.main.api.share_mapping_get_by_instance_uuid')
|
||||||
|
@mock.patch('nova.api.openstack.common.get_instance')
|
||||||
|
def test_index(
|
||||||
|
self, mock_get_instance, mock_db_get_shares, mock_shares_support
|
||||||
|
):
|
||||||
|
timeutils.set_time_override()
|
||||||
|
NOW = timeutils.utcnow()
|
||||||
|
instance = self.fake_get_instance()
|
||||||
|
mock_get_instance.return_value = instance
|
||||||
|
|
||||||
|
fake_db_shares = [
|
||||||
|
{
|
||||||
|
'created_at': NOW,
|
||||||
|
'updated_at': None,
|
||||||
|
'deleted_at': None,
|
||||||
|
'deleted': False,
|
||||||
|
"id": 1,
|
||||||
|
"uuid": "33a8e0cb-5f82-409a-b310-89c41f8bf023",
|
||||||
|
"instance_uuid": "48c16a1a-183f-4052-9dac-0e4fc1e498ae",
|
||||||
|
"share_id": "48c16a1a-183f-4052-9dac-0e4fc1e498ad",
|
||||||
|
"status": "active",
|
||||||
|
"tag": "foo",
|
||||||
|
"export_location": "10.0.0.50:/mnt/foo",
|
||||||
|
"share_proto": "NFS",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'created_at': NOW,
|
||||||
|
'updated_at': None,
|
||||||
|
'deleted_at': None,
|
||||||
|
'deleted': False,
|
||||||
|
"id": 2,
|
||||||
|
"uuid": "33a8e0cb-5f82-409a-b310-89c41f8bf024",
|
||||||
|
"instance_uuid": "48c16a1a-183f-4052-9dac-0e4fc1e498ae",
|
||||||
|
"share_id": "e8debdc0-447a-4376-a10a-4cd9122d7986",
|
||||||
|
"status": "active",
|
||||||
|
"tag": "bar",
|
||||||
|
"export_location": "10.0.0.50:/mnt/bar",
|
||||||
|
"share_proto": "NFS",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
fake_shares = {
|
||||||
|
"shares": [
|
||||||
|
{
|
||||||
|
"share_id": "48c16a1a-183f-4052-9dac-0e4fc1e498ad",
|
||||||
|
"status": "active",
|
||||||
|
"tag": "foo",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"share_id": "e8debdc0-447a-4376-a10a-4cd9122d7986",
|
||||||
|
"status": "active",
|
||||||
|
"tag": "bar",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
mock_db_get_shares.return_value = fake_db_shares
|
||||||
|
output = self.controller.index(self.req, instance.uuid)
|
||||||
|
mock_db_get_shares.assert_called_once_with(mock.ANY, instance.uuid)
|
||||||
|
self.assertEqual(output, fake_shares)
|
||||||
|
|
||||||
|
@mock.patch('nova.compute.api.API.allow_share')
|
||||||
|
@mock.patch(
|
||||||
|
'nova.virt.hardware.check_shares_supported', return_value=None
|
||||||
|
)
|
||||||
|
@mock.patch(
|
||||||
|
'nova.db.main.api.share_mapping_get_by_instance_uuid_and_share_id'
|
||||||
|
)
|
||||||
|
@mock.patch('nova.db.main.api.share_mapping_update')
|
||||||
|
@mock.patch('nova.api.openstack.common.get_instance')
|
||||||
|
def test_create(
|
||||||
|
self,
|
||||||
|
mock_get_instance,
|
||||||
|
mock_db_update_share,
|
||||||
|
mock_db_get_share,
|
||||||
|
mock_shares_support,
|
||||||
|
mock_allow
|
||||||
|
):
|
||||||
|
instance = self.fake_get_instance()
|
||||||
|
|
||||||
|
mock_get_instance.return_value = instance
|
||||||
|
|
||||||
|
fake_db_share = {
|
||||||
|
'created_at': None,
|
||||||
|
'updated_at': None,
|
||||||
|
'deleted_at': None,
|
||||||
|
'deleted': False,
|
||||||
|
"id": 1,
|
||||||
|
"uuid": "7ddcf3ae-82d4-4f93-996a-2b6cbcb42c2b",
|
||||||
|
"instance_uuid": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
|
||||||
|
"share_id": "e8debdc0-447a-4376-a10a-4cd9122d7986",
|
||||||
|
"status": "attaching",
|
||||||
|
"tag": "e8debdc0-447a-4376-a10a-4cd9122d7986",
|
||||||
|
"export_location": "10.0.0.50:/mnt/foo",
|
||||||
|
"share_proto": "NFS",
|
||||||
|
}
|
||||||
|
|
||||||
|
body = {
|
||||||
|
'share': {
|
||||||
|
'share_id': 'e8debdc0-447a-4376-a10a-4cd9122d7986'
|
||||||
|
}}
|
||||||
|
|
||||||
|
mock_db_update_share.return_value = fake_db_share
|
||||||
|
mock_db_get_share.side_effect = [None, fake_db_share]
|
||||||
|
self.controller.create(self.req, instance.uuid, body=body)
|
||||||
|
|
||||||
|
mock_allow.assert_called_once()
|
||||||
|
self.assertIsInstance(
|
||||||
|
mock_allow.call_args.args[1], objects.instance.Instance)
|
||||||
|
self.assertEqual(mock_allow.call_args.args[1].uuid, instance.uuid)
|
||||||
|
self.assertIsInstance(
|
||||||
|
mock_allow.call_args.args[2], objects.share_mapping.ShareMapping)
|
||||||
|
self.assertEqual(
|
||||||
|
mock_allow.call_args.args[2].share_id, fake_db_share['share_id'])
|
||||||
|
|
||||||
|
mock_db_update_share.assert_called_once_with(
|
||||||
|
mock.ANY,
|
||||||
|
mock.ANY,
|
||||||
|
instance.uuid,
|
||||||
|
fake_db_share['share_id'],
|
||||||
|
'attaching',
|
||||||
|
fake_db_share['tag'],
|
||||||
|
fake_db_share['export_location'],
|
||||||
|
fake_db_share['share_proto'],
|
||||||
|
)
|
||||||
|
|
||||||
|
@mock.patch('nova.compute.api.API.allow_share')
|
||||||
|
@mock.patch(
|
||||||
|
'nova.virt.hardware.check_shares_supported', return_value=None
|
||||||
|
)
|
||||||
|
@mock.patch(
|
||||||
|
'nova.db.main.api.share_mapping_get_by_instance_uuid_and_share_id'
|
||||||
|
)
|
||||||
|
@mock.patch('nova.db.main.api.share_mapping_update')
|
||||||
|
@mock.patch('nova.api.openstack.common.get_instance')
|
||||||
|
def test_create_share_with_new_tag(
|
||||||
|
self,
|
||||||
|
mock_get_instance,
|
||||||
|
mock_db_update_share,
|
||||||
|
mock_db_get_share,
|
||||||
|
mock_shares_support,
|
||||||
|
mock_allow
|
||||||
|
):
|
||||||
|
instance = self.fake_get_instance()
|
||||||
|
|
||||||
|
mock_get_instance.return_value = instance
|
||||||
|
|
||||||
|
fake_db_share = {
|
||||||
|
'created_at': None,
|
||||||
|
'updated_at': None,
|
||||||
|
'deleted_at': None,
|
||||||
|
'deleted': False,
|
||||||
|
"id": 1,
|
||||||
|
"uuid": "7ddcf3ae-82d4-4f93-996a-2b6cbcb42c2b",
|
||||||
|
"instance_uuid": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
|
||||||
|
"share_id": "e8debdc0-447a-4376-a10a-4cd9122d7986",
|
||||||
|
"status": "attaching",
|
||||||
|
"tag": "e8debdc0-447a-4376-a10a-4cd9122d7986",
|
||||||
|
"export_location": "10.0.0.50:/mnt/foo",
|
||||||
|
"share_proto": "NFS",
|
||||||
|
}
|
||||||
|
|
||||||
|
body = {
|
||||||
|
'share': {
|
||||||
|
'share_id': 'e8debdc0-447a-4376-a10a-4cd9122d7986'
|
||||||
|
}}
|
||||||
|
|
||||||
|
mock_db_update_share.return_value = fake_db_share
|
||||||
|
mock_db_get_share.side_effect = [None, fake_db_share]
|
||||||
|
self.controller.create(self.req, instance.uuid, body=body)
|
||||||
|
|
||||||
|
mock_allow.assert_called_once()
|
||||||
|
self.assertIsInstance(
|
||||||
|
mock_allow.call_args.args[1], objects.instance.Instance)
|
||||||
|
self.assertEqual(mock_allow.call_args.args[1].uuid, instance.uuid)
|
||||||
|
self.assertIsInstance(
|
||||||
|
mock_allow.call_args.args[2], objects.share_mapping.ShareMapping)
|
||||||
|
self.assertEqual(
|
||||||
|
mock_allow.call_args.args[2].share_id, fake_db_share['share_id'])
|
||||||
|
|
||||||
|
mock_db_update_share.assert_called_once_with(
|
||||||
|
mock.ANY,
|
||||||
|
mock.ANY,
|
||||||
|
instance.uuid,
|
||||||
|
fake_db_share['share_id'],
|
||||||
|
'attaching',
|
||||||
|
fake_db_share['tag'],
|
||||||
|
fake_db_share['export_location'],
|
||||||
|
fake_db_share['share_proto'],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Change the tag of the share
|
||||||
|
body['share']['tag'] = 'my-tag'
|
||||||
|
mock_db_update_share.return_value['tag'] = "my-tag"
|
||||||
|
mock_db_get_share.side_effect = [
|
||||||
|
fake_db_share,
|
||||||
|
mock_db_update_share.return_value,
|
||||||
|
]
|
||||||
|
|
||||||
|
exc = self.assertRaises(
|
||||||
|
webob.exc.HTTPConflict,
|
||||||
|
self.controller.create,
|
||||||
|
self.req,
|
||||||
|
instance.uuid,
|
||||||
|
body=body,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertIn(
|
||||||
|
"Share 'e8debdc0-447a-4376-a10a-4cd9122d7986' or tag 'my-tag' "
|
||||||
|
"already associated to this server",
|
||||||
|
str(exc))
|
||||||
|
|
||||||
|
@mock.patch(
|
||||||
|
'nova.virt.hardware.check_shares_supported', return_value=None
|
||||||
|
)
|
||||||
|
@mock.patch('nova.api.openstack.common.get_instance')
|
||||||
|
def test_create_passing_a_share_with_an_error(
|
||||||
|
self,
|
||||||
|
mock_get_instance,
|
||||||
|
mock_shares_support,
|
||||||
|
):
|
||||||
|
instance = self.fake_get_instance()
|
||||||
|
|
||||||
|
mock_get_instance.return_value = instance
|
||||||
|
|
||||||
|
body = {
|
||||||
|
'share': {
|
||||||
|
'share_id': 'e8debdc0-447a-4376-a10a-4cd9122d7986'
|
||||||
|
}}
|
||||||
|
|
||||||
|
self.manila_fixture.mock_get.side_effect = (
|
||||||
|
self.manila_fixture.fake_get_share_status_error
|
||||||
|
)
|
||||||
|
|
||||||
|
exc = self.assertRaises(
|
||||||
|
webob.exc.HTTPConflict,
|
||||||
|
self.controller.create,
|
||||||
|
self.req,
|
||||||
|
instance.uuid,
|
||||||
|
body=body,
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
str(exc),
|
||||||
|
"Share e8debdc0-447a-4376-a10a-4cd9122d7986 is in 'error' "
|
||||||
|
"instead of 'available' status.",
|
||||||
|
)
|
||||||
|
|
||||||
|
@mock.patch(
|
||||||
|
'nova.virt.hardware.check_shares_supported', return_value=None
|
||||||
|
)
|
||||||
|
@mock.patch('nova.api.openstack.common.get_instance')
|
||||||
|
def test_create_passing_unknown_protocol(
|
||||||
|
self,
|
||||||
|
mock_get_instance,
|
||||||
|
mock_shares_support,
|
||||||
|
):
|
||||||
|
instance = self.fake_get_instance()
|
||||||
|
|
||||||
|
mock_get_instance.return_value = instance
|
||||||
|
|
||||||
|
body = {
|
||||||
|
'share': {
|
||||||
|
'share_id': 'e8debdc0-447a-4376-a10a-4cd9122d7986'
|
||||||
|
}}
|
||||||
|
|
||||||
|
self.manila_fixture.mock_get.side_effect = (
|
||||||
|
self.manila_fixture.fake_get_share_unknown_protocol
|
||||||
|
)
|
||||||
|
|
||||||
|
exc = self.assertRaises(
|
||||||
|
webob.exc.HTTPConflict,
|
||||||
|
self.controller.create,
|
||||||
|
self.req,
|
||||||
|
instance.uuid,
|
||||||
|
body=body,
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
str(exc),
|
||||||
|
"Share protocol CIFS is not supported."
|
||||||
|
)
|
||||||
|
|
||||||
|
@mock.patch('nova.compute.api.API.deny_share')
|
||||||
|
@mock.patch(
|
||||||
|
'nova.virt.hardware.check_shares_supported', return_value=None
|
||||||
|
)
|
||||||
|
@mock.patch('nova.db.main.api.'
|
||||||
|
'share_mapping_delete_by_instance_uuid_and_share_id')
|
||||||
|
@mock.patch('nova.db.main.api.'
|
||||||
|
'share_mapping_get_by_instance_uuid_and_share_id')
|
||||||
|
@mock.patch('nova.api.openstack.common.get_instance')
|
||||||
|
def test_delete(
|
||||||
|
self,
|
||||||
|
mock_get_instance,
|
||||||
|
mock_db_get_shares,
|
||||||
|
mock_db_delete_share,
|
||||||
|
mock_shares_support,
|
||||||
|
mock_deny
|
||||||
|
):
|
||||||
|
instance = self.fake_get_instance()
|
||||||
|
|
||||||
|
mock_get_instance.return_value = instance
|
||||||
|
|
||||||
|
fake_db_share = models.ShareMapping()
|
||||||
|
fake_db_share.created_at = None
|
||||||
|
fake_db_share.updated_at = None
|
||||||
|
fake_db_share.deleted_at = None
|
||||||
|
fake_db_share.deleted = False
|
||||||
|
fake_db_share.id = 1
|
||||||
|
fake_db_share.uuid = "33a8e0cb-5f82-409a-b310-89c41f8bf023"
|
||||||
|
fake_db_share.instance_uuid = "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
|
||||||
|
fake_db_share.share_id = "e8debdc0-447a-4376-a10a-4cd9122d7986"
|
||||||
|
fake_db_share.status = "inactive"
|
||||||
|
fake_db_share.tag = "e8debdc0-447a-4376-a10a-4cd9122d7986"
|
||||||
|
fake_db_share.export_location = "10.0.0.50:/mnt/foo"
|
||||||
|
fake_db_share.share_proto = "NFS"
|
||||||
|
|
||||||
|
mock_db_get_shares.return_value = fake_db_share
|
||||||
|
self.controller.delete(
|
||||||
|
self.req, instance.uuid, fake_db_share.share_id)
|
||||||
|
|
||||||
|
mock_deny.assert_called_once()
|
||||||
|
self.assertIsInstance(
|
||||||
|
mock_deny.call_args.args[1], objects.instance.Instance)
|
||||||
|
self.assertEqual(mock_deny.call_args.args[1].uuid, instance.uuid)
|
||||||
|
self.assertIsInstance(
|
||||||
|
mock_deny.call_args.args[2], objects.share_mapping.ShareMapping)
|
||||||
|
self.assertEqual(
|
||||||
|
mock_deny.call_args.args[2].share_id, fake_db_share['share_id'])
|
||||||
@@ -159,6 +159,10 @@ policy_data = """
|
|||||||
"os_compute_api:os-server-password:show": "",
|
"os_compute_api:os-server-password:show": "",
|
||||||
"os_compute_api:os-server-password:clear": "",
|
"os_compute_api:os-server-password:clear": "",
|
||||||
"os_compute_api:os-server-external-events:create": "",
|
"os_compute_api:os-server-external-events:create": "",
|
||||||
|
"os_compute_api:os-server-shares:index": "",
|
||||||
|
"os_compute_api:os-server-shares:create": "",
|
||||||
|
"os_compute_api:os-server-shares:show": "",
|
||||||
|
"os_compute_api:os-server-shares:delete": "",
|
||||||
"os_compute_api:os-server-tags:index": "",
|
"os_compute_api:os-server-tags:index": "",
|
||||||
"os_compute_api:os-server-tags:show": "",
|
"os_compute_api:os-server-tags:show": "",
|
||||||
"os_compute_api:os-server-tags:update": "",
|
"os_compute_api:os-server-tags:update": "",
|
||||||
|
|||||||
@@ -488,6 +488,10 @@ class RealRolePolicyTestCase(test.NoDBTestCase):
|
|||||||
"os_compute_api:os-instance-actions:list",
|
"os_compute_api:os-instance-actions:list",
|
||||||
"os_compute_api:os-instance-actions:show",
|
"os_compute_api:os-instance-actions:show",
|
||||||
"os_compute_api:os-server-password:show",
|
"os_compute_api:os-server-password:show",
|
||||||
|
"os_compute_api:os-server-shares:index",
|
||||||
|
"os_compute_api:os-server-shares:create",
|
||||||
|
"os_compute_api:os-server-shares:show",
|
||||||
|
"os_compute_api:os-server-shares:delete",
|
||||||
"os_compute_api:os-server-tags:index",
|
"os_compute_api:os-server-tags:index",
|
||||||
"os_compute_api:os-server-tags:show",
|
"os_compute_api:os-server-tags:show",
|
||||||
"os_compute_api:os-floating-ips:list",
|
"os_compute_api:os-floating-ips:list",
|
||||||
|
|||||||
Reference in New Issue
Block a user