Attach Manila shares via virtiofs (API)
This patch introduce the REST API modifications to attach/detach a share and list/show share mappings. Manila is the OpenStack Shared Filesystems service. These series of patches implement changes required in Nova to allow the shares provided by Manila to be associated with and attached to instances using virtiofs. Implements: blueprint libvirt-virtiofs-attach-manila-shares Change-Id: I0255a5697cd4ea148bd91c4f6fd183841d69a333
This commit is contained in:
parent
d6f5a30caa
commit
5e508a09b3
@ -31,6 +31,7 @@ the `API guide <https://docs.openstack.org/api-guide/compute/index.html>`_.
|
||||
.. include:: os-instance-actions.inc
|
||||
.. include:: os-interface.inc
|
||||
.. include:: os-server-password.inc
|
||||
.. include:: os-server-shares.inc
|
||||
.. include:: os-volume-attachments.inc
|
||||
.. include:: flavors.inc
|
||||
.. include:: os-flavor-access.inc
|
||||
|
163
api-ref/source/os-server-shares.inc
Normal file
163
api-ref/source/os-server-shares.inc
Normal file
@ -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
|
||||
required: true
|
||||
type: string
|
||||
share_id_path:
|
||||
description: |
|
||||
The UUID of the attached share.
|
||||
in: path
|
||||
required: true
|
||||
type: string
|
||||
snapshot_id_path:
|
||||
description: |
|
||||
The UUID of the snapshot.
|
||||
@ -3742,13 +3748,13 @@ hosts.availability_zone_none:
|
||||
type: none
|
||||
hours:
|
||||
description: |
|
||||
The duration that the server exists (in hours).
|
||||
The duration that the server exists (in hours).
|
||||
in: body
|
||||
required: true
|
||||
type: float
|
||||
hours_optional:
|
||||
description: |
|
||||
The duration that the server exists (in hours).
|
||||
The duration that the server exists (in hours).
|
||||
in: body
|
||||
required: false
|
||||
type: float
|
||||
@ -6809,6 +6815,56 @@ set_metadata:
|
||||
in: body
|
||||
required: true
|
||||
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:
|
||||
description: |
|
||||
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",
|
||||
"version": "2.96",
|
||||
"version": "2.97",
|
||||
"min_version": "2.1",
|
||||
"updated": "2013-07-23T11:33:21Z"
|
||||
}
|
||||
|
@ -22,7 +22,7 @@
|
||||
}
|
||||
],
|
||||
"status": "CURRENT",
|
||||
"version": "2.96",
|
||||
"version": "2.97",
|
||||
"min_version": "2.1",
|
||||
"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.96 - Add support for returning pinned_availability_zone in
|
||||
``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
|
||||
@ -265,7 +273,7 @@ REST_API_VERSION_HISTORY = """REST API Version History:
|
||||
# Note(cyeoh): This only applies for the v2.1 API once microversions
|
||||
# support is fully merged. It does not affect the V2 API.
|
||||
_MIN_API_VERSION = '2.1'
|
||||
_MAX_API_VERSION = '2.96'
|
||||
_MAX_API_VERSION = '2.97'
|
||||
DEFAULT_API_VERSION = _MIN_API_VERSION
|
||||
|
||||
# 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())
|
||||
except exception.UnsupportedRPCVersion as e:
|
||||
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
|
||||
CONF.api.enable_instance_password):
|
||||
|
@ -81,6 +81,11 @@ class MigrateServerController(wsgi.Controller):
|
||||
exception.ExtendedResourceRequestOldCompute,
|
||||
) as e:
|
||||
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.expected_errors((400, 403, 404, 409))
|
||||
@ -156,6 +161,11 @@ class MigrateServerController(wsgi.Controller):
|
||||
except exception.InstanceInvalidState as state_error:
|
||||
common.raise_http_conflict_for_instance_invalid_state(state_error,
|
||||
'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):
|
||||
force = body["os-migrateLive"].get("force", False)
|
||||
|
@ -1255,3 +1255,16 @@ behavior.
|
||||
|
||||
The ``server show`` and ``server list --long`` responses now include the
|
||||
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_migrations
|
||||
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_topology
|
||||
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,
|
||||
security_groups.ServerSecurityGroupController, [])
|
||||
|
||||
server_shares_controller = functools.partial(_create_controller,
|
||||
server_shares.ServerSharesController, [])
|
||||
|
||||
server_tags_controller = functools.partial(_create_controller,
|
||||
server_tags.ServerTagsController, [])
|
||||
@ -825,6 +828,14 @@ ROUTE_LIST = (
|
||||
('/servers/{server_id}/os-security-groups', {
|
||||
'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', {
|
||||
'GET': [server_tags_controller, 'index'],
|
||||
'PUT': [server_tags_controller, 'update_all'],
|
||||
|
95
nova/api/openstack/compute/schemas/server_shares.py
Normal file
95
nova/api/openstack/compute/schemas/server_shares.py
Normal file
@ -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
|
||||
}
|
262
nova/api/openstack/compute/server_shares.py
Normal file
262
nova/api/openstack/compute/server_shares.py
Normal file
@ -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:
|
||||
msg = _("Invalid instance image.")
|
||||
raise exc.HTTPBadRequest(explanation=msg)
|
||||
except (
|
||||
exception.ForbiddenSharesNotSupported,
|
||||
exception.ForbiddenWithShare) as e:
|
||||
raise exc.HTTPConflict(explanation=e.format_message())
|
||||
|
||||
@wsgi.response(204)
|
||||
@wsgi.expected_errors((404, 409))
|
||||
|
@ -59,6 +59,10 @@ class ShelveController(wsgi.Controller):
|
||||
'shelve', id)
|
||||
except exception.ForbiddenPortsWithAccelerator as e:
|
||||
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.expected_errors((400, 404, 409))
|
||||
|
@ -49,6 +49,10 @@ class SuspendServerController(wsgi.Controller):
|
||||
'suspend', id)
|
||||
except exception.ForbiddenPortsWithAccelerator as e:
|
||||
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.expected_errors((404, 409))
|
||||
|
46
nova/api/openstack/compute/views/server_shares.py
Normal file
46
nova/api/openstack/compute/views/server_shares.py
Normal file
@ -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'
|
||||
}
|
||||
|
||||
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 = {
|
||||
'oneOf': [
|
||||
|
@ -721,14 +721,24 @@ class ShareNotFound(NotFound):
|
||||
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):
|
||||
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):
|
||||
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):
|
||||
msg_fmt = _("Share %(share_id)s used by instance %(instance_uuid)s "
|
||||
"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_metadata
|
||||
from nova.policies import server_password
|
||||
from nova.policies import server_shares
|
||||
from nova.policies import server_tags
|
||||
from nova.policies import server_topology
|
||||
from nova.policies import servers
|
||||
@ -114,6 +115,7 @@ def list_rules():
|
||||
server_groups.list_rules(),
|
||||
server_metadata.list_rules(),
|
||||
server_password.list_rules(),
|
||||
server_shares.list_rules(),
|
||||
server_tags.list_rules(),
|
||||
server_topology.list_rules(),
|
||||
servers.list_rules(),
|
||||
|
70
nova/policies/server_shares.py
Normal file
70
nova/policies/server_shares.py
Normal file
@ -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):
|
||||
# Return the preferred path otherwise choose the first one
|
||||
paths = []
|
||||
for export_location in export_locations:
|
||||
if export_location.is_preferred:
|
||||
return export_location.path
|
||||
else:
|
||||
paths.append(export_location.path)
|
||||
return paths[0]
|
||||
try:
|
||||
for export_location in export_locations:
|
||||
if export_location.is_preferred:
|
||||
return export_location.path
|
||||
else:
|
||||
paths.append(export_location.path)
|
||||
return paths[0]
|
||||
except (IndexError, NameError):
|
||||
return None
|
||||
|
||||
client = _manilaclient(context, admin=False)
|
||||
LOG.debug("Get share id:'%s' data from manila", share_id)
|
||||
|
23
nova/tests/fixtures/manila.py
vendored
23
nova/tests/fixtures/manila.py
vendored
@ -80,6 +80,29 @@ class ManilaFixture(fixtures.Fixture):
|
||||
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):
|
||||
manila_share = ManilaShare(share_id, "CEPHFS")
|
||||
export_location = "10.0.0.50:/mnt/foo"
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
{
|
||||
"share": {
|
||||
"share_id": "%(share_id)s"
|
||||
}
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
{
|
||||
"share":
|
||||
{
|
||||
"share_id": "%(share_id)s",
|
||||
"status": "attaching",
|
||||
"tag": "%(share_id)s"
|
||||
}
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
{
|
||||
"share": {
|
||||
"share_id": "%(share_id)s",
|
||||
"tag": "%(tag)s"
|
||||
}
|
||||
}
|
@ -0,0 +1 @@
|
||||
server-shares-create-req.json.tpl
|
@ -0,0 +1,9 @@
|
||||
{
|
||||
"shares": [
|
||||
{
|
||||
"share_id": "e8debdc0-447a-4376-a10a-4cd9122d7986",
|
||||
"status": "inactive",
|
||||
"tag": "e8debdc0-447a-4376-a10a-4cd9122d7986"
|
||||
}
|
||||
]
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
{
|
||||
"share":
|
||||
{
|
||||
"share_id": "%(share_id)s",
|
||||
"status": "inactive",
|
||||
"tag": "%(share_id)s"
|
||||
}
|
||||
}
|
482
nova/tests/functional/api_sample_tests/test_server_shares.py
Normal file
482
nova/tests/functional/api_sample_tests/test_server_shares.py
Normal file
@ -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"
|
||||
}
|
||||
}
|
||||
)
|
411
nova/tests/unit/api/openstack/compute/test_server_shares.py
Normal file
411
nova/tests/unit/api/openstack/compute/test_server_shares.py
Normal file
@ -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:clear": "",
|
||||
"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:show": "",
|
||||
"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: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:show",
|
||||
"os_compute_api:os-floating-ips:list",
|
||||
|
Loading…
Reference in New Issue
Block a user