Merge "Attach Manila shares via virtiofs (API)"
This commit is contained in:
@@ -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'],
|
||||
|
||||
@@ -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:
|
||||
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))
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user