diff --git a/manila/api/common.py b/manila/api/common.py index 6e1a756965..11962e6bf9 100644 --- a/manila/api/common.py +++ b/manila/api/common.py @@ -254,6 +254,18 @@ def check_net_id_and_subnet_id(body): raise webob.exc.HTTPBadRequest(explanation=msg) +def check_share_network_is_active(share_network): + network_status = share_network.get('status') + if network_status != constants.STATUS_NETWORK_ACTIVE: + msg = _("The share network %(id)s used isn't in an 'active' state. " + "Current status is %(status)s. The action may be retried " + "after the share network has changed its state.") % { + 'id': share_network['id'], + 'status': share_network.get('status'), + } + raise webob.exc.HTTPBadRequest(explanation=msg) + + class ViewBuilder(object): """Model API responses as dictionaries.""" diff --git a/manila/api/openstack/api_version_request.py b/manila/api/openstack/api_version_request.py index 4328313ffa..86e085665b 100644 --- a/manila/api/openstack/api_version_request.py +++ b/manila/api/openstack/api_version_request.py @@ -165,13 +165,19 @@ REST_API_VERSION_HISTORY = """ which can add minimum and maximum share size restrictions on a per share-type granularity. * 2.62 - Added quota control to per share size. + * 2.63 - Changed the existing behavior of 'add_security_service' action on + the share network's endpoint to allow the addition of security + services, even when the share network is in use. Also, added new + actions on the share network's endpoint: + 'update_security_service', 'update_security_service_check' and + 'add_security_service_check'. """ # The minimum and maximum versions of the API supported # The default api version request is defined to be the # minimum version of the API supported. _MIN_API_VERSION = "2.0" -_MAX_API_VERSION = "2.62" +_MAX_API_VERSION = "2.63" DEFAULT_API_VERSION = _MIN_API_VERSION diff --git a/manila/api/openstack/rest_api_version_history.rst b/manila/api/openstack/rest_api_version_history.rst index 83f590840e..41bb22faa9 100644 --- a/manila/api/openstack/rest_api_version_history.rst +++ b/manila/api/openstack/rest_api_version_history.rst @@ -341,3 +341,15 @@ user documentation. 2.62 ---- Added quota control to per share size. + +2.63 +---- + Added the possibility to attach security services to share networks in use. + Also, an attached security service can be replaced for another one of + the same 'type'. In order to support those operations a 'status' field was + added in the share networks as well as, a new property called + 'security_service_update_support' was included in the share networks and + share servers. Also new action APIs have been added to the share-networks + endpoint: 'update_security_service', 'update_security_service_check' and + 'add_security_service_check'. + diff --git a/manila/api/v1/shares.py b/manila/api/v1/shares.py index d596a5fd99..28714f1a59 100644 --- a/manila/api/v1/shares.py +++ b/manila/api/v1/shares.py @@ -330,12 +330,14 @@ class ShareMixin(object): if share_network_id: try: - self.share_api.get_share_network( + share_network = self.share_api.get_share_network( context, share_network_id) except exception.ShareNetworkNotFound as e: raise exc.HTTPNotFound(explanation=e.msg) - kwargs['share_network_id'] = share_network_id + + common.check_share_network_is_active(share_network) + if availability_zone_id: if not db.share_network_subnet_get_by_availability_zone_id( context, share_network_id, @@ -402,6 +404,8 @@ class ShareMixin(object): if share_type: kwargs['share_type'] = share_type + if share_network_id: + kwargs['share_network_id'] = share_network_id new_share = self.share_api.create(context, share_proto, size, @@ -430,6 +434,11 @@ class ShareMixin(object): access_data.pop('metadata', None) share = self.share_api.get(context, id) + share_network_id = share.get('share_network_id') + if share_network_id: + share_network = db.share_network_get(context, share_network_id) + common.check_share_network_is_active(share_network) + if (not allow_on_error_status and self._any_instance_has_errored_rules(share)): msg = _("Access rules cannot be added while the share or any of " @@ -471,6 +480,13 @@ class ShareMixin(object): access_id = body.get( 'deny_access', body.get('os-deny_access'))['access_id'] + share = self.share_api.get(context, id) + share_network_id = share.get('share_network_id', None) + + if share_network_id: + share_network = db.share_network_get(context, share_network_id) + common.check_share_network_is_active(share_network) + try: access = self.share_api.access_get(context, access_id) if access.share_id != id: diff --git a/manila/api/v2/share_networks.py b/manila/api/v2/share_networks.py index 6d351ff3c0..3510f35aea 100644 --- a/manila/api/v2/share_networks.py +++ b/manila/api/v2/share_networks.py @@ -19,7 +19,6 @@ import copy from oslo_db import exception as db_exception from oslo_log import log from oslo_utils import timeutils -import six from six.moves import http_client import webob from webob import exc @@ -28,11 +27,13 @@ from manila.api import common from manila.api.openstack import api_version_request as api_version from manila.api.openstack import wsgi from manila.api.views import share_networks as share_networks_views +from manila.common import constants from manila.db import api as db_api from manila import exception from manila.i18n import _ from manila import policy from manila import quota +from manila import share from manila.share import rpcapi as share_rpcapi from manila import utils @@ -42,14 +43,20 @@ LOG = log.getLogger(__name__) QUOTAS = quota.QUOTAS -class ShareNetworkController(wsgi.Controller): +class ShareNetworkController(wsgi.Controller, wsgi.AdminActionsMixin): """The Share Network API controller for the OpenStack API.""" + resource_name = 'share_network' _view_builder_class = share_networks_views.ViewBuilder def __init__(self): super(ShareNetworkController, self).__init__() self.share_rpcapi = share_rpcapi.ShareAPI() + self.share_api = share.API() + + valid_statuses = { + 'status': set(constants.SHARE_NETWORK_STATUSES) + } def show(self, req, id): """Return data about the requested network info.""" @@ -59,7 +66,7 @@ class ShareNetworkController(wsgi.Controller): try: share_network = db_api.share_network_get(context, id) except exception.ShareNetworkNotFound as e: - raise exc.HTTPNotFound(explanation=six.text_type(e)) + raise exc.HTTPNotFound(explanation=e.msg) return self._view_builder.build_share_network(req, share_network) @@ -70,6 +77,9 @@ class ShareNetworkController(wsgi.Controller): def _share_network_contains_subnets(self, share_network): return len(share_network['share_network_subnets']) > 1 + def _update(self, *args, **kwargs): + db_api.share_network_update(*args, **kwargs) + def delete(self, req, id): """Delete specified share network.""" context = req.environ['manila.context'] @@ -78,7 +88,7 @@ class ShareNetworkController(wsgi.Controller): try: share_network = db_api.share_network_get(context, id) except exception.ShareNetworkNotFound as e: - raise exc.HTTPNotFound(explanation=six.text_type(e)) + raise exc.HTTPNotFound(explanation=e.msg) share_instances = ( db_api.share_instances_get_all_by_share_network(context, id) @@ -251,7 +261,7 @@ class ShareNetworkController(wsgi.Controller): try: share_network = db_api.share_network_get(context, id) except exception.ShareNetworkNotFound as e: - raise exc.HTTPNotFound(explanation=six.text_type(e)) + raise exc.HTTPNotFound(explanation=e.msg) update_values = body[RESOURCE_NAME] @@ -397,56 +407,62 @@ class ShareNetworkController(wsgi.Controller): share_network['id']) return self._view_builder.build_share_network(req, share_network) - def action(self, req, id, body): - _actions = { - 'add_security_service': self._add_security_service, - 'remove_security_service': self._remove_security_service - } - for action, data in body.items(): - try: - return _actions[action](req, id, data) - except KeyError: - msg = _("Share networks does not have %s action") % action - raise exc.HTTPBadRequest(explanation=msg) - - def _add_security_service(self, req, id, data): + @wsgi.action("add_security_service") + def add_security_service(self, req, id, body): """Associate share network with a given security service.""" context = req.environ['manila.context'] - policy.check_policy(context, RESOURCE_NAME, 'add_security_service') share_network = db_api.share_network_get(context, id) - if self._share_network_subnets_contain_share_servers(share_network): - msg = _("Cannot add security services. Share network is used.") - raise exc.HTTPForbidden(explanation=msg) - security_service = db_api.security_service_get( - context, data['security_service_id']) - for attached_service in share_network['security_services']: - if attached_service['type'] == security_service['type']: - msg = _("Cannot add security service to share network. " - "Security service with '%(ss_type)s' type already " - "added to '%(sn_id)s' share network") % { - 'ss_type': security_service['type'], - 'sn_id': share_network['id']} - raise exc.HTTPConflict(explanation=msg) + policy.check_policy(context, RESOURCE_NAME, 'add_security_service', + target_obj=share_network) + try: + data = body['add_security_service'] + + security_service = db_api.security_service_get( + context, data['security_service_id']) + except KeyError: + msg = "Malformed request body" + raise exc.HTTPBadRequest(explanation=msg) + + contain_share_servers = ( + self._share_network_subnets_contain_share_servers(share_network)) + + support_adding_to_in_use_networks = ( + req.api_version_request >= api_version.APIVersionRequest("2.63")) + + if contain_share_servers: + if not support_adding_to_in_use_networks: + msg = _("Cannot add security services. Share network is used.") + raise exc.HTTPForbidden(explanation=msg) + try: + self.share_api.update_share_network_security_service( + context, share_network, security_service) + except exception.ServiceIsDown as e: + raise exc.HTTPConflict(explanation=e.msg) + except exception.InvalidShareNetwork as e: + raise exc.HTTPBadRequest(explanation=e.msg) + except exception.InvalidSecurityService as e: + raise exc.HTTPConflict(explanation=e.msg) + try: share_network = db_api.share_network_add_security_service( context, id, data['security_service_id']) - except KeyError: - msg = "Malformed request body" - raise exc.HTTPBadRequest(explanation=msg) except exception.NotFound as e: - raise exc.HTTPNotFound(explanation=six.text_type(e)) + raise exc.HTTPNotFound(explanation=e.msg) except exception.ShareNetworkSecurityServiceAssociationError as e: - raise exc.HTTPBadRequest(explanation=six.text_type(e)) + raise exc.HTTPBadRequest(explanation=e.msg) return self._view_builder.build_share_network(req, share_network) - def _remove_security_service(self, req, id, data): + @wsgi.action('remove_security_service') + def remove_security_service(self, req, id, body): """Dissociate share network from a given security service.""" context = req.environ['manila.context'] - policy.check_policy(context, RESOURCE_NAME, 'remove_security_service') share_network = db_api.share_network_get(context, id) + policy.check_policy(context, RESOURCE_NAME, 'remove_security_service', + target_obj=share_network) + data = body['remove_security_service'] if self._share_network_subnets_contain_share_servers(share_network): msg = _("Cannot remove security services. Share network is used.") @@ -460,12 +476,152 @@ class ShareNetworkController(wsgi.Controller): msg = "Malformed request body" raise exc.HTTPBadRequest(explanation=msg) except exception.NotFound as e: - raise exc.HTTPNotFound(explanation=six.text_type(e)) + raise exc.HTTPNotFound(explanation=e.msg) except exception.ShareNetworkSecurityServiceDissociationError as e: - raise exc.HTTPBadRequest(explanation=six.text_type(e)) + raise exc.HTTPBadRequest(explanation=e.msg) return self._view_builder.build_share_network(req, share_network) + @wsgi.Controller.api_version('2.63') + @wsgi.action('update_security_service') + @wsgi.response(202) + def update_security_service(self, req, id, body): + """Update security service parameters from a given share network.""" + context = req.environ['manila.context'] + share_network = db_api.share_network_get(context, id) + policy.check_policy(context, RESOURCE_NAME, 'update_security_service', + target_obj=share_network) + try: + data = body['update_security_service'] + + current_security_service = db_api.security_service_get( + context, data['current_service_id'] + ) + new_security_service = db_api.security_service_get( + context, data['new_service_id'] + ) + except KeyError: + msg = "Malformed request body." + raise exc.HTTPBadRequest(explanation=msg) + except exception.NotFound: + msg = ("The current security service or the new security service " + "doesn't exist.") + raise exc.HTTPBadRequest(explanation=msg) + + try: + self.share_api.update_share_network_security_service( + context, share_network, new_security_service, + current_security_service=current_security_service) + except exception.ServiceIsDown as e: + raise exc.HTTPConflict(explanation=e.msg) + except exception.InvalidShareNetwork as e: + raise exc.HTTPBadRequest(explanation=e.msg) + except exception.InvalidSecurityService as e: + raise exc.HTTPConflict(explanation=e.msg) + + try: + share_network = db_api.share_network_update_security_service( + context, + id, + data['current_service_id'], + data['new_service_id']) + except exception.NotFound as e: + raise exc.HTTPNotFound(explanation=e.msg) + except (exception.ShareNetworkSecurityServiceDissociationError, + exception.ShareNetworkSecurityServiceAssociationError) as e: + raise exc.HTTPBadRequest(explanation=e.msg) + + return self._view_builder.build_share_network(req, share_network) + + @wsgi.Controller.api_version('2.63') + @wsgi.action('update_security_service_check') + @wsgi.response(202) + def check_update_security_service(self, req, id, body): + """Check the feasibility of updating a security service.""" + context = req.environ['manila.context'] + share_network = db_api.share_network_get(context, id) + policy.check_policy(context, RESOURCE_NAME, + 'update_security_service_check', + target_obj=share_network) + try: + data = body['update_security_service_check'] + + current_security_service = db_api.security_service_get( + context, data['current_service_id'] + ) + new_security_service = db_api.security_service_get( + context, data['new_service_id'] + ) + except KeyError: + msg = "Malformed request body." + raise exc.HTTPBadRequest(explanation=msg) + except exception.NotFound: + msg = ("The current security service or the new security service " + "doesn't exist.") + raise exc.HTTPBadRequest(explanation=msg) + + reset_check = utils.get_bool_from_api_params('reset_operation', data) + + try: + result = ( + self.share_api.check_share_network_security_service_update( + context, share_network, new_security_service, + current_security_service=current_security_service, + reset_operation=reset_check)) + except exception.ServiceIsDown as e: + raise exc.HTTPConflict(explanation=e.msg) + except exception.InvalidShareNetwork as e: + raise exc.HTTPBadRequest(explanation=e.msg) + except exception.InvalidSecurityService as e: + raise exc.HTTPConflict(explanation=e.msg) + + return self._view_builder.build_security_service_update_check( + req, data, result) + + @wsgi.Controller.api_version('2.63') + @wsgi.action("add_security_service_check") + @wsgi.response(202) + def check_add_security_service(self, req, id, body): + """Check the feasibility of associate a new security service.""" + context = req.environ['manila.context'] + share_network = db_api.share_network_get(context, id) + policy.check_policy(context, RESOURCE_NAME, + 'add_security_service_check', + target_obj=share_network) + data = body['add_security_service_check'] + try: + security_service = db_api.security_service_get( + context, data['security_service_id']) + except KeyError: + msg = "Malformed request body." + raise exc.HTTPBadRequest(explanation=msg) + except exception.NotFound: + msg = ("Security service %s doesn't exist." + ) % data['security_service_id'] + raise exc.HTTPBadRequest(explanation=msg) + + reset_check = utils.get_bool_from_api_params('reset_operation', data) + + try: + result = ( + self.share_api.check_share_network_security_service_update( + context, share_network, security_service, + reset_operation=reset_check)) + except exception.ServiceIsDown as e: + raise exc.HTTPConflict(explanation=e.msg) + except exception.InvalidShareNetwork as e: + raise exc.HTTPBadRequest(explanation=e.msg) + except exception.InvalidSecurityService as e: + raise exc.HTTPConflict(explanation=e.msg) + + return self._view_builder.build_security_service_update_check( + req, data, result) + + @wsgi.Controller.api_version('2.63') + @wsgi.action('reset_status') + def reset_status(self, req, id, body): + return self._reset_status(req, id, body) + def create_resource(): return wsgi.Resource(ShareNetworkController()) diff --git a/manila/api/v2/share_replicas.py b/manila/api/v2/share_replicas.py index 893cc015e0..1a31b21468 100644 --- a/manila/api/v2/share_replicas.py +++ b/manila/api/v2/share_replicas.py @@ -162,6 +162,10 @@ class ShareReplicationController(wsgi.Controller, wsgi.AdminActionsMixin): share_network_id = share_ref.get('share_network_id', None) + if share_network_id: + share_network = db.share_network_get(context, share_network_id) + common.check_share_network_is_active(share_network) + try: new_replica = self.share_api.create_share_replica( context, share_ref, availability_zone=availability_zone, @@ -226,6 +230,11 @@ class ShareReplicationController(wsgi.Controller, wsgi.AdminActionsMixin): msg = _("No replica exists with ID %s.") raise exc.HTTPNotFound(explanation=msg % id) + share_network_id = replica.get('share_network_id') + if share_network_id: + share_network = db.share_network_get(context, share_network_id) + common.check_share_network_is_active(share_network) + replica_state = replica.get('replica_state') if replica_state == constants.REPLICA_STATE_ACTIVE: diff --git a/manila/api/v2/share_servers.py b/manila/api/v2/share_servers.py index 5c4dba7d5b..b20f609423 100644 --- a/manila/api/v2/share_servers.py +++ b/manila/api/v2/share_servers.py @@ -18,6 +18,7 @@ from six.moves import http_client import webob from webob import exc +from manila.api import common from manila.api.openstack import wsgi from manila.api.v1 import share_servers from manila.api.views import share_server_migration as server_migration_views @@ -105,6 +106,17 @@ class ShareServerController(share_servers.ShareServerController, except exception.ShareServerNotFound as e: raise exc.HTTPNotFound(explanation=e.msg) + network_subnet_id = share_server.get('share_network_subnet_id', None) + if network_subnet_id: + subnet = db_api.share_network_subnet_get(context, + network_subnet_id) + share_network_id = subnet['share_network_id'] + else: + share_network_id = share_server.get('share_network_id') + + share_network = db_api.share_network_get(context, share_network_id) + common.check_share_network_is_active(share_network) + allowed_statuses = [constants.STATUS_ERROR, constants.STATUS_ACTIVE, constants.STATUS_MANAGE_ERROR, constants.STATUS_UNMANAGE_ERROR] @@ -172,6 +184,8 @@ class ShareServerController(share_servers.ShareServerController, "with API version >= 2.51.") % share_network_id raise exc.HTTPBadRequest(explanation=msg) + common.check_share_network_is_active(network_subnet['share_network']) + if share_utils.extract_host(host, 'pool'): msg = _("Host parameter should not contain pool.") raise exc.HTTPBadRequest(explanation=msg) @@ -242,6 +256,13 @@ class ShareServerController(share_servers.ShareServerController, msg = _("Share network %s not " "found.") % new_share_network_id raise exc.HTTPBadRequest(explanation=msg) + common.check_share_network_is_active(new_share_network) + else: + share_network_id = ( + share_server['share_network_subnet']['share_network_id']) + current_share_network = db_api.share_network_get( + context, share_network_id) + common.check_share_network_is_active(current_share_network) try: self.share_api.share_server_migration_start( @@ -359,6 +380,13 @@ class ShareServerController(share_servers.ShareServerController, msg = _("Share network %s not " "found.") % new_share_network_id raise exc.HTTPBadRequest(explanation=msg) + common.check_share_network_is_active(new_share_network) + else: + share_network_id = ( + share_server['share_network_subnet']['share_network_id']) + current_share_network = db_api.share_network_get( + context, share_network_id) + common.check_share_network_is_active(current_share_network) try: result = self.share_api.share_server_migration_check( diff --git a/manila/api/v2/share_snapshots.py b/manila/api/v2/share_snapshots.py index 540cd6f7d5..37af493e62 100644 --- a/manila/api/v2/share_snapshots.py +++ b/manila/api/v2/share_snapshots.py @@ -27,6 +27,7 @@ from manila.api.openstack import wsgi from manila.api.v1 import share_snapshots from manila.api.views import share_snapshots as snapshot_views from manila.common import constants +from manila.db import api as db_api from manila import exception from manila.i18n import _ from manila import share @@ -162,6 +163,13 @@ class ShareSnapshotsController(share_snapshots.ShareSnapshotMixin, msg = _("Required parameter %s is empty.") % parameter raise exc_response(explanation=msg) + def _check_if_share_share_network_is_active(self, context, snapshot): + share_network_id = snapshot['share'].get('share_network_id') + if share_network_id: + share_network = db_api.share_network_get( + context, share_network_id) + common.check_share_network_is_active(share_network) + def _allow(self, req, id, body, enable_ipv6=False): context = req.environ['manila.context'] @@ -184,6 +192,8 @@ class ShareSnapshotsController(share_snapshots.ShareSnapshotMixin, snapshot = self.share_api.get_snapshot(context, id) + self._check_if_share_share_network_is_active(context, snapshot) + self._check_mount_snapshot_support(context, snapshot) try: @@ -212,6 +222,8 @@ class ShareSnapshotsController(share_snapshots.ShareSnapshotMixin, self._check_mount_snapshot_support(context, snapshot) + self._check_if_share_share_network_is_active(context, snapshot) + access = self.share_api.snapshot_access_get(context, access_id) if access['share_snapshot_id'] != snapshot['id']: diff --git a/manila/api/v2/shares.py b/manila/api/v2/shares.py index 1501c26eb0..0dc9e49275 100644 --- a/manila/api/v2/shares.py +++ b/manila/api/v2/shares.py @@ -18,6 +18,7 @@ from six.moves import http_client import webob from webob import exc +from manila.api import common from manila.api.openstack import api_version_request as api_version from manila.api.openstack import wsgi from manila.api.v1 import share_manage @@ -256,6 +257,13 @@ class ShareController(shares.ShareMixin, msg = _("Share network %s not " "found.") % new_share_network_id raise exc.HTTPBadRequest(explanation=msg) + common.check_share_network_is_active(new_share_network) + else: + share_network_id = share.get('share_network_id', None) + if share_network_id: + current_share_network = db.share_network_get( + context, share_network_id) + common.check_share_network_is_active(current_share_network) new_share_type_id = params.get('new_share_type_id', None) if new_share_type_id: diff --git a/manila/api/views/share_networks.py b/manila/api/views/share_networks.py index c05bba3957..d1f6083985 100644 --- a/manila/api/views/share_networks.py +++ b/manila/api/views/share_networks.py @@ -21,7 +21,8 @@ class ViewBuilder(common.ViewBuilder): _collection_name = 'share_networks' _detail_version_modifiers = ["add_gateway", "add_mtu", "add_nova_net_id", - "add_subnets"] + "add_subnets", + "add_status_and_sec_service_update_fields"] def build_share_network(self, request, share_network): """View of a share network.""" @@ -35,6 +36,25 @@ class ViewBuilder(common.ViewBuilder): request, share_network, is_detail) for share_network in share_networks]} + def build_security_service_update_check(self, request, params, result): + """View of security service add or update check.""" + context = request.environ['manila.context'] + requested_operation = { + 'operation': ('update_security_service' + if params.get('current_service_id') + else 'add_security_service'), + 'current_security_service': params.get('current_service_id'), + 'new_security_service': (params.get('new_service_id') or + params.get('security_service_id')) + } + view = { + 'compatible': result['compatible'], + 'requested_operation': requested_operation, + } + if context.is_admin: + view['hosts_check_result'] = result['hosts_check_result'] + return view + def _update_share_network_info(self, request, share_network): for sns in share_network.get('share_network_subnets') or []: if sns.get('is_default') and sns.get('is_default') is True: @@ -108,3 +128,10 @@ class ViewBuilder(common.ViewBuilder): @common.ViewBuilder.versioned_method("1.0", "2.25") def add_nova_net_id(self, context, network_dict, network): network_dict['nova_net_id'] = None + + @common.ViewBuilder.versioned_method("2.63") + def add_status_and_sec_service_update_fields( + self, context, network_dict, network): + network_dict['status'] = network.get('status') + network_dict['security_service_update_support'] = network.get( + 'security_service_update_support') diff --git a/manila/api/views/share_servers.py b/manila/api/views/share_servers.py index 30b2543855..8b33a4106e 100644 --- a/manila/api/views/share_servers.py +++ b/manila/api/views/share_servers.py @@ -23,7 +23,8 @@ class ViewBuilder(common.ViewBuilder): _detail_version_modifiers = [ "add_is_auto_deletable_and_identifier_fields", "add_share_network_subnet_id_field", - "add_task_state_and_source_server_fields" + "add_task_state_and_source_server_fields", + "add_sec_service_update_fields" ] def build_share_server(self, request, share_server): @@ -82,3 +83,9 @@ class ViewBuilder(common.ViewBuilder): share_server_dict['task_state'] = share_server['task_state'] share_server_dict['source_share_server_id'] = ( share_server['source_share_server_id']) + + @common.ViewBuilder.versioned_method("2.63") + def add_sec_service_update_fields( + self, context, share_server_dict, share_server): + share_server_dict['security_service_update_support'] = share_server[ + 'security_service_update_support'] diff --git a/manila/cmd/manage.py b/manila/cmd/manage.py index ca684c5b0b..96f62b38f7 100644 --- a/manila/cmd/manage.py +++ b/manila/cmd/manage.py @@ -78,6 +78,10 @@ HOST_UPDATE_HELP_MSG = ("A fully qualified host string is of the format " HOST_UPDATE_CURRENT_HOST_HELP = ("Current share host name. %s" % HOST_UPDATE_HELP_MSG) HOST_UPDATE_NEW_HOST_HELP = "New share host name. %s" % HOST_UPDATE_HELP_MSG +SHARE_SERVERS_UPDATE_HELP = ("List of share servers to be updated, separated " + "by commas.") +SHARE_SERVERS_UPDATE_CAPABILITIES_HELP = ( + "List of share server capabilities to be updated, separated by commas.") # Decorators for actions @@ -399,6 +403,42 @@ class ShareCommands(object): print(msg % msg_args) +class ShareServerCommands(object): + @args('--share_servers', required=True, + help=SHARE_SERVERS_UPDATE_HELP) + @args('--capabilities', required=True, + help=SHARE_SERVERS_UPDATE_CAPABILITIES_HELP) + @args('--value', required=False, type=bool, default=False, + help="If those capabilities will be enabled (True) or disabled " + "(False)") + def update_share_server_capabilities(self, share_servers, capabilities, + value=False): + """Update the share server capabilities. + + This method receives a list of share servers and capabilities + in order to have it updated with the value specified. If the value + was not specified the default is False. + """ + share_servers = [server.strip() for server in share_servers.split(",")] + capabilities = [cap.strip() for cap in capabilities.split(",")] + supported_capabilities = ['security_service_update_support'] + + values = dict() + for capability in capabilities: + if capability not in supported_capabilities: + print("One or more capabilities are invalid for this " + "operation. The supported capability(ies) is(are) %s." + % supported_capabilities) + sys.exit(1) + values[capability] = value + + ctxt = context.get_admin_context() + db.share_servers_update(ctxt, share_servers, values) + print("The capability(ies) %s of the following share server(s)" + " %s was(were) updated to %s." % + (capabilities, share_servers, value)) + + CATEGORIES = { 'config': ConfigCommands, 'db': DbCommands, @@ -406,6 +446,7 @@ CATEGORIES = { 'logs': GetLogCommands, 'service': ServiceCommands, 'share': ShareCommands, + 'share_server': ShareServerCommands, 'shell': ShellCommands, 'version': VersionCommands } diff --git a/manila/common/constants.py b/manila/common/constants.py index ce97e91395..6cf56ef63d 100644 --- a/manila/common/constants.py +++ b/manila/common/constants.py @@ -66,6 +66,14 @@ STATUS_ACTIVE = 'active' STATUS_SERVER_MIGRATING = 'server_migrating' STATUS_SERVER_MIGRATING_TO = 'server_migrating_to' +# Share server update statuses +STATUS_SERVER_NETWORK_CHANGE = 'network_change' + +# Share network statuses +STATUS_NETWORK_ACTIVE = 'active' +STATUS_NETWORK_ERROR = 'error' +STATUS_NETWORK_CHANGE = 'network_change' + ACCESS_RULES_STATES = ( ACCESS_STATE_QUEUED_TO_APPLY, ACCESS_STATE_QUEUED_TO_DENY, @@ -214,6 +222,13 @@ SHARE_SERVER_STATUSES = ( STATUS_INACTIVE, STATUS_SERVER_MIGRATING, STATUS_SERVER_MIGRATING_TO, + STATUS_SERVER_NETWORK_CHANGE, +) + +SHARE_NETWORK_STATUSES = ( + STATUS_NETWORK_ACTIVE, + STATUS_NETWORK_ERROR, + STATUS_NETWORK_CHANGE, ) REPLICA_STATE_ACTIVE = 'active' diff --git a/manila/db/api.py b/manila/db/api.py index 250a6ae370..5f568c9f17 100644 --- a/manila/db/api.py +++ b/manila/db/api.py @@ -854,17 +854,34 @@ def share_network_get_all_by_security_service(context, security_service_id): def share_network_add_security_service(context, id, security_service_id): + """Associate a security service with a share network.""" return IMPL.share_network_add_security_service(context, id, security_service_id) def share_network_remove_security_service(context, id, security_service_id): + """Dissociate a security service from a share network.""" return IMPL.share_network_remove_security_service(context, id, security_service_id) +def share_network_security_service_association_get( + context, share_network_id, security_service_id): + """Get given share network and security service association.""" + return IMPL.share_network_security_service_association_get( + context, share_network_id, security_service_id) + + +def share_network_update_security_service(context, id, + current_security_service_id, + new_security_service_id): + """Update a security service association with a share network.""" + return IMPL.share_network_update_security_service( + context, id, current_security_service_id, new_security_service_id) + + def count_share_networks(context, project_id, user_id=None, share_type_id=None, session=None): return IMPL.count_share_networks( @@ -1022,6 +1039,12 @@ def share_server_backend_details_set(context, share_server_id, server_details): server_details) +def share_servers_update(context, share_server_ids, values): + """Updates values of a bunch of share servers at once.""" + return IMPL.share_servers_update( + context, share_server_ids, values) + + ################## @@ -1483,3 +1506,22 @@ def backend_info_update(context, host, value=None, """Update hash info for host.""" return IMPL.backend_info_update(context, host=host, value=value, delete_existing=delete_existing) + +#################### + + +def async_operation_data_get(context, entity_id, key=None, default=None): + """Get one, list or all key-value pairs for given entity_id.""" + return IMPL.async_operation_data_get(context, entity_id, key, default) + + +def async_operation_data_update(context, entity_id, details, + delete_existing=False): + """Update key-value pairs for given entity_id.""" + return IMPL.async_operation_data_update(context, entity_id, details, + delete_existing) + + +def async_operation_data_delete(context, entity_id, key=None): + """Remove one, list or all key-value pairs for given entity_id.""" + return IMPL.async_operation_data_delete(context, entity_id, key) diff --git a/manila/db/migrations/alembic/versions/478c445d8d3e_add_security_service_update_control_fields.py b/manila/db/migrations/alembic/versions/478c445d8d3e_add_security_service_update_control_fields.py new file mode 100644 index 0000000000..fb40aab07f --- /dev/null +++ b/manila/db/migrations/alembic/versions/478c445d8d3e_add_security_service_update_control_fields.py @@ -0,0 +1,90 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""add_security_service_update_control_fields + +Revision ID: 478c445d8d3e +Revises: 0c23aec99b74 +Create Date: 2020-12-07 12:33:41.444202 + +""" + +# revision identifiers, used by Alembic. +revision = '478c445d8d3e' +down_revision = '0c23aec99b74' + +from alembic import op +from manila.common import constants +from oslo_log import log +import sqlalchemy as sa + +SHARE_SERVERS_TABLE = 'share_servers' +SHARE_NETWORKS_TABLE = 'share_networks' +ASYNC_OPERATION_DATA_TABLE = 'async_operation_data' +LOG = log.getLogger(__name__) + + +def upgrade(): + context = op.get_context() + mysql_dl = context.bind.dialect.name == 'mysql' + datetime_type = (sa.dialects.mysql.DATETIME(fsp=6) + if mysql_dl else sa.DateTime) + try: + op.create_table( + ASYNC_OPERATION_DATA_TABLE, + sa.Column('created_at', datetime_type), + sa.Column('updated_at', datetime_type), + sa.Column('deleted_at', datetime_type), + sa.Column('deleted', sa.Integer, default=0), + sa.Column('entity_uuid', sa.String(36), + nullable=False, primary_key=True), + sa.Column('key', sa.String(255), + nullable=False, primary_key=True), + sa.Column('value', sa.String(1023), nullable=False), + mysql_engine='InnoDB', + ) + op.add_column( + SHARE_SERVERS_TABLE, + sa.Column('security_service_update_support', sa.Boolean, + nullable=False, server_default=sa.sql.false()) + ) + op.add_column( + SHARE_NETWORKS_TABLE, + sa.Column('status', sa.String(36), nullable=False, + server_default=constants.STATUS_NETWORK_ACTIVE)) + except Exception: + msg_args = { + 'async_op_table': ASYNC_OPERATION_DATA_TABLE, + 'sec_serv_column': 'share_servers.security_service_update_support', + 'shr_net_column': 'share_networks.status', + } + LOG.error('Table %(async_op_table)s and table columns ' + '%(sec_serv_column)s and %(shr_net_column)s were not' + ' created!', msg_args) + raise + + +def downgrade(): + try: + op.drop_table(ASYNC_OPERATION_DATA_TABLE) + op.drop_column(SHARE_SERVERS_TABLE, 'security_service_update_support') + op.drop_column(SHARE_NETWORKS_TABLE, 'status') + except Exception: + msg_args = { + 'async_op_table': ASYNC_OPERATION_DATA_TABLE, + 'sec_serv_column': 'share_servers.security_service_update_support', + 'shr_net_column': 'share_networks.status', + } + LOG.error('Table %(async_op_table)s and table columns ' + '%(sec_serv_column)s and %(shr_net_column)s were not ' + 'dropped!', msg_args) + raise diff --git a/manila/db/sqlalchemy/api.py b/manila/db/sqlalchemy/api.py index f759a137cc..f714dc2c65 100644 --- a/manila/db/sqlalchemy/api.py +++ b/manila/db/sqlalchemy/api.py @@ -3847,6 +3847,21 @@ def share_network_add_security_service(context, id, security_service_id): return share_nw_ref +@require_context +def share_network_security_service_association_get( + context, share_network_id, security_service_id): + session = get_session() + + with session.begin(): + association = (model_query( + context, + models.ShareNetworkSecurityServiceAssociation, + session=session).filter_by( + share_network_id=share_network_id).filter_by( + security_service_id=security_service_id).first()) + return association + + @require_context def share_network_remove_security_service(context, id, security_service_id): session = get_session() @@ -3874,6 +3889,43 @@ def share_network_remove_security_service(context, id, security_service_id): return share_nw_ref +@require_context +def share_network_update_security_service(context, id, + current_security_service_id, + new_security_service_id): + session = get_session() + + with session.begin(): + share_nw_ref = share_network_get(context, id, session=session) + # Check if the old security service exists + security_service_get(context, current_security_service_id, + session=session) + new_security_service_ref = security_service_get( + context, new_security_service_id, session=session) + + assoc_ref = (model_query( + context, + models.ShareNetworkSecurityServiceAssociation, + session=session).filter_by( + share_network_id=id).filter_by( + security_service_id=current_security_service_id).first()) + + if assoc_ref: + assoc_ref.soft_delete(session) + else: + msg = "No association defined" + raise exception.ShareNetworkSecurityServiceDissociationError( + share_network_id=id, + security_service_id=current_security_service_id, + reason=msg) + + # Add new association + share_nw_ref.security_services += [new_security_service_ref] + share_nw_ref.save(session=session) + + return share_nw_ref + + @require_context def count_share_networks(context, project_id, user_id=None, share_type_id=None, session=None): @@ -4117,7 +4169,10 @@ def share_server_get_all_with_filters(context, filters): if filters.get('source_share_server_id'): query = query.filter_by( source_share_server_id=filters.get('source_share_server_id')) - + if filters.get('share_network_id'): + query = query.filter( + models.ShareNetworkSubnet.share_network_id == + filters.get('share_network_id')) return query.all() @@ -4177,6 +4232,20 @@ def share_server_backend_details_delete(context, share_server_id, item.soft_delete(session) +@require_context +def share_servers_update( + context, share_server_ids, values, session=None): + session = session or get_session() + + result = ( + model_query( + context, models.ShareServer, read_deleted="no", + session=session).filter( + models.ShareServer.id.in_(share_server_ids)).update( + values, synchronize_session=False)) + return result + + ################### def _driver_private_data_query(session, context, entity_id, key=None, @@ -5775,3 +5844,92 @@ def _backend_info_query(session, context, host, read_deleted=False): ).first() return result + +################### + + +def _async_operation_data_query(session, context, entity_id, key=None, + read_deleted=False): + query = model_query( + context, models.AsynchronousOperationData, session=session, + read_deleted=read_deleted, + ).filter_by( + entity_uuid=entity_id, + ) + + if isinstance(key, list): + return query.filter(models.AsynchronousOperationData.key.in_(key)) + elif key is not None: + return query.filter_by(key=key) + + return query + + +@require_context +def async_operation_data_get(context, entity_id, key=None, + default=None, session=None): + if not session: + session = get_session() + + query = _async_operation_data_query(session, context, entity_id, key) + + if key is None or isinstance(key, list): + return {item.key: item.value for item in query.all()} + else: + result = query.first() + return result["value"] if result is not None else default + + +@require_context +def async_operation_data_update(context, entity_id, details, + delete_existing=False, session=None): + new_details = copy.deepcopy(details) + + if not session: + session = get_session() + + with session.begin(): + # Process existing data + original_data = session.query( + models.AsynchronousOperationData).filter_by( + entity_uuid=entity_id).all() + + for data_ref in original_data: + in_new_details = data_ref['key'] in new_details + + if in_new_details: + new_value = str(new_details.pop(data_ref['key'])) + data_ref.update({ + "value": new_value, + "deleted": 0, + "deleted_at": None + }) + data_ref.save(session=session) + elif delete_existing and data_ref['deleted'] != 1: + data_ref.update({ + "deleted": 1, "deleted_at": timeutils.utcnow() + }) + data_ref.save(session=session) + + # Add new data + for key, value in new_details.items(): + data_ref = models.AsynchronousOperationData() + data_ref.update({ + "entity_uuid": entity_id, + "key": key, + "value": str(value) + }) + data_ref.save(session=session) + + return details + + +@require_context +def async_operation_data_delete(context, entity_id, key=None, session=None): + if not session: + session = get_session() + + with session.begin(): + query = _async_operation_data_query(session, context, + entity_id, key) + query.update({"deleted": 1, "deleted_at": timeutils.utcnow()}) diff --git a/manila/db/sqlalchemy/models.py b/manila/db/sqlalchemy/models.py index 8621772648..5d045f968e 100644 --- a/manila/db/sqlalchemy/models.py +++ b/manila/db/sqlalchemy/models.py @@ -188,7 +188,8 @@ class Share(BASE, ManilaBase): __tablename__ = 'shares' _extra_keys = ['name', 'export_location', 'export_locations', 'status', 'host', 'share_server_id', 'share_network_id', - 'availability_zone', 'access_rules_status', 'share_type_id'] + 'availability_zone', 'access_rules_status', 'share_type_id', + 'share_network_status'] @property def name(self): @@ -227,7 +228,8 @@ class Share(BASE, ManilaBase): def __getattr__(self, item): proxified_properties = ('status', 'host', 'share_server_id', 'share_network_id', 'availability_zone', - 'share_type_id', 'share_type') + 'share_type_id', 'share_type', + 'share_network_status') if item in proxified_properties: return getattr(self.instance, item, None) @@ -920,6 +922,10 @@ class ShareNetwork(BASE, ManilaBase): user_id = Column(String(255), nullable=False) name = Column(String(255), nullable=True) description = Column(String(255), nullable=True) + status = Column(Enum( + constants.STATUS_NETWORK_ACTIVE, constants.STATUS_NETWORK_ERROR, + constants.STATUS_NETWORK_CHANGE), + default=constants.STATUS_NETWORK_ACTIVE) security_services = orm.relationship( "SecurityService", secondary="share_network_security_service_association", @@ -935,7 +941,7 @@ class ShareNetwork(BASE, ManilaBase): 'SecurityService.deleted == "False")') share_instances = orm.relationship( "ShareInstance", - backref='share_network', + backref=orm.backref('share_network'), primaryjoin='and_(' 'ShareNetwork.id == ShareInstance.share_network_id,' 'ShareInstance.deleted == "False")') @@ -947,6 +953,18 @@ class ShareNetwork(BASE, ManilaBase): '(ShareNetwork.id == ShareNetworkSubnet.share_network_id,' 'ShareNetworkSubnet.deleted == "False")') + @property + def security_service_update_support(self): + share_servers_support_updating = [] + for network_subnet in self.share_network_subnets: + for server in network_subnet['share_servers']: + share_servers_support_updating.append( + server['security_service_update_support']) + # NOTE(carloss): all share servers within this share network must + # support updating security services in order to have this property + # set to True. + return all(share_servers_support_updating) + class ShareNetworkSubnet(BASE, ManilaBase): """Represents a share network subnet used by some resources.""" @@ -998,6 +1016,10 @@ class ShareNetworkSubnet(BASE, ManilaBase): def share_network_name(self): return self.share_network['name'] + @property + def share_network_status(self): + return self.share_network['status'] + class ShareServer(BASE, ManilaBase): """Represents share server used by share.""" @@ -1013,6 +1035,8 @@ class ShareServer(BASE, ManilaBase): task_state = Column(String(255), nullable=True) source_share_server_id = Column(String(36), ForeignKey('share_servers.id'), nullable=True) + security_service_update_support = Column( + Boolean, nullable=False, default=False) status = Column(Enum( constants.STATUS_INACTIVE, constants.STATUS_ACTIVE, constants.STATUS_ERROR, constants.STATUS_DELETING, @@ -1020,7 +1044,8 @@ class ShareServer(BASE, ManilaBase): constants.STATUS_MANAGING, constants.STATUS_UNMANAGING, constants.STATUS_UNMANAGE_ERROR, constants.STATUS_MANAGE_ERROR, constants.STATUS_SERVER_MIGRATING, - constants.STATUS_SERVER_MIGRATING_TO), + constants.STATUS_SERVER_MIGRATING_TO, + constants.STATUS_SERVER_NETWORK_CHANGE), default=constants.STATUS_INACTIVE) network_allocations = orm.relationship( "NetworkAllocation", @@ -1053,6 +1078,10 @@ class ShareServer(BASE, ManilaBase): return {model['key']: model['value'] for model in self._backend_details} + @property + def share_network_status(self): + return self.share_network_subnet['share_network']['status'] + _extra_keys = ['backend_details'] @@ -1309,6 +1338,14 @@ class BackendInfo(BASE, ManilaBase): info_hash = Column(String(255)) +class AsynchronousOperationData(BASE, ManilaBase): + """Represents data as key-value pairs for asynchronous operations.""" + __tablename__ = 'async_operation_data' + entity_uuid = Column(String(36), nullable=False, primary_key=True) + key = Column(String(255), nullable=False, primary_key=True) + value = Column(String(1023), nullable=False) + + def register_models(): """Register Models and create metadata. diff --git a/manila/exception.py b/manila/exception.py index e7abd74b0d..eec125d74d 100644 --- a/manila/exception.py +++ b/manila/exception.py @@ -242,6 +242,10 @@ class ShareServerNotFoundByFilters(ShareServerNotFound): "filters: %(filters_description)s.") +class InvalidShareNetwork(Invalid): + message = _("Invalid share network: %(reason)s") + + class ShareServerInUse(InUse): message = _("Share server %(share_server_id)s is in use.") @@ -597,6 +601,10 @@ class SecurityServiceNotFound(NotFound): message = _("Security service %(security_service_id)s could not be found.") +class InvalidSecurityService(Invalid): + message = _("Invalid security service: %(reason)s") + + class ShareNetworkSecurityServiceAssociationError(ManilaException): message = _("Failed to associate share network %(share_network_id)s" " and security service %(security_service_id)s: %(reason)s.") diff --git a/manila/policies/share_network.py b/manila/policies/share_network.py index e643aa02e4..1d58fd1bf9 100644 --- a/manila/policies/share_network.py +++ b/manila/policies/share_network.py @@ -57,7 +57,22 @@ deprecated_share_network_get_all = policy.DeprecatedRule( name=BASE_POLICY_NAME % 'get_all_share_networks', check_str=base.RULE_ADMIN_API ) - +deprecated_share_network_add_security_service_check = policy.DeprecatedRule( + name=BASE_POLICY_NAME % 'add_security_service_check', + check_str=base.RULE_DEFAULT +) +deprecated_share_network_update_security_service = policy.DeprecatedRule( + name=BASE_POLICY_NAME % 'update_security_service', + check_str=base.RULE_DEFAULT +) +deprecated_share_network_update_security_service_check = policy.DeprecatedRule( + name=BASE_POLICY_NAME % 'update_security_service_check', + check_str=base.RULE_DEFAULT +) +deprecated_share_network_reset_status = policy.DeprecatedRule( + name=BASE_POLICY_NAME % 'reset_status', + check_str=base.RULE_ADMIN_API +) share_network_policies = [ policy.DocumentedRuleDefault( @@ -173,6 +188,22 @@ share_network_policies = [ deprecated_reason=DEPRECATED_REASON, deprecated_since=versionutils.deprecated.WALLABY ), + policy.DocumentedRuleDefault( + name=BASE_POLICY_NAME % 'add_security_service_check', + check_str=base.SYSTEM_ADMIN_OR_PROJECT_MEMBER, + scope_types=['system', 'project'], + description="Check the feasibility of add security service to a share " + "network.", + operations=[ + { + 'method': 'POST', + 'path': '/share-networks/{share_network_id}/action' + } + ], + deprecated_rule=deprecated_share_network_add_security_service_check, + deprecated_reason=DEPRECATED_REASON, + deprecated_since=versionutils.deprecated.WALLABY + ), policy.DocumentedRuleDefault( name=BASE_POLICY_NAME % 'remove_security_service', check_str=base.SYSTEM_ADMIN_OR_PROJECT_MEMBER, @@ -188,6 +219,52 @@ share_network_policies = [ deprecated_reason=DEPRECATED_REASON, deprecated_since=versionutils.deprecated.WALLABY ), + policy.DocumentedRuleDefault( + name=BASE_POLICY_NAME % 'update_security_service', + check_str=base.SYSTEM_ADMIN_OR_PROJECT_MEMBER, + scope_types=['system', 'project'], + description="Update security service from share network.", + operations=[ + { + 'method': 'POST', + 'path': '/share-networks/{share_network_id}/action' + } + ], + deprecated_rule=deprecated_share_network_update_security_service, + deprecated_reason=DEPRECATED_REASON, + deprecated_since=versionutils.deprecated.WALLABY + ), + policy.DocumentedRuleDefault( + name=BASE_POLICY_NAME % 'update_security_service_check', + check_str=base.SYSTEM_ADMIN_OR_PROJECT_MEMBER, + scope_types=['system', 'project'], + description="Check the feasibility of update a security service from " + "share network.", + operations=[ + { + 'method': 'POST', + 'path': '/share-networks/{share_network_id}/action' + } + ], + deprecated_rule=deprecated_share_network_update_security_service_check, + deprecated_reason=DEPRECATED_REASON, + deprecated_since=versionutils.deprecated.WALLABY + ), + policy.DocumentedRuleDefault( + name=BASE_POLICY_NAME % 'reset_status', + check_str=base.SYSTEM_ADMIN_OR_PROJECT_ADMIN, + scope_types=['system', 'project'], + description="Reset share network`s status.", + operations=[ + { + 'method': 'POST', + 'path': '/share-networks/{share_network_id}/action' + } + ], + deprecated_rule=deprecated_share_network_reset_status, + deprecated_reason=DEPRECATED_REASON, + deprecated_since=versionutils.deprecated.WALLABY + ), policy.DocumentedRuleDefault( name=BASE_POLICY_NAME % 'get_all_share_networks', check_str=base.SYSTEM_READER, diff --git a/manila/scheduler/host_manager.py b/manila/scheduler/host_manager.py index 320811d480..96707d4450 100644 --- a/manila/scheduler/host_manager.py +++ b/manila/scheduler/host_manager.py @@ -146,6 +146,7 @@ class HostState(object): self.replication_domain = None self.ipv4_support = None self.ipv6_support = None + self.security_service_update_support = False # PoolState for all pools self.pools = {} @@ -335,6 +336,10 @@ class HostState(object): pool_cap['sg_consistent_snapshot_support'] = ( self.sg_consistent_snapshot_support) + if 'security_service_update_support' not in pool_cap: + pool_cap['security_service_update_support'] = ( + self.security_service_update_support) + if self.ipv4_support is not None: pool_cap['ipv4_support'] = self.ipv4_support @@ -364,6 +369,8 @@ class HostState(object): self.ipv4_support = capability['ipv4_support'] if capability.get('ipv6_support') is not None: self.ipv6_support = capability['ipv6_support'] + self.security_service_update_support = capability.get( + 'security_service_update_support', False) def consume_from_share(self, share): """Incrementally update host state from an share.""" @@ -460,6 +467,8 @@ class PoolState(HostState): 'replication_domain') self.sg_consistent_snapshot_support = capability.get( 'sg_consistent_snapshot_support') + self.security_service_update_support = capability.get( + 'security_service_update_support', False) def update_pools(self, capability): # Do nothing, since we don't have pools within pool, yet diff --git a/manila/scheduler/utils.py b/manila/scheduler/utils.py index 481117531b..59ff9632fa 100644 --- a/manila/scheduler/utils.py +++ b/manila/scheduler/utils.py @@ -57,6 +57,8 @@ def generate_stats(host_state, properties): host_state.sg_consistent_snapshot_support), 'ipv4_support': host_state.ipv4_support, 'ipv6_support': host_state.ipv6_support, + 'security_service_update_support': ( + host_state.security_service_update_support) } host_caps = host_state.capabilities diff --git a/manila/share/access.py b/manila/share/access.py index 10628485ee..90ea81186f 100644 --- a/manila/share/access.py +++ b/manila/share/access.py @@ -94,6 +94,25 @@ class ShareInstanceAccessDatabaseMixin(object): context, share_instance_id, updates, with_share_data=True) return share_instance + def update_share_instances_access_rules_status( + self, context, status, share_instance_ids): + """Update the access_rules_status of all share instances. + + .. note:: + Before making this call, make sure that all share instances have + their status set to a value that will block new operations to + happen during this update. + + :param status: Force a state change on all share instances regardless + of the prior state. + :param share_instance_ids: List of share instance ids to have their + access rules status updated. + """ + updates = {'access_rules_status': status} + + self.db.share_instances_status_update( + context, share_instance_ids, updates) + @locked_access_rules_operation def get_and_update_share_instance_access_rules(self, context, filters=None, updates=None, @@ -321,7 +340,7 @@ class ShareInstanceAccess(ShareInstanceAccessDatabaseMixin): add_rules, delete_rules, rules_to_be_removed_from_db, share_server) - self._process_driver_rule_updates( + self.process_driver_rule_updates( context, driver_rule_updates, share_instance_id) # Update access rules that are still in 'applying' state @@ -434,8 +453,8 @@ class ShareInstanceAccess(ShareInstanceAccessDatabaseMixin): context, conditionally_change=conditionally_change, share_instance_id=share_instance_id) - def _process_driver_rule_updates(self, context, driver_rule_updates, - share_instance_id): + def process_driver_rule_updates(self, context, driver_rule_updates, + share_instance_id): for rule_id, rule_updates in driver_rule_updates.items(): if 'state' in rule_updates: # We allow updates *only* if the state is unchanged from diff --git a/manila/share/api.py b/manila/share/api.py index 812ac6255f..dcdfd94afe 100644 --- a/manila/share/api.py +++ b/manila/share/api.py @@ -19,6 +19,7 @@ """ Handles all requests relating to shares. """ +import json from oslo_config import cfg from oslo_log import log @@ -27,7 +28,10 @@ from oslo_utils import strutils from oslo_utils import timeutils import six +from manila.api import common as api_common from manila.common import constants +from manila import context as manila_context +from manila import coordination from manila.data import rpcapi as data_rpcapi from manila.db import base from manila import exception @@ -61,6 +65,29 @@ GB = 1048576 * 1024 QUOTAS = quota.QUOTAS +def locked_security_service_update_operation(operation): + """Lock decorator for security service operation. + + Takes a named lock prior to executing the operation. The lock is named with + the ids of the security services. + """ + + def wrapped(*args, **kwargs): + new_id = kwargs.get('new_security_service_id', '') + current_id = kwargs.get('current_security_service_id', '') + + @coordination.synchronized( + 'locked-security-service-update-operation-%(new)s-%(curr)s' % { + 'new': new_id, + 'curr': current_id, + }) + def locked_security_service_operation(*_args, **_kwargs): + return operation(*_args, **_kwargs) + return locked_security_service_operation(*args, **kwargs) + + return wrapped + + class API(base.Base): """API for interacting with the share manager.""" @@ -69,6 +96,7 @@ class API(base.Base): self.scheduler_rpcapi = scheduler_rpcapi.SchedulerAPI() self.share_rpcapi = share_rpcapi.ShareAPI() self.access_helper = access.ShareInstanceAccess(self.db, None) + coordination.LOCK_COORDINATOR.start() def _get_all_availability_zones_with_subnets(self, context, share_network_id): @@ -826,6 +854,16 @@ class API(base.Base): context, share_server['share_network_subnet_id']) share_data['share_network_id'] = subnet['share_network_id'] + try: + share_network = self.db.share_network_get( + context, share_data['share_network_id']) + except exception.ShareNetworkNotFound: + msg = _("Share network %s was not found." + ) % share_data['share_network_id'] + raise exception.InvalidInput(reason=msg) + # Check if share network is active, otherwise raise a BadRequest + api_common.check_share_network_is_active(share_network) + share_data.update({ 'user_id': context.user_id, 'project_id': context.project_id, @@ -2694,3 +2732,338 @@ class API(base.Base): 'task_state': dest_share_server['task_state'] }) return result + + def _share_network_update_initial_checks(self, context, share_network, + new_security_service, + current_security_service=None): + api_common.check_share_network_is_active(share_network) + + if not current_security_service: + # Since we are adding a new security service, we can't have one + # of the same type already associated with this share network + for attached_service in share_network['security_services']: + if attached_service['type'] == new_security_service['type']: + msg = _("Cannot add security service to share network. " + "Security service with '%(ss_type)s' type already " + "added to '%(sn_id)s' share network") % { + 'ss_type': new_security_service['type'], + 'sn_id': share_network['id'] + } + raise exception.InvalidSecurityService(reason=msg) + else: + # Validations needed only for update operation + current_service_is_associated = ( + self.db.share_network_security_service_association_get( + context, share_network['id'], + current_security_service['id'])) + + if not current_service_is_associated: + msg = _("The specified current security service %(service)s " + "is not associated to the share network %(network)s." + ) % { + 'service': current_security_service['id'], + 'network': share_network['id'] + } + raise exception.InvalidSecurityService(reason=msg) + + if (current_security_service['type'] != + new_security_service['type']): + msg = _("A security service can only be replaced by one of " + "the same type. The current security service type is " + "'%(ss_type)s' and the new security service type is " + "'%(new_ss_type)s'") % { + 'ss_type': current_security_service['type'], + 'new_ss_type': new_security_service['type'], + 'sn_id': share_network['id'] + } + raise exception.InvalidSecurityService(reason=msg) + + share_servers = set() + for subnet in share_network['share_network_subnets']: + if subnet['share_servers']: + share_servers.update(subnet['share_servers']) + + backend_hosts = set() + if share_servers: + if not share_network['security_service_update_support']: + msg = _("Updating security services is not supported on this " + "share network (%(sn_id)s) while it has shares. " + "See the capability " + "'security_service_update_support'.") % { + "sn_id": share_network["id"] + } + raise exception.InvalidShareNetwork(reason=msg) + + # We can only handle "active" share servers for now + for share_server in share_servers: + if share_server['status'] != constants.STATUS_ACTIVE: + msg = _('Some resources exported on share network ' + '%(shar_net_id)s are not currently available.') % { + 'shar_net_id': share_network['id'] + } + raise exception.InvalidShareNetwork(reason=msg) + # Create a set of backend hosts + backend_hosts.add(share_server['host']) + + for backend_host in backend_hosts: + # We need an admin context to validate these hosts + admin_ctx = manila_context.get_admin_context() + # Make sure the host is in the list of available hosts + utils.validate_service_host(admin_ctx, backend_host) + + shares = self.get_all( + context, search_opts={'share_network_id': share_network['id']}) + shares_not_available = [ + share['id'] for share in shares if + share['status'] != constants.STATUS_AVAILABLE] + + if shares_not_available: + msg = _("Some shares exported on share network %(sn_id)s are " + "not available: %(share_ids)s.") % { + 'sn_id': share_network['id'], + 'share_ids': shares_not_available, + } + raise exception.InvalidShareNetwork(reason=msg) + + shares_rules_not_available = [ + share['id'] for share in shares if + share['instance'][ + 'access_rules_status'] != constants.STATUS_ACTIVE] + + if shares_rules_not_available: + msg = _( + "Either these shares or one of their replicas or " + "migration copies exported on share network %(sn_id)s " + "are not available: %(share_ids)s.") % { + 'sn_id': share_network['id'], + 'share_ids': shares_rules_not_available, + } + raise exception.InvalidShareNetwork(reason=msg) + + busy_shares = [] + for share in shares: + try: + self._check_is_share_busy(share) + except exception.ShareBusyException: + busy_shares.append(share['id']) + if busy_shares: + msg = _("Some shares exported on share network %(sn_id)s " + "are busy: %(share_ids)s.") % { + 'sn_id': share_network['id'], + 'share_ids': busy_shares, + } + raise exception.InvalidShareNetwork(reason=msg) + + return list(share_servers), list(backend_hosts) + + def get_security_service_update_key( + self, operation, new_security_service_id, + current_security_service_id=None): + if current_security_service_id: + return ('share_network_sec_service_update_' + + current_security_service_id + '_' + + new_security_service_id + '_' + operation) + else: + return ('share_network_sec_service_add_' + + new_security_service_id + '_' + operation) + + @locked_security_service_update_operation + def _security_service_update_validate_hosts( + self, context, share_network, + backend_hosts, share_servers, + new_security_service_id=None, + current_security_service_id=None): + + # create a key based on users request + update_key = self.get_security_service_update_key( + 'hosts_check', new_security_service_id, + current_security_service_id=current_security_service_id) + + # check if there is an entry being processed + update_value = self.db.async_operation_data_get( + context, share_network['id'], update_key) + if not update_value: + # Create a new entry, send all asynchronous rpcs and return + hosts_to_validate = {} + for host in backend_hosts: + hosts_to_validate[host] = None + self.db.async_operation_data_update( + context, share_network['id'], + {update_key: json.dumps(hosts_to_validate)}) + for host in backend_hosts: + (self.share_rpcapi. + check_update_share_network_security_service( + context, host, share_network['id'], + new_security_service_id, + current_security_service_id=( + current_security_service_id))) + return None, hosts_to_validate + + else: + # process current existing hosts and update them if needed + current_hosts = json.loads(update_value) + hosts_to_include = ( + set(backend_hosts).difference(set(current_hosts.keys()))) + hosts_to_validate = {} + for host in backend_hosts: + hosts_to_validate[host] = current_hosts.get(host, None) + + # Check if there is any unsupported host + if any(hosts_to_validate[host] is False for host in backend_hosts): + return False, hosts_to_validate + + # Update the list of hosts to be validated + if hosts_to_include: + self.db.async_operation_data_update( + context, share_network['id'], + {update_key: json.dumps(hosts_to_validate)}) + + for host in hosts_to_include: + # send asynchronous check only for new backend hosts + (self.share_rpcapi. + check_update_share_network_security_service( + context, host, share_network['id'], + new_security_service_id, + current_security_service_id=( + current_security_service_id))) + + return None, hosts_to_validate + + if all(hosts_to_validate[host] for host in backend_hosts): + return True, hosts_to_validate + + return None, current_hosts + + def check_share_network_security_service_update( + self, context, share_network, new_security_service, + current_security_service=None, reset_operation=False): + share_servers, backend_hosts = ( + self._share_network_update_initial_checks( + context, share_network, new_security_service, + current_security_service=current_security_service)) + + if not backend_hosts: + # There is no backend host to validate. Operation is supported. + return { + 'compatible': True, + 'hosts_check_result': {}, + } + curr_sec_serv_id = ( + current_security_service['id'] + if current_security_service else None) + + key = self.get_security_service_update_key( + 'hosts_check', new_security_service['id'], + current_security_service_id=curr_sec_serv_id) + if reset_operation: + self.db.async_operation_data_delete(context, share_network['id'], + key) + try: + compatible, hosts_info = ( + self._security_service_update_validate_hosts( + context, share_network, backend_hosts, share_servers, + new_security_service_id=new_security_service['id'], + current_security_service_id=curr_sec_serv_id)) + except Exception as e: + LOG.error(e) + # Due to an internal error, we will delete the entry + self.db.async_operation_data_delete( + context, share_network['id'], key) + msg = _( + 'The share network %(share_net_id)s cannot be updated ' + 'since at least one of its backend hosts do not support ' + 'this operation.') % { + 'share_net_id': share_network['id']} + raise exception.InvalidShareNetwork(reason=msg) + + return { + 'compatible': compatible, + 'hosts_check_result': hosts_info + } + + def update_share_network_security_service(self, context, share_network, + new_security_service, + current_security_service=None): + share_servers, backend_hosts = ( + self._share_network_update_initial_checks( + context, share_network, new_security_service, + current_security_service=current_security_service)) + if not backend_hosts: + # There is no backend host to validate or update. + return + + curr_sec_serv_id = ( + current_security_service['id'] + if current_security_service else None) + + update_key = self.get_security_service_update_key( + 'hosts_check', new_security_service['id'], + current_security_service_id=curr_sec_serv_id) + # check if there is an entry being processed at this moment + update_value = self.db.async_operation_data_get( + context, share_network['id'], update_key) + if not update_value: + msg = _( + 'The share network %(share_net_id)s cannot start the update ' + 'process since no check operation was found. Before starting ' + 'the update operation, a "check" operation must be triggered ' + 'to validate if all backend hosts support the provided ' + 'configuration paramaters.') % { + 'share_net_id': share_network['id'] + } + raise exception.InvalidShareNetwork(reason=msg) + + try: + result, __ = self._security_service_update_validate_hosts( + context, share_network, backend_hosts, share_servers, + new_security_service_id=new_security_service['id'], + current_security_service_id=curr_sec_serv_id) + except Exception: + # Due to an internal error, we will delete the entry + self.db.async_operation_data_delete( + context, share_network['id'], update_key) + msg = _( + 'The share network %(share_net_id)s cannot be updated ' + 'since at least one of its backend hosts do not support ' + 'this operation.') % { + 'share_net_id': share_network['id']} + raise exception.InvalidShareNetwork(reason=msg) + + if result is False: + msg = _( + 'The share network %(share_net_id)s cannot be updated ' + 'since at least one of its backend hosts do not support ' + 'this operation.') % { + 'share_net_id': share_network['id']} + raise exception.InvalidShareNetwork(reason=msg) + elif result is None: + msg = _( + 'Not all of the validation has been completed yet. A ' + 'validation check is in progress. This operation can be ' + 'retried.') + raise exception.InvalidShareNetwork(reason=msg) + + self.db.share_network_update( + context, share_network['id'], + {'status': constants.STATUS_NETWORK_CHANGE}) + + # NOTE(dviroel): We want to change the status for all share servers to + # identify when all modifications are made, and update share network + # status to 'active' again. + share_servers_ids = [ss.id for ss in share_servers] + self.db.share_servers_update( + context, share_servers_ids, + {'status': constants.STATUS_SERVER_NETWORK_CHANGE}) + + for backend_host in backend_hosts: + self.share_rpcapi.update_share_network_security_service( + context, backend_host, share_network['id'], + new_security_service['id'], + current_security_service_id=curr_sec_serv_id) + + # Erase db entry, since we won't need it anymore + self.db.async_operation_data_delete( + context, share_network['id'], update_key) + + LOG.info('Security service update has been started for share network ' + '%(share_net_id)s.', {'share_net_id': share_network['id']}) diff --git a/manila/share/driver.py b/manila/share/driver.py index 266cd21062..6f9cd751a5 100644 --- a/manila/share/driver.py +++ b/manila/share/driver.py @@ -273,6 +273,10 @@ class ShareDriver(object): self._stats = {} self.ip_versions = None self.ipv6_implemented = False + # Indicates whether a driver supports update of security services for + # in-use share networks. This property will be saved in every new share + # server. + self.security_service_update_support = False self.pools = [] if self.configuration: @@ -1315,6 +1319,8 @@ class ShareDriver(object): replication_domain=self.replication_domain, filter_function=self.get_filter_function(), goodness_function=self.get_goodness_function(), + security_service_update_support=( + self.security_service_update_support), ) if isinstance(data, dict): common.update(data) @@ -3184,3 +3190,134 @@ class ShareDriver(object): """ raise NotImplementedError() + + def update_share_server_security_service( + self, context, share_server, network_info, share_instances, + share_instance_rules, new_security_service, + current_security_service=None): + """Updates share server security service configuration. + + If the driver supports different security services, the user can + request the addition of a new security service, with a different type. + If the user wants to update the current security service configuration, + the driver will receive both current and new security services, which + will always be of the same type. + + :param context: The 'context.RequestContext' object for the request. + :param share_server: Reference to the share server object that will be + updated. + :param network_info: All network allocation associated with the share + server that will be updated. + :param share_instances: A list of share instances that belong to the + share server that is being updated. + :param share_instance_rules: A list of access rules, grouped by share + instance, in the following format. + + Example:: + + [ + { + 'share_instance_id': '3bc10d67-2598-4122-bb62-0bdeaa8c6db3', + 'access_rules': + [ + { + 'access_id':'906d0094-3e34-4d6c-a184-d08a908033e3', + 'access_type':'ip', + 'access_key':None, + 'access_to':'10.0.0.1', + 'access_level':'rw' + ... + }, + ], + }, + ] + + :param new_security_service: New security service object to be + configured in the share server. + :param current_security_service: When provided, represents the current + security service that will be replaced by the + 'new_security_service'. + + :raises: ShareBackendException. + A ShareBackendException should only be raised if the share server + failed to update the security service, compromising all its access + rules. By raising an exception, the share server and all its share + instances will be set to 'error'. + :return: None, or a dictionary of updates in the following format. + + Example:: + + { + '3bc10d67-2598-4122-bb62-0bdeaa8c6db3': + { + '09960614-8574-4e03-89cf-7cf267b0bd08': + { + 'access_key': 'alice31493e5441b8171d2310d80e37e', + 'state': 'error', + }, + '28f6eabb-4342-486a-a7f4-45688f0c0295': + { + 'access_key': 'bob0078aa042d5a7325480fd13228b', + 'state': 'active', + }, + }, + } + + The top level keys are share_instance_id's which should provide + another dictionary of access rules to be updated, indexed by their + 'access_id'. The inner access rules dictionary should only contain the + access rules that need to be updated. + """ + raise NotImplementedError() + + def check_update_share_server_security_service( + self, context, share_server, network_info, share_instances, + share_instance_rules, new_security_service, + current_security_service=None): + """Check if the current share server security service is supported. + + If the driver supports different security services, the user can + request the addition of a new security service, with a different type. + If the user wants to update the current security service configuration, + the driver will receive both current and new security services, which + will always be of the same type. + + :param context: The 'context.RequestContext' object for the request. + :param share_server: Reference to the share server object that will be + updated. + :param network_info: All network allocation associated with the share + server that will be updated. + :param share_instances: A list of share instances that belong to the + share server that is affected by the update. + :param share_instance_rules: A list of access rules, grouped by share + instance, in the following format. + + Example:: + + [ + { + 'share_instance_id': '3bc10d67-2598-4122-bb62-0bdeaa8c6db3', + 'access_rules': + [ + { + 'access_id':'906d0094-3e34-4d6c-a184-d08a908033e3', + 'access_type':'ip', + 'access_key':None, + 'access_to':'10.0.0.1', + 'access_level':'rw' + ... + }, + ], + }, + ] + + :param new_security_service: New security service object to be + configured in the share server. + :param current_security_service: When provided, represents the current + security service that will be replaced by the + 'new_security_service'. + + :return: 'True' if the driver support the requested update, 'False' + otherwise. + """ + raise NotImplementedError() diff --git a/manila/share/manager.py b/manila/share/manager.py index 7169ad5471..299717ae2b 100644 --- a/manila/share/manager.py +++ b/manila/share/manager.py @@ -23,6 +23,7 @@ import copy import datetime import functools import hashlib +import json from operator import xor from oslo_config import cfg @@ -183,6 +184,25 @@ def locked_share_replica_operation(operation): return wrapped +def locked_share_network_operation(operation): + """Lock decorator for share network operations. + + Takes a named lock prior to executing the operation. The lock is named with + the id of the share network. + """ + + def wrapped(*args, **kwargs): + share_network_id = kwargs.get('share_network_id') + + @coordination.synchronized( + 'locked-share-network-operation-%s' % share_network_id) + def locked_network_operation(*_args, **_kwargs): + return operation(*_args, **_kwargs) + return locked_network_operation(*args, **kwargs) + + return wrapped + + def add_hooks(f): """Hook decorator to perform action before and after a share method call @@ -218,7 +238,7 @@ def add_hooks(f): class ShareManager(manager.SchedulerDependentManager): """Manages NAS storages.""" - RPC_API_VERSION = '1.21' + RPC_API_VERSION = '1.22' def __init__(self, share_driver=None, service_name=None, *args, **kwargs): """Load the driver from args, or from flags.""" @@ -698,6 +718,8 @@ class ShareManager(manager.SchedulerDependentManager): 'host': self.host, 'share_network_subnet_id': share_network_subnet_id, 'status': constants.STATUS_CREATING, + 'security_service_update_support': ( + self.driver.security_service_update_support), } ) @@ -785,6 +807,8 @@ class ShareManager(manager.SchedulerDependentManager): 'host': self.host, 'share_network_subnet_id': share_network_subnet_id, 'status': constants.STATUS_CREATING, + 'security_service_update_support': ( + self.driver.security_service_update_support), } ) @@ -962,7 +986,9 @@ class ShareManager(manager.SchedulerDependentManager): { 'host': self.host, 'share_network_subnet_id': share_network_subnet_id, - 'status': constants.STATUS_CREATING + 'status': constants.STATUS_CREATING, + 'security_service_update_support': ( + self.driver.security_service_update_support), } ) @@ -2046,11 +2072,11 @@ class ShareManager(manager.SchedulerDependentManager): self._notify_about_share_usage(context, share, share_instance, "create.end") - def _update_share_replica_access_rules_state(self, context, - share_replica_id, state): - """Update the access_rules_status for the share replica.""" + def _update_share_instance_access_rules_state(self, context, + share_instance_id, state): + """Update the access_rules_status for the share instance.""" self.access_helper.get_and_update_share_instance_access_rules_status( - context, status=state, share_instance_id=share_replica_id) + context, status=state, share_instance_id=share_instance_id) def _get_replica_snapshots_for_snapshot(self, context, snapshot_id, active_replica_id, @@ -2208,7 +2234,7 @@ class ShareManager(manager.SchedulerDependentManager): context, share_replica['id'], {'status': constants.STATUS_ERROR, 'replica_state': constants.STATUS_ERROR}) - self._update_share_replica_access_rules_state( + self._update_share_instance_access_rules_state( context, share_replica['id'], constants.STATUS_ERROR) self.message_api.create( context, @@ -2236,11 +2262,11 @@ class ShareManager(manager.SchedulerDependentManager): 'progress': '100%'}) if replica_ref.get('access_rules_status'): - self._update_share_replica_access_rules_state( + self._update_share_instance_access_rules_state( context, share_replica['id'], replica_ref.get('access_rules_status')) else: - self._update_share_replica_access_rules_state( + self._update_share_instance_access_rules_state( context, share_replica['id'], constants.STATUS_ACTIVE) @@ -2497,7 +2523,7 @@ class ShareManager(manager.SchedulerDependentManager): context, updated_replica['id'], updates) if updated_replica.get('access_rules_status'): - self._update_share_replica_access_rules_state( + self._update_share_instance_access_rules_state( context, share_replica['id'], updated_replica.get('access_rules_status')) @@ -3861,6 +3887,7 @@ class ShareManager(manager.SchedulerDependentManager): def _report_driver_status(self, context): LOG.info('Updating share status') share_stats = self.driver.get_share_stats(refresh=True) + if not share_stats: return @@ -4629,7 +4656,9 @@ class ShareManager(manager.SchedulerDependentManager): 'is_auto_deletable': share_server.get('is_auto_deletable', None), 'identifier': share_server.get('identifier', None), 'network_allocations': share_server.get('network_allocations', - None) + None), + 'share_network_status': share_server.get( + 'share_network_status', None) } return share_server_ref @@ -4680,6 +4709,7 @@ class ShareManager(manager.SchedulerDependentManager): 'source_share_group_snapshot_member_id': share_instance.get( 'source_share_group_snapshot_member_id'), 'availability_zone': share_instance.get('availability_zone'), + 'share_network_status': share_instance.get('share_network_status') } if share_instance_ref['share_server']: share_instance_ref['share_server'] = self._get_share_server_dict( @@ -5486,3 +5516,204 @@ class ShareManager(manager.SchedulerDependentManager): return self.driver.share_server_migration_get_progress( context, src_share_server, dest_share_server, share_instances, snapshot_instances) + + @locked_share_network_operation + def _check_share_network_update_finished(self, context, share_network_id): + # Check if this share network is already active + share_network = self.db.share_network_get(context, share_network_id) + if share_network['status'] == constants.STATUS_NETWORK_ACTIVE: + return + + share_servers = self.db.share_server_get_all_with_filters( + context, {'share_network_id': share_network_id} + ) + + if all([ss['status'] != constants.STATUS_SERVER_NETWORK_CHANGE + for ss in share_servers]): + # All share servers have updated their configuration + self.db.share_network_update( + context, share_network_id, + {'status': constants.STATUS_NETWORK_ACTIVE}) + + def _update_share_network_security_service( + self, context, share_network_id, new_security_service_id, + current_security_service_id=None, check_only=False): + + new_security_service = self.db.security_service_get( + context, new_security_service_id) + + current_security_service = None + if current_security_service_id: + current_security_service = self.db.security_service_get( + context, current_security_service_id) + + new_ss_type = new_security_service['type'] + backend_details_data = { + 'name': new_security_service['name'], + 'ou': new_security_service['ou'], + 'domain': new_security_service['domain'], + 'server': new_security_service['server'], + 'dns_ip': new_security_service['dns_ip'], + 'user': new_security_service['user'], + 'type': new_ss_type, + 'password': new_security_service['password'], + } + + share_network = self.db.share_network_get( + context, share_network_id) + + share_servers = self.db.share_server_get_all_by_host( + context, self.host, + filters={'share_network_id': share_network_id}) + + for share_server in share_servers: + share_network_subnet = share_server['share_network_subnet'] + share_network_subnet_id = share_network_subnet['id'] + + # Get share_network_subnet in case it was updated. + share_network_subnet = self.db.share_network_subnet_get( + context, share_network_subnet_id) + network_info = self._form_server_setup_info( + context, share_server, share_network, share_network_subnet) + + share_instances = ( + self.db.share_instances_get_all_by_share_server( + context, share_server['id'], with_share_data=True)) + share_instance_ids = [sn.id for sn in share_instances] + + share_instances_rules = [] + for share_instance_id in share_instance_ids: + instance_rules = { + 'share_instance_id': share_instance_id, + 'access_rules': ( + self.db.share_access_get_all_for_instance( + context, share_instance_id)) + } + share_instances_rules.append(instance_rules) + + # Only check if the driver supports this kind of update. + if check_only: + if self.driver.check_update_share_server_security_service( + context, share_server, network_info, + share_instances, share_instances_rules, + new_security_service, + current_security_service=current_security_service): + # Check the next share server. + continue + else: + # At least one share server doesn't support this update + return False + + # NOTE(dviroel): We always do backend details update since it + # should be the expected configuration for this share server. Any + # issue with this operation should be fixed by the admin which will + # guarantee that storage and backend_details configurations match. + self.db.share_server_backend_details_set( + context, share_server['id'], + {'security_service_' + new_ss_type: jsonutils.dumps( + backend_details_data)}) + try: + updates = self.driver.update_share_server_security_service( + context, share_server, network_info, + share_instances, share_instances_rules, + new_security_service, + current_security_service=current_security_service) or {} + except Exception: + operation = 'add' + sec_serv_info = ('new security service %s' + % new_security_service_id) + if current_security_service_id: + operation = 'update' + sec_serv_info = ('current security service %s and ' + % current_security_service_id + + sec_serv_info) + msg = _("Share server %(server_id)s has failed on security " + "service %(operation)s operation for " + "%(sec_serv_ids)s.") % { + 'server_id': share_server['id'], + 'operation': operation, + 'sec_serv_ids': sec_serv_info, + } + LOG.exception(msg) + # Set share server to error. Security service configuration + # must be fixed before restoring it to active again. + self.db.share_server_update( + context, share_server['id'], + {'status': constants.STATUS_ERROR}) + + if current_security_service: + # NOTE(dviroel): An already configured security service has + # failed on update operation. We will set all share + # instances to 'error'. + if share_instance_ids: + self.db.share_instances_status_update( + context, share_instance_ids, + {'status': constants.STATUS_ERROR}) + # Update share instance access rules status + (self.access_helper + .update_share_instances_access_rules_status( + context, constants.SHARE_INSTANCE_RULES_ERROR, + share_instance_ids)) + # Go to the next share server + continue + + # Update access rules based on drivers updates + for instance_id, rules_updates in updates.items(): + self.access_helper.process_driver_rule_updates( + context, rules_updates, instance_id) + + msg = _("Security service was successfully updated on share " + "server %s.") % share_server['id'] + LOG.info(msg) + self.db.share_server_update( + context, share_server['id'], + {'status': constants.STATUS_ACTIVE}) + + if check_only: + # All share servers support the requested update + return True + + # Check if all share servers have already finished their updates in + # order to properly update share network status + self._check_share_network_update_finished(context, share_network['id']) + + def update_share_network_security_service( + self, context, share_network_id, new_security_service_id, + current_security_service_id=None): + self._update_share_network_security_service( + context, share_network_id, new_security_service_id, + current_security_service_id=current_security_service_id, + check_only=False) + + def check_update_share_network_security_service( + self, context, share_network_id, new_security_service_id, + current_security_service_id=None): + is_supported = self._update_share_network_security_service( + context, share_network_id, new_security_service_id, + current_security_service_id=current_security_service_id, + check_only=True) + self._update_share_network_security_service_operations( + context, share_network_id, is_supported, + new_security_service_id=new_security_service_id, + current_security_service_id=current_security_service_id) + + @api.locked_security_service_update_operation + def _update_share_network_security_service_operations( + self, context, share_network_id, is_supported, + new_security_service_id=None, + current_security_service_id=None): + update_check_key = self.share_api.get_security_service_update_key( + 'hosts_check', new_security_service_id, + current_security_service_id) + current_hosts_info = self.db.async_operation_data_get( + context, share_network_id, update_check_key) + if current_hosts_info: + current_hosts = json.loads(current_hosts_info) + current_hosts[self.host] = is_supported + self.db.async_operation_data_update( + context, share_network_id, + {update_check_key: json.dumps(current_hosts)}) + else: + LOG.debug('A share network security service check was requested ' + 'but no entries were found in database. Ignoring call ' + 'and returning.') diff --git a/manila/share/rpcapi.py b/manila/share/rpcapi.py index 7d150d1b0b..8170e66232 100644 --- a/manila/share/rpcapi.py +++ b/manila/share/rpcapi.py @@ -79,6 +79,8 @@ class ShareAPI(object): 1.20 - Add share_instance_id parameter for create_share_server() method 1.21 - Add share_server_migration_start, share_server_migration_check() and share_server_get_progress() + 1.22 - Add update_share_network_security_service() and + check_update_share_network_security_service() """ BASE_RPC_API_VERSION = '1.0' @@ -87,7 +89,7 @@ class ShareAPI(object): super(ShareAPI, self).__init__() target = messaging.Target(topic=CONF.share_topic, version=self.BASE_RPC_API_VERSION) - self.client = rpc.get_client(target, version_cap='1.21') + self.client = rpc.get_client(target, version_cap='1.22') def create_share_instance(self, context, share_instance, host, request_spec, filter_properties, @@ -436,3 +438,27 @@ class ShareAPI(object): call_context.cast(context, 'snapshot_update_access', snapshot_instance_id=snapshot_instance['id']) + + def update_share_network_security_service( + self, context, dest_host, share_network_id, + new_security_service_id, current_security_service_id=None): + host = utils.extract_host(dest_host) + call_context = self.client.prepare(server=host, version='1.22') + call_context.cast( + context, + 'update_share_network_security_service', + share_network_id=share_network_id, + new_security_service_id=new_security_service_id, + current_security_service_id=current_security_service_id) + + def check_update_share_network_security_service( + self, context, dest_host, share_network_id, + new_security_service_id, current_security_service_id=None): + host = utils.extract_host(dest_host) + call_context = self.client.prepare(server=host, version='1.22') + call_context.cast( + context, + 'check_update_share_network_security_service', + share_network_id=share_network_id, + new_security_service_id=new_security_service_id, + current_security_service_id=current_security_service_id) diff --git a/manila/share_group/api.py b/manila/share_group/api.py index 1e194be833..0ac608a021 100644 --- a/manila/share_group/api.py +++ b/manila/share_group/api.py @@ -23,6 +23,7 @@ from oslo_utils import excutils from oslo_utils import strutils import six +from manila.api import common as api_common from manila.common import constants from manila.db import base from manila import exception @@ -109,13 +110,19 @@ class API(base.Base): "False, a share_network_id must not be provided.") raise exception.InvalidInput(reason=msg) + share_network = {} try: if share_network_id: - self.db.share_network_get(context, share_network_id) + share_network = self.db.share_network_get( + context, share_network_id) except exception.ShareNetworkNotFound: msg = _("The specified share network does not exist.") raise exception.InvalidInput(reason=msg) + if share_network: + # Check if share network is active, otherwise raise a BadRequest + api_common.check_share_network_is_active(share_network) + if (driver_handles_share_servers and not (source_share_group_snapshot_id or share_network_id)): msg = _("When using a share type with the " diff --git a/manila/tests/api/v1/test_share_servers.py b/manila/tests/api/v1/test_share_servers.py index 9c2096f5b5..06e2612420 100644 --- a/manila/tests/api/v1/test_share_servers.py +++ b/manila/tests/api/v1/test_share_servers.py @@ -42,7 +42,8 @@ fake_share_server_list = { 'is_auto_deletable': False, 'task_state': None, 'source_share_server_id': None, - 'identifier': 'fake_id' + 'identifier': 'fake_id', + 'security_service_update_support': False }, { 'status': constants.STATUS_ERROR, @@ -56,7 +57,9 @@ fake_share_server_list = { 'is_auto_deletable': True, 'task_state': None, 'source_share_server_id': None, - 'identifier': 'fake_id_2' + 'identifier': 'fake_id_2', + 'security_service_update_support': False + }, ] } @@ -94,7 +97,8 @@ fake_share_server_get_result = { 'is_auto_deletable': False, 'task_state': None, 'source_share_server_id': None, - 'identifier': 'fake_id' + 'identifier': 'fake_id', + 'security_service_update_support': False } } @@ -131,6 +135,8 @@ class FakeShareServer(object): self.task_state = kwargs.get('task_state') self.source_share_server_id = kwargs.get('source_share_server_id') self.backend_details = share_server_backend_details + self.security_service_update_support = kwargs.get( + 'security_service_update_support', False) def __getitem__(self, item): return getattr(self, item) @@ -148,7 +154,8 @@ def fake_share_server_get_all(): identifier='fake_id_2', task_state=None, is_auto_deletable=True, - status=constants.STATUS_ERROR) + status=constants.STATUS_ERROR, + security_service_update_support=False) ] return fake_share_servers diff --git a/manila/tests/api/v1/test_shares.py b/manila/tests/api/v1/test_shares.py index 803dbbef6e..9c13d4c7dd 100644 --- a/manila/tests/api/v1/test_shares.py +++ b/manila/tests/api/v1/test_shares.py @@ -228,6 +228,7 @@ class ShareAPITest(test.TestCase): "availability_zone": "zone1:host1", "share_network_id": "fakenetid" } + fake_network = {'id': 'fakenetid'} create_mock = mock.Mock(return_value=stubs.stub_share('1', display_name=shr['name'], display_description=shr['description'], @@ -237,7 +238,9 @@ class ShareAPITest(test.TestCase): share_network_id=shr['share_network_id'])) self.mock_object(share_api.API, 'create', create_mock) self.mock_object(share_api.API, 'get_share_network', mock.Mock( - return_value={'id': 'fakenetid'})) + return_value=fake_network)) + self.mock_object(common, 'check_share_network_is_active', + mock.Mock(return_value=True)) self.mock_object( db, 'share_network_subnet_get_by_availability_zone_id', mock.Mock(return_value={'id': 'fakesubnetid'})) @@ -250,11 +253,50 @@ class ShareAPITest(test.TestCase): req.environ['manila.context'], self.resource_name, 'create') expected = self._get_expected_share_detailed_response(shr) expected['share'].pop('snapshot_support') + common.check_share_network_is_active.assert_called_once_with( + fake_network) self.assertEqual(expected, res_dict) # pylint: disable=unsubscriptable-object self.assertEqual("fakenetid", create_mock.call_args[1]['share_network_id']) + def test_share_create_with_share_net_not_active(self): + shr = { + "size": 100, + "name": "Share Test Name", + "description": "Share Test Desc", + "share_proto": "fakeproto", + "availability_zone": "zone1:host1", + "share_network_id": "fakenetid" + } + share_network = db_utils.create_share_network( + status=constants.STATUS_NETWORK_CHANGE) + create_mock = mock.Mock(return_value=stubs.stub_share('1', + display_name=shr['name'], + display_description=shr['description'], + size=shr['size'], + share_proto=shr['share_proto'].upper(), + availability_zone=shr['availability_zone'], + share_network_id=shr['share_network_id'])) + self.mock_object(share_api.API, 'create', create_mock) + self.mock_object(share_api.API, 'get_share_network', mock.Mock( + return_value=share_network)) + self.mock_object(common, 'check_share_network_is_active', + mock.Mock(side_effect=webob.exc.HTTPBadRequest())) + + body = {"share": copy.deepcopy(shr)} + req = fakes.HTTPRequest.blank('/shares') + self.assertRaises( + webob.exc.HTTPBadRequest, + self.controller.create, + req, + body) + + self.mock_policy_check.assert_called_once_with( + req.environ['manila.context'], self.resource_name, 'create') + common.check_share_network_is_active.assert_called_once_with( + share_network) + def test_share_create_from_snapshot_without_share_net_no_parent(self): shr = { "size": 100, @@ -296,6 +338,7 @@ class ShareAPITest(test.TestCase): "share_network_id": None, } parent_share_net = 444 + fake_share_net = {'id': parent_share_net} create_mock = mock.Mock(return_value=stubs.stub_share('1', display_name=shr['name'], display_description=shr['description'], @@ -314,7 +357,9 @@ class ShareAPITest(test.TestCase): self.mock_object(share_api.API, 'get', mock.Mock( return_value=parent_share)) self.mock_object(share_api.API, 'get_share_network', mock.Mock( - return_value={'id': parent_share_net})) + return_value=fake_share_net)) + self.mock_object(common, 'check_share_network_is_active', + mock.Mock(return_value=True)) self.mock_object( db, 'share_network_subnet_get_by_availability_zone_id') @@ -327,6 +372,8 @@ class ShareAPITest(test.TestCase): req.environ['manila.context'], self.resource_name, 'create') expected = self._get_expected_share_detailed_response(shr) expected['share'].pop('snapshot_support') + common.check_share_network_is_active.assert_called_once_with( + fake_share_net) self.assertEqual(expected, res_dict) # pylint: disable=unsubscriptable-object self.assertEqual(parent_share_net, @@ -343,6 +390,7 @@ class ShareAPITest(test.TestCase): "snapshot_id": 333, "share_network_id": parent_share_net } + fake_share_net = {'id': parent_share_net} create_mock = mock.Mock(return_value=stubs.stub_share('1', display_name=shr['name'], display_description=shr['description'], @@ -355,13 +403,15 @@ class ShareAPITest(test.TestCase): self.mock_object(share_api.API, 'create', create_mock) self.mock_object(share_api.API, 'get_snapshot', stubs.stub_snapshot_get) + self.mock_object(common, 'check_share_network_is_active', + mock.Mock(return_value=True)) parent_share = stubs.stub_share( '1', instance={'share_network_id': parent_share_net}, create_share_from_snapshot_support=True) self.mock_object(share_api.API, 'get', mock.Mock( return_value=parent_share)) self.mock_object(share_api.API, 'get_share_network', mock.Mock( - return_value={'id': parent_share_net})) + return_value=fake_share_net)) self.mock_object( db, 'share_network_subnet_get_by_availability_zone_id') @@ -374,6 +424,8 @@ class ShareAPITest(test.TestCase): req.environ['manila.context'], self.resource_name, 'create') expected = self._get_expected_share_detailed_response(shr) expected['share'].pop('snapshot_support') + common.check_share_network_is_active.assert_called_once_with( + fake_share_net) self.assertEqual(expected, res_dict) # pylint: disable=unsubscriptable-object self.assertEqual(parent_share_net, @@ -415,6 +467,7 @@ class ShareAPITest(test.TestCase): "snapshot_id": 333, "share_network_id": parent_share_net } + fake_share_net = {'id': parent_share_net} create_mock = mock.Mock(return_value=stubs.stub_share('1', display_name=shr['name'], display_description=shr['description'], @@ -427,13 +480,15 @@ class ShareAPITest(test.TestCase): self.mock_object(share_api.API, 'create', create_mock) self.mock_object(share_api.API, 'get_snapshot', stubs.stub_snapshot_get) + self.mock_object(common, 'check_share_network_is_active', + mock.Mock(return_value=True)) parent_share = stubs.stub_share( '1', instance={'share_network_id': parent_share_net}, create_share_from_snapshot_support=False) self.mock_object(share_api.API, 'get', mock.Mock( return_value=parent_share)) self.mock_object(share_api.API, 'get_share_network', mock.Mock( - return_value={'id': parent_share_net})) + return_value=fake_share_net)) self.mock_object( db, 'share_network_subnet_get_by_availability_zone_id') @@ -446,6 +501,8 @@ class ShareAPITest(test.TestCase): req.environ['manila.context'], self.resource_name, 'create') expected = self._get_expected_share_detailed_response(shr) expected['share'].pop('snapshot_support') + common.check_share_network_is_active.assert_called_once_with( + fake_share_net) self.assertDictEqual(expected, res_dict) # pylint: disable=unsubscriptable-object self.assertEqual(parent_share_net, @@ -503,6 +560,7 @@ class ShareAPITest(test.TestCase): self.mock_object( db, 'share_network_subnet_get_by_availability_zone_id', mock.Mock(return_value=None)) + self.mock_object(common, 'check_share_network_is_active') body = {"share": fake_share_with_sn} @@ -903,6 +961,29 @@ class ShareActionsTest(test.TestCase): self.mock_policy_check.assert_called_once_with( req.environ['manila.context'], 'share', 'allow_access') + def test_allow_access_with_network_id(self): + share_network = db_utils.create_share_network() + share = db_utils.create_share(share_network_id=share_network['id']) + access = {'access_type': 'user', 'access_to': '1' * 4} + + self.mock_object(share_api.API, + 'allow_access', + mock.Mock(return_value={'fake': 'fake'})) + self.mock_object(self.controller._access_view_builder, 'view', + mock.Mock(return_value={'access': {'fake': 'fake'}})) + self.mock_object(share_api.API, 'get', mock.Mock(return_value=share)) + + id = 'fake_share_id' + body = {'os-allow_access': access} + expected = {'access': {'fake': 'fake'}} + req = fakes.HTTPRequest.blank('/v1/tenant1/shares/%s/action' % id) + + res = self.controller._allow_access(req, id, body) + + self.assertEqual(expected, res) + self.mock_policy_check.assert_called_once_with( + req.environ['manila.context'], 'share', 'allow_access') + @ddt.data( {'access_type': 'error_type', 'access_to': '127.0.0.1'}, {'access_type': 'ip', 'access_to': 'localhost'}, @@ -947,6 +1028,23 @@ class ShareActionsTest(test.TestCase): self.mock_policy_check.assert_called_once_with( req.environ['manila.context'], 'share', 'deny_access') + def test_deny_access_with_share_network_id(self): + self.mock_object(share_api.API, "deny_access", mock.Mock()) + self.mock_object(share_api.API, "access_get", _fake_access_get) + share_network = db_utils.create_share_network() + share = db_utils.create_share(share_network_id=share_network['id']) + self.mock_object(share_api.API, 'get', mock.Mock(return_value=share)) + + id = 'fake_share_id' + body = {"os-deny_access": {"access_id": 'fake_acces_id'}} + req = fakes.HTTPRequest.blank('/v1/tenant1/shares/%s/action' % id) + + res = self.controller._deny_access(req, id, body) + + self.assertEqual(202, res.status_int) + self.mock_policy_check.assert_called_once_with( + req.environ['manila.context'], 'share', 'deny_access') + def test_deny_access_not_found(self): def _stub_deny_access(*args, **kwargs): pass diff --git a/manila/tests/api/v2/test_share_networks.py b/manila/tests/api/v2/test_share_networks.py index 511ce23c10..fba42e20f1 100644 --- a/manila/tests/api/v2/test_share_networks.py +++ b/manila/tests/api/v2/test_share_networks.py @@ -28,8 +28,10 @@ from manila.api.v2 import share_networks from manila.db import api as db_api from manila import exception from manila import quota +from manila.share import api as share_api from manila import test from manila.tests.api import fakes +from manila.tests import db_utils fake_share_network_subnet = { @@ -54,7 +56,9 @@ fake_share_network = { 'name': 'fake name', 'description': 'fake description', 'security_services': [], - 'share_network_subnets': [] + 'share_network_subnets': [], + 'security_service_update_support': True, + 'status': 'active' } @@ -79,6 +83,8 @@ fake_sn_with_ss_shortened = { 'name': 'test-sn', } +ADD_UPDATE_SEC_SERVICE_VERSION = '2.63' + QUOTAS = quota.QUOTAS @@ -91,6 +97,7 @@ class ShareNetworkAPITest(test.TestCase): self.req = fakes.HTTPRequest.blank('/share-networks') self.body = {share_networks.RESOURCE_NAME: {'name': 'fake name'}} self.context = self.req.environ['manila.context'] + self.share_api = share_api.API() def _check_share_network_view_shortened(self, view, share_nw): self.assertEqual(share_nw['id'], view['id']) @@ -874,7 +881,7 @@ class ShareNetworkAPITest(test.TestCase): self.context, share_nw) @ddt.data(*set(("1.0", "2.25", "2.26", api_version._MAX_API_VERSION))) - def test_action_add_security_service(self, microversion): + def test_add_security_service(self, microversion): share_network_id = 'fake network id' security_service_id = 'fake ss id' self.mock_object( @@ -884,15 +891,123 @@ class ShareNetworkAPITest(test.TestCase): security_service_id}} req = fakes.HTTPRequest.blank('/share-networks', version=microversion) - with mock.patch.object(self.controller, '_add_security_service', + with mock.patch.object(self.controller, 'add_security_service', mock.Mock()): - self.controller.action(req, share_network_id, body) - self.controller._add_security_service.assert_called_once_with( - req, share_network_id, body['add_security_service']) + self.controller.add_security_service(req, share_network_id, body) + self.controller.add_security_service.assert_called_once_with( + req, share_network_id, body) - @mock.patch.object(db_api, 'share_network_get', mock.Mock()) - @mock.patch.object(db_api, 'security_service_get', mock.Mock()) - def test_action_add_security_service_conflict(self): + def _setup_add_sec_services_with_servers_tests( + self, share_network, security_service, network_is_active=True, + version=ADD_UPDATE_SEC_SERVICE_VERSION, + share_api_update_services_action=mock.Mock()): + self.mock_object( + db_api, 'share_network_get', mock.Mock(return_value=share_network)) + self.mock_object( + db_api, 'security_service_get', + mock.Mock(return_value=security_service)) + self.mock_object( + self.controller, '_share_network_subnets_contain_share_servers', + mock.Mock(return_value=True)) + self.mock_object( + self.controller.share_api, 'update_share_network_security_service', + share_api_update_services_action) + self.mock_object( + common, 'check_share_network_is_active', + mock.Mock(return_value=network_is_active)) + self.mock_object(db_api, 'share_network_add_security_service') + self.mock_object(self.controller._view_builder, 'build_share_network') + + body = { + 'add_security_service': { + 'security_service_id': security_service['id'] + } + } + req = fakes.HTTPRequest.blank( + '/add_security_service', version=version, use_admin_context=True) + context = req.environ['manila.context'] + + return req, context, body + + def test_add_security_service_with_servers(self): + security_service = db_utils.create_security_service() + security_service_id = security_service['id'] + share_network = db_utils.create_share_network() + share_network_id = share_network['id'] + req, context, body = self._setup_add_sec_services_with_servers_tests( + share_network, security_service) + + self.controller.add_security_service(req, share_network_id, body) + + db_api.security_service_get.assert_called_once_with( + context, security_service_id) + (self.controller._share_network_subnets_contain_share_servers. + assert_called_once_with(share_network)) + db_api.share_network_get.assert_called_once_with( + context, share_network_id) + (self.controller.share_api.update_share_network_security_service. + assert_called_once_with(context, share_network, security_service)) + + def test_add_security_service_with_server_invalid_version(self): + security_service = db_utils.create_security_service() + security_service_id = security_service['id'] + share_network = db_utils.create_share_network() + share_network_id = share_network['id'] + req, context, body = self._setup_add_sec_services_with_servers_tests( + share_network, security_service, version='2.59') + + self.assertRaises( + webob_exc.HTTPForbidden, + self.controller.add_security_service, + req, share_network_id, body + ) + + db_api.security_service_get.assert_called_once_with( + context, security_service_id) + (self.controller._share_network_subnets_contain_share_servers. + assert_called_once_with(share_network)) + db_api.share_network_get.assert_called_once_with( + context, share_network_id) + + @ddt.data( + (exception.ServiceIsDown(message='fake'), webob_exc.HTTPConflict), + (exception.InvalidShareNetwork(message='fake'), + webob_exc.HTTPBadRequest) + ) + @ddt.unpack + def test_add_security_service_with_server_update_failed( + self, side_effect, exception_to_raise): + security_service = db_utils.create_security_service() + security_service_id = security_service['id'] + share_network_id = fake_share_network['id'] + fake_share_network['security_service_update_support'] = True + action = mock.Mock(side_effect=side_effect) + + req, context, body = self._setup_add_sec_services_with_servers_tests( + fake_share_network, security_service, + share_api_update_services_action=action) + + self.assertRaises( + exception_to_raise, + self.controller.add_security_service, + req, share_network_id, body + ) + + db_api.security_service_get.assert_called_once_with( + context, security_service_id) + db_api.share_network_get.assert_called_once_with( + context, share_network_id) + (self.controller.share_api.update_share_network_security_service. + assert_called_once_with(context, fake_share_network, + security_service)) + + @ddt.data( + (exception.NotFound(message='fake'), webob_exc.HTTPNotFound), + (exception.ShareNetworkSecurityServiceAssociationError(message='fake'), + webob_exc.HTTPBadRequest)) + @ddt.unpack + def test_action_add_security_service_conflict(self, captured_exception, + expected_raised_exception): share_network = fake_share_network.copy() share_network['security_services'] = [{'id': 'security_service_1', 'type': 'ldap'}] @@ -900,28 +1015,148 @@ class ShareNetworkAPITest(test.TestCase): 'type': 'ldap'} body = {'add_security_service': {'security_service_id': security_service['id']}} + request = fakes.HTTPRequest.blank( + '/share-networks', use_admin_context=True) self.mock_object( self.controller, '_share_network_subnets_contain_share_servers', mock.Mock(return_value=False)) + update_sec_serv_mock = self.mock_object( + self.controller.share_api, 'update_share_network_security_service') + self.mock_object(db_api, 'share_network_get', + mock.Mock(return_value=share_network)) + self.mock_object(db_api, 'security_service_get', + mock.Mock(return_value=security_service)) + self.mock_object(share_networks.policy, 'check_policy') + self.mock_object( + db_api, 'share_network_add_security_service', + mock.Mock(side_effect=captured_exception)) db_api.security_service_get.return_value = security_service db_api.share_network_get.return_value = share_network - with mock.patch.object(share_networks.policy, 'check_policy', - mock.Mock()): - self.assertRaises(webob_exc.HTTPConflict, - self.controller.action, - self.req, - share_network['id'], - body) - db_api.share_network_get.assert_called_once_with( - self.req.environ['manila.context'], share_network['id']) - db_api.security_service_get.assert_called_once_with( - self.req.environ['manila.context'], security_service['id']) - share_networks.policy.check_policy.assert_called_once_with( - self.req.environ['manila.context'], - share_networks.RESOURCE_NAME, - 'add_security_service', - ) + self.assertRaises(expected_raised_exception, + self.controller.add_security_service, + request, + share_network['id'], + body) + + db_api.share_network_get.assert_called_once_with( + request.environ['manila.context'], share_network['id']) + db_api.security_service_get.assert_called_once_with( + request.environ['manila.context'], security_service['id']) + share_networks.policy.check_policy.assert_called_once_with( + request.environ['manila.context'], + share_networks.RESOURCE_NAME, + 'add_security_service', target_obj=share_network) + update_sec_serv_mock.assert_called_once_with( + request.environ['manila.context'], share_network, + security_service) + + def _setup_update_sec_services_with_servers_tests( + self, share_network, security_services, + version=ADD_UPDATE_SEC_SERVICE_VERSION, + share_api_update_services_action=mock.Mock()): + + self.mock_object( + db_api, 'share_network_get', mock.Mock(return_value=share_network)) + self.mock_object( + db_api, 'security_service_get', + mock.Mock(side_effect=security_services)) + self.mock_object( + self.controller.share_api, 'update_share_network_security_service', + share_api_update_services_action) + self.mock_object(self.controller._view_builder, 'build_share_network') + self.mock_object(db_api, 'share_network_update_security_service') + + body = { + 'update_security_service': { + 'current_service_id': security_services[0]['id'], + 'new_service_id': security_services[1]['id'] + } + } + req = fakes.HTTPRequest.blank( + '/add_security_service', version=version, use_admin_context=True) + context = req.environ['manila.context'] + + return req, context, body + + def test_update_security_service_service_not_found(self): + security_services = [ + db_utils.create_security_service() for i in range(2)] + share_network = copy.deepcopy(fake_share_network) + share_network['security_service_update_support'] = True + + req, context, body = ( + self._setup_update_sec_services_with_servers_tests( + share_network, security_services)) + + db_api.security_service_get.side_effect = exception.NotFound('fake') + + self.assertRaises( + webob_exc.HTTPBadRequest, + self.controller.update_security_service, + req, share_network['id'], body) + + db_api.share_network_get.assert_called_once_with( + context, share_network['id']) + db_api.security_service_get.assert_has_calls( + [mock.call(context, security_services[0]['id'])] + ) + + @ddt.data( + (exception.ServiceIsDown(message='fake'), webob_exc.HTTPConflict), + (exception.InvalidShareNetwork(message='fake'), + webob_exc.HTTPBadRequest)) + @ddt.unpack + def test_update_security_service_share_api_failure(self, side_effect, exc): + security_services = [ + db_utils.create_security_service() for i in range(2)] + share_network = copy.deepcopy(fake_share_network) + share_network['security_service_update_support'] = True + + req, context, body = ( + self._setup_update_sec_services_with_servers_tests( + share_network, security_services, + share_api_update_services_action=mock.Mock( + side_effect=side_effect))) + + self.assertRaises( + exc, + self.controller.update_security_service, + req, share_network['id'], body) + + db_api.share_network_get.assert_called_once_with( + context, share_network['id']) + db_api.security_service_get.assert_has_calls( + [mock.call(context, security_services[0]['id']), + mock.call(context, security_services[1]['id'])] + ) + + def test_update_security_service(self): + security_services = [ + db_utils.create_security_service() for i in range(2)] + share_network = copy.copy(fake_share_network) + share_network['security_service_update_support'] = True + + req, context, body = ( + self._setup_update_sec_services_with_servers_tests( + share_network, security_services)) + + self.controller.update_security_service( + req, share_network['id'], body) + + db_api.share_network_get.assert_called_once_with( + context, share_network['id']) + db_api.security_service_get.assert_has_calls( + [mock.call(context, security_services[0]['id']), + mock.call(context, security_services[1]['id'])] + ) + (self.controller.share_api.update_share_network_security_service. + assert_called_once_with( + context, share_network, security_services[1], + current_security_service=security_services[0])) + db_api.share_network_update_security_service.assert_called_once_with( + context, share_network['id'], security_services[0]['id'], + security_services[1]['id']) @ddt.data(*set(("1.0", "2.25", "2.26", api_version._MAX_API_VERSION))) def test_action_remove_security_service(self, microversion): @@ -933,11 +1168,12 @@ class ShareNetworkAPITest(test.TestCase): security_service_id}} req = fakes.HTTPRequest.blank('/share-networks', version=microversion) - with mock.patch.object(self.controller, '_remove_security_service', + with mock.patch.object(self.controller, 'remove_security_service', mock.Mock()): - self.controller.action(req, share_network_id, body) - self.controller._remove_security_service.assert_called_once_with( - req, share_network_id, body['remove_security_service']) + self.controller.remove_security_service( + req, share_network_id, body) + self.controller.remove_security_service.assert_called_once_with( + req, share_network_id, body) @mock.patch.object(db_api, 'share_network_get', mock.Mock()) @mock.patch.object(share_networks.policy, 'check_policy', mock.Mock()) @@ -956,7 +1192,7 @@ class ShareNetworkAPITest(test.TestCase): }, } self.assertRaises(webob_exc.HTTPForbidden, - self.controller.action, + self.controller.remove_security_service, self.req, share_network['id'], body) @@ -965,23 +1201,17 @@ class ShareNetworkAPITest(test.TestCase): share_networks.policy.check_policy.assert_called_once_with( self.req.environ['manila.context'], share_networks.RESOURCE_NAME, - 'remove_security_service') - - def test_action_bad_request(self): - share_network_id = 'fake network id' - body = {'bad_action': {}} - - self.assertRaises(webob_exc.HTTPBadRequest, - self.controller.action, - self.req, - share_network_id, - body) + 'remove_security_service', target_obj=share_network) @ddt.data('add_security_service', 'remove_security_service') def test_action_security_service_contains_share_servers(self, action): share_network = fake_share_network.copy() security_service = {'id': ' security_service_2', 'type': 'ldap'} + method_to_call = ( + self.controller.add_security_service + if action == 'add_security_service' + else self.controller.remove_security_service) body = { action: { 'security_service_id': security_service['id'] @@ -990,12 +1220,14 @@ class ShareNetworkAPITest(test.TestCase): self.mock_object(share_networks.policy, 'check_policy') self.mock_object(db_api, 'share_network_get', mock.Mock(return_value=share_network)) + self.mock_object(db_api, 'security_service_get', + mock.Mock(return_value=security_service)) self.mock_object( self.controller, '_share_network_subnets_contain_share_servers', mock.Mock(return_value=True)) self.assertRaises(webob_exc.HTTPForbidden, - self.controller.action, + method_to_call, self.req, share_network['id'], body) @@ -1003,4 +1235,228 @@ class ShareNetworkAPITest(test.TestCase): self.req.environ['manila.context'], share_network['id']) share_networks.policy.check_policy.assert_called_once_with( self.req.environ['manila.context'], - share_networks.RESOURCE_NAME, action) + share_networks.RESOURCE_NAME, action, target_obj=share_network) + + def _setup_data_for_check_update_tests(self): + security_services = [ + db_utils.create_security_service() for i in range(2)] + share_network = db_utils.create_share_network() + body = { + 'update_security_service_check': { + 'reset_operation': False, + 'current_service_id': security_services[0]['id'], + 'new_service_id': security_services[1]['id'], + } + } + request = fakes.HTTPRequest.blank( + '/share-networks', use_admin_context=True, version='2.63') + return security_services, share_network, body, request + + def test_check_update_security_service_not_found(self): + security_services, share_network, body, request = ( + self._setup_data_for_check_update_tests()) + + context = request.environ['manila.context'] + + self.mock_object(share_networks.policy, 'check_policy') + self.mock_object(db_api, 'share_network_get', + mock.Mock(return_value=share_network)) + self.mock_object(db_api, 'security_service_get', + mock.Mock(side_effect=exception.NotFound())) + + self.assertRaises( + webob_exc.HTTPBadRequest, + self.controller.check_update_security_service, + request, + share_network['id'], + body) + + db_api.share_network_get.assert_called_once_with( + context, share_network['id'] + ) + db_api.security_service_get.assert_called_once_with( + context, security_services[0]['id']) + + def test_check_update_security_service(self): + security_services, share_network, body, request = ( + self._setup_data_for_check_update_tests()) + context = request.environ['manila.context'] + share_api_return = {'fake_key': 'fake_value'} + + self.mock_object(share_networks.policy, 'check_policy') + self.mock_object(db_api, 'share_network_get', + mock.Mock(return_value=share_network)) + self.mock_object( + db_api, 'security_service_get', + mock.Mock( + side_effect=[security_services[0], security_services[1]])) + self.mock_object( + self.controller.share_api, + 'check_share_network_security_service_update', + mock.Mock(return_vale=share_api_return)) + self.mock_object( + self.controller._view_builder, + 'build_security_service_update_check') + + self.controller.check_update_security_service( + request, share_network['id'], body) + + db_api.share_network_get.assert_called_once_with( + context, share_network['id']) + db_api.security_service_get.assert_has_calls( + [mock.call(context, security_services[0]['id']), + mock.call(context, security_services[1]['id'])]) + (self.controller.share_api.check_share_network_security_service_update. + assert_called_once_with( + context, share_network, security_services[1], + current_security_service=security_services[0], + reset_operation=False)) + + @ddt.data( + (exception.ServiceIsDown(message='fake'), webob_exc.HTTPConflict), + (exception.InvalidShareNetwork(message='fake'), + webob_exc.HTTPBadRequest)) + @ddt.unpack + def test_check_update_security_service_share_api_failed( + self, captured_exception, exception_to_be_raised): + security_services, share_network, body, request = ( + self._setup_data_for_check_update_tests()) + context = request.environ['manila.context'] + + self.mock_object(share_networks.policy, 'check_policy') + self.mock_object(db_api, 'share_network_get', + mock.Mock(return_value=share_network)) + self.mock_object( + db_api, 'security_service_get', + mock.Mock( + side_effect=[security_services[0], security_services[1]])) + self.mock_object( + self.controller.share_api, + 'check_share_network_security_service_update', + mock.Mock(side_effect=captured_exception)) + + self.assertRaises( + exception_to_be_raised, + self.controller.check_update_security_service, + request, + share_network['id'], + body) + + db_api.share_network_get.assert_called_once_with( + context, share_network['id']) + db_api.security_service_get.assert_has_calls( + [mock.call(context, security_services[0]['id']), + mock.call(context, security_services[1]['id'])]) + (self.controller.share_api.check_share_network_security_service_update. + assert_called_once_with( + context, share_network, security_services[1], + current_security_service=security_services[0], + reset_operation=False)) + + def _setup_data_for_check_add_tests(self): + security_service = db_utils.create_security_service() + share_network = db_utils.create_share_network() + body = { + 'add_security_service_check': { + 'reset_operation': False, + 'security_service_id': security_service['id'], + } + } + request = fakes.HTTPRequest.blank( + '/share-networks', use_admin_context=True, version='2.63') + return security_service, share_network, body, request + + def test_check_add_security_service_not_found(self): + security_service, share_network, body, request = ( + self._setup_data_for_check_add_tests()) + + context = request.environ['manila.context'] + + self.mock_object(share_networks.policy, 'check_policy') + self.mock_object(db_api, 'share_network_get', + mock.Mock(return_value=share_network)) + self.mock_object(db_api, 'security_service_get', + mock.Mock(side_effect=exception.NotFound())) + + self.assertRaises( + webob_exc.HTTPBadRequest, + self.controller.check_add_security_service, + request, + share_network['id'], + body) + + db_api.share_network_get.assert_called_once_with( + context, share_network['id'] + ) + db_api.security_service_get.assert_called_once_with( + context, security_service['id']) + + def test_check_add_security_service(self): + security_service, share_network, body, request = ( + self._setup_data_for_check_add_tests()) + context = request.environ['manila.context'] + share_api_return = {'fake_key': 'fake_value'} + + self.mock_object(share_networks.policy, 'check_policy') + self.mock_object(db_api, 'share_network_get', + mock.Mock(return_value=share_network)) + self.mock_object( + db_api, 'security_service_get', + mock.Mock(return_value=security_service)) + self.mock_object( + self.controller.share_api, + 'check_share_network_security_service_update', + mock.Mock(return_vale=share_api_return)) + self.mock_object( + self.controller._view_builder, + 'build_security_service_update_check') + + self.controller.check_add_security_service( + request, share_network['id'], body) + + db_api.share_network_get.assert_called_once_with( + context, share_network['id']) + db_api.security_service_get.assert_called_once_with( + context, security_service['id']) + (self.controller.share_api.check_share_network_security_service_update. + assert_called_once_with( + context, share_network, security_service, + reset_operation=False)) + + @ddt.data( + (exception.ServiceIsDown(message='fake'), webob_exc.HTTPConflict), + (exception.InvalidShareNetwork(message='fake'), + webob_exc.HTTPBadRequest)) + @ddt.unpack + def test_check_add_security_service_share_api_failed( + self, captured_exception, exception_to_be_raised): + security_service, share_network, body, request = ( + self._setup_data_for_check_add_tests()) + context = request.environ['manila.context'] + + self.mock_object(share_networks.policy, 'check_policy') + self.mock_object(db_api, 'share_network_get', + mock.Mock(return_value=share_network)) + self.mock_object( + db_api, 'security_service_get', + mock.Mock(return_value=security_service)) + self.mock_object( + self.controller.share_api, + 'check_share_network_security_service_update', + mock.Mock(side_effect=captured_exception)) + + self.assertRaises( + exception_to_be_raised, + self.controller.check_add_security_service, + request, + share_network['id'], + body) + + db_api.share_network_get.assert_called_once_with( + context, share_network['id']) + db_api.security_service_get.assert_called_once_with( + context, security_service['id']) + (self.controller.share_api.check_share_network_security_service_update. + assert_called_once_with( + context, share_network, security_service, + reset_operation=False)) diff --git a/manila/tests/api/v2/test_share_replicas.py b/manila/tests/api/v2/test_share_replicas.py index 80acc4780c..98644d4710 100644 --- a/manila/tests/api/v2/test_share_replicas.py +++ b/manila/tests/api/v2/test_share_replicas.py @@ -15,11 +15,13 @@ from unittest import mock +import copy import ddt from oslo_config import cfg from oslo_serialization import jsonutils from webob import exc +from manila.api import common from manila.api.openstack import api_version_request as api_version from manila.api.v2 import share_replicas from manila.common import constants @@ -56,6 +58,17 @@ class ShareReplicasApiTest(test.TestCase): experimental=True, use_admin_context=True) self.admin_context = self.replicas_req_admin.environ['manila.context'] self.mock_policy_check = self.mock_object(policy, 'check_policy') + self.fake_share_network = { + 'id': 'fake network id', + 'project_id': 'fake project', + 'updated_at': None, + 'name': 'fake name', + 'description': 'fake description', + 'security_services': [], + 'share_network_subnets': [], + 'security_service_update_support': True, + 'status': 'active' + } def _get_context(self, role): return getattr(self, '%s_context' % role) @@ -370,6 +383,7 @@ class ShareReplicasApiTest(test.TestCase): mock__view_builder_call = self.mock_object( share_replicas.replication_view.ReplicationViewBuilder, 'detail_list') + share_network = db_utils.create_share_network() body = { 'share_replica': { 'share_id': 'FAKE_SHAREID', @@ -381,6 +395,10 @@ class ShareReplicasApiTest(test.TestCase): mock.Mock(return_value=fake_replica)) self.mock_object(share.API, 'create_share_replica', mock.Mock(side_effect=exception_type(**exc_args))) + self.mock_object(share_replicas.db, 'share_network_get', + mock.Mock(return_value=share_network)) + self.mock_object(common, 'check_share_network_is_active', + mock.Mock(return_value=True)) self.assertRaises(exc.HTTPBadRequest, self.controller.create, @@ -388,6 +406,10 @@ class ShareReplicasApiTest(test.TestCase): self.assertFalse(mock__view_builder_call.called) self.mock_policy_check.assert_called_once_with( self.member_context, self.resource_name, 'create') + share_replicas.db.share_network_get.assert_called_once_with( + self.member_context, fake_replica['share_network_id']) + common.check_share_network_is_active.assert_called_once_with( + share_network) @ddt.data((True, PRE_GRADUATION_VERSION), (False, GRADUATION_VERSION)) @ddt.unpack @@ -395,6 +417,7 @@ class ShareReplicasApiTest(test.TestCase): fake_replica, expected_replica = self._get_fake_replica( replication_type='writable', admin=is_admin, microversion=microversion) + share_network = db_utils.create_share_network() body = { 'share_replica': { 'share_id': 'FAKE_SHAREID', @@ -408,6 +431,10 @@ class ShareReplicasApiTest(test.TestCase): self.mock_object(share_replicas.db, 'share_replicas_get_available_active_replica', mock.Mock(return_value=[{'id': 'active1'}])) + self.mock_object(share_replicas.db, 'share_network_get', + mock.Mock(return_value=share_network)) + self.mock_object(common, 'check_share_network_is_active', + mock.Mock(return_value=True)) req = self._get_request(microversion, is_admin) req_context = req.environ['manila.context'] @@ -417,6 +444,10 @@ class ShareReplicasApiTest(test.TestCase): self.assertEqual(expected_replica, res_dict['share_replica']) self.mock_policy_check.assert_called_once_with( req_context, self.resource_name, 'create') + share_replicas.db.share_network_get.assert_called_once_with( + req_context, fake_replica['share_network_id']) + common.check_share_network_is_active.assert_called_once_with( + share_network) def test_delete_invalid_replica(self): fake_exception = exception.ShareReplicaNotFound( @@ -492,6 +523,8 @@ class ShareReplicasApiTest(test.TestCase): replica_state=constants.REPLICA_STATE_ACTIVE) self.mock_object(share_replicas.db, 'share_replica_get', mock.Mock(return_value=replica)) + self.mock_object(share_replicas.db, 'share_network_get', + mock.Mock(return_value=self.fake_share_network)) mock_api_promote_replica_call = self.mock_object( share.API, 'promote_share_replica') @@ -509,6 +542,8 @@ class ShareReplicasApiTest(test.TestCase): exception_type = exception.ReplicationException(reason='xyz') self.mock_object(share_replicas.db, 'share_replica_get', mock.Mock(return_value=replica)) + self.mock_object(share_replicas.db, 'share_network_get', + mock.Mock(return_value=self.fake_share_network)) mock_api_promote_replica_call = self.mock_object( share.API, 'promote_share_replica', mock.Mock(side_effect=exception_type)) @@ -522,12 +557,33 @@ class ShareReplicasApiTest(test.TestCase): self.mock_policy_check.assert_called_once_with( self.member_context, self.resource_name, 'promote') + def test_promote_share_network_not_active(self): + body = {'promote': None} + replica, expected_replica = self._get_fake_replica( + replica_state=constants.REPLICA_STATE_IN_SYNC) + fake_share_network = copy.deepcopy(self.fake_share_network) + fake_share_network['status'] = constants.STATUS_NETWORK_CHANGE + self.mock_object(share_replicas.db, 'share_replica_get', + mock.Mock(return_value=replica)) + self.mock_object(share_replicas.db, 'share_network_get', + mock.Mock(return_value=fake_share_network)) + + self.assertRaises(exc.HTTPBadRequest, + self.controller.promote, + self.replicas_req, + replica['id'], + body) + self.mock_policy_check.assert_called_once_with( + self.member_context, self.resource_name, 'promote') + def test_promote_admin_required_exception(self): body = {'promote': None} replica, expected_replica = self._get_fake_replica( replica_state=constants.REPLICA_STATE_IN_SYNC) self.mock_object(share_replicas.db, 'share_replica_get', mock.Mock(return_value=replica)) + self.mock_object(share_replicas.db, 'share_network_get', + mock.Mock(return_value=self.fake_share_network)) mock_api_promote_replica_call = self.mock_object( share.API, 'promote_share_replica', mock.Mock(side_effect=exception.AdminRequired)) @@ -549,6 +605,8 @@ class ShareReplicasApiTest(test.TestCase): microversion=microversion) self.mock_object(share_replicas.db, 'share_replica_get', mock.Mock(return_value=replica)) + self.mock_object(share_replicas.db, 'share_network_get', + mock.Mock(return_value=self.fake_share_network)) mock_api_promote_replica_call = self.mock_object( share.API, 'promote_share_replica', mock.Mock(return_value=replica)) diff --git a/manila/tests/api/v2/test_share_servers.py b/manila/tests/api/v2/test_share_servers.py index c068656c0a..89a90c6ce6 100644 --- a/manila/tests/api/v2/test_share_servers.py +++ b/manila/tests/api/v2/test_share_servers.py @@ -18,6 +18,7 @@ from unittest import mock import ddt import webob +from manila.api import common from manila.api.v2 import share_servers from manila.common import constants from manila import context as ctx_api @@ -287,11 +288,40 @@ class ShareServerControllerTest(test.TestCase): return_value=share_network)) self.mock_object(db_api, 'share_network_subnet_get_default_subnet', mock.Mock(return_value=share_net_subnet)) + self.mock_object(common, 'check_share_network_is_active', + mock.Mock(return_value=True)) self.assertRaises( exception_to_raise, self.controller.manage, req, {'share_server': self._setup_manage_test_request_body()}) + common.check_share_network_is_active.assert_called_once_with( + share_net_subnet['share_network']) + policy.check_policy.assert_called_once_with( + context, self.resource_name, 'manage_share_server') + + def test__validate_manage_share_network_not_active(self): + req = fakes.HTTPRequest.blank('/manage', version="2.49") + context = req.environ['manila.context'] + + share_network = db_utils.create_share_network() + share_net_subnet = db_utils.create_share_network_subnet( + share_network_id=share_network['id']) + + self.mock_object(db_api, 'share_network_get', mock.Mock( + return_value=share_network)) + self.mock_object(db_api, 'share_network_subnet_get_default_subnet', + mock.Mock(return_value=share_net_subnet)) + self.mock_object(utils, 'validate_service_host') + self.mock_object(common, 'check_share_network_is_active', + mock.Mock(side_effect=webob.exc.HTTPBadRequest())) + + self.assertRaises( + webob.exc.HTTPBadRequest, self.controller.manage, req, + {'share_server': self._setup_manage_test_request_body()}) + + common.check_share_network_is_active.assert_called_once_with( + share_net_subnet['share_network']) policy.check_policy.assert_called_once_with( context, self.resource_name, 'manage_share_server') @@ -434,8 +464,15 @@ class ShareServerControllerTest(test.TestCase): def _setup_unmanage_tests(self, status=constants.STATUS_ACTIVE): server = db_utils.create_share_server( id='fake_server_id', status=status) + share_network = db_utils.create_share_network() + network_subnet = db_utils.create_share_network_subnet( + share_network_id=share_network['id']) self.mock_object(db_api, 'share_server_get', mock.Mock(return_value=server)) + self.mock_object(db_api, 'share_network_get', + mock.Mock(return_value=share_network)) + self.mock_object(db_api, 'share_network_subnet_get', + mock.Mock(return_value=network_subnet)) return server @ddt.data(exception.ShareServerInUse, exception.PolicyNotAuthorized) @@ -446,6 +483,8 @@ class ShareServerControllerTest(test.TestCase): error = mock.Mock(side_effect=exc('foobar')) mock_unmanage = self.mock_object( share_api.API, 'unmanage_share_server', error) + self.mock_object(common, 'check_share_network_is_active', + mock.Mock(return_value=True)) body = {'unmanage': {'force': True}} self.assertRaises(webob.exc.HTTPBadRequest, @@ -455,9 +494,44 @@ class ShareServerControllerTest(test.TestCase): body) mock_unmanage.assert_called_once_with(context, server, force=True) + db_api.share_network_get.assert_called() + common.check_share_network_is_active.assert_called() policy.check_policy.assert_called_once_with( context, self.resource_name, 'unmanage_share_server') + def test_unmanage_share_server_network_not_active(self): + """Tests unmanaging share servers""" + req = fakes.HTTPRequest.blank( + '/v2/share-servers/fake_server_id/', version="2.63") + context = req.environ['manila.context'] + share_server = db_utils.create_share_server() + network_subnet = db_utils.create_share_network_subnet() + share_network = db_utils.create_share_network() + get_mock = self.mock_object( + db_api, 'share_server_get', mock.Mock(return_value=share_server)) + get_subnet_mock = self.mock_object( + db_api, 'share_network_subnet_get', + mock.Mock(return_value=network_subnet)) + get_network_mock = self.mock_object( + db_api, 'share_network_get', + mock.Mock(return_value=share_network)) + is_active_mock = self.mock_object( + common, 'check_share_network_is_active', + mock.Mock(side_effect=webob.exc.HTTPBadRequest())) + body = {'unmanage': {'force': True}} + + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.unmanage, + req, + 'fake_server_id', + body) + get_mock.assert_called_once_with(context, 'fake_server_id') + get_subnet_mock.assert_called_once_with( + context, share_server.get('share_network_subnet_id')) + get_network_mock.assert_called_once_with( + context, network_subnet['share_network_id']) + is_active_mock.assert_called_once_with(share_network) + def _get_server_migration_request(self, server_id): req = fakes.HTTPRequest.blank( '/share-servers/%s/action' % server_id, @@ -478,6 +552,8 @@ class ShareServerControllerTest(test.TestCase): return_value=share_network)) self.mock_object(db_api, 'share_server_get', mock.Mock(return_value=server)) + self.mock_object(common, 'check_share_network_is_active', + mock.Mock(return_value=True)) self.mock_object(share_api.API, 'share_server_migration_start') body = { @@ -499,6 +575,8 @@ class ShareServerControllerTest(test.TestCase): new_share_network=share_network) db_api.share_network_get.assert_called_once_with( context, 'fake_net_id') + common.check_share_network_is_active.assert_called_once_with( + share_network) @ddt.data({'api_exception': exception.ServiceIsDown(service='fake_srv'), 'expected_exception': webob.exc.HTTPBadRequest}, @@ -507,8 +585,12 @@ class ShareServerControllerTest(test.TestCase): @ddt.unpack def test_share_server_migration_start_conflict(self, api_exception, expected_exception): + share_network = db_utils.create_share_network() + share_network_subnet = db_utils.create_share_network_subnet( + share_network_id=share_network['id']) server = db_utils.create_share_server( - id='fake_server_id', status=constants.STATUS_ACTIVE) + id='fake_server_id', status=constants.STATUS_ACTIVE, + share_network_subnet_id=share_network_subnet['id']) req = self._get_server_migration_request(server['id']) context = req.environ['manila.context'] body = { @@ -523,6 +605,10 @@ class ShareServerControllerTest(test.TestCase): mock.Mock(side_effect=api_exception)) self.mock_object(db_api, 'share_server_get', mock.Mock(return_value=server)) + self.mock_object(common, 'check_share_network_is_active', + mock.Mock(return_value=True)) + self.mock_object(db_api, 'share_network_get', + mock.Mock(return_value=share_network)) self.assertRaises(expected_exception, self.controller.share_server_migration_start, @@ -531,6 +617,10 @@ class ShareServerControllerTest(test.TestCase): db_api.share_server_get.assert_called_once_with(context, server['id']) migration_start_params = body['migration_start'] + common.check_share_network_is_active.assert_called_once_with( + share_network) + db_api.share_network_get.assert_called_once_with( + context, share_network['id']) share_api.API.share_server_migration_start.assert_called_once_with( context, server, migration_start_params['host'], migration_start_params['writable'], @@ -960,6 +1050,8 @@ class ShareServerControllerTest(test.TestCase): mock_network_get = self.mock_object( db_api, 'share_network_get', mock.Mock(return_value=fake_share_network)) + self.mock_object(common, 'check_share_network_is_active', + mock.Mock(return_value=True)) mock_migration_check = self.mock_object( share_api.API, 'share_server_migration_check', mock.Mock(return_value=driver_result)) @@ -974,6 +1066,8 @@ class ShareServerControllerTest(test.TestCase): context, fake_share_server['id']) mock_network_get.assert_called_once_with( context, fake_share_network['id']) + common.check_share_network_is_active.assert_called_once_with( + fake_share_network) mock_migration_check.assert_called_once_with( context, fake_share_server, fake_host, requested_writable, requested_nondisruptive, requested_preserve_snapshots, @@ -1030,7 +1124,7 @@ class ShareServerControllerTest(test.TestCase): {'api_exception': exception.ServiceIsDown(service='fake_srv'), 'expected_exception': webob.exc.HTTPBadRequest}, {'api_exception': exception.InvalidShareServer(reason=""), - 'expected_exception': webob.exc.HTTPConflict}) + 'expected_exception': webob.exc.HTTPBadRequest}) @ddt.unpack def test_share_server_migration_complete_exceptions_from_api( self, api_exception, expected_exception): @@ -1048,21 +1142,16 @@ class ShareServerControllerTest(test.TestCase): self.mock_object(db_api, 'share_server_get', mock.Mock(return_value='fake_share_server')) - self.mock_object(share_api.API, 'share_server_migration_check', + self.mock_object(share_api.API, 'share_server_migration_complete', mock.Mock(side_effect=api_exception)) self.assertRaises( expected_exception, - self.controller.share_server_migration_check, + self.controller.share_server_migration_complete, req, 'fake_id', body ) db_api.share_server_get.assert_called_once_with(context, 'fake_id') - migration_check_params = body['migration_check'] - share_api.API.share_server_migration_check.assert_called_once_with( - context, 'fake_share_server', migration_check_params['host'], - migration_check_params['writable'], - migration_check_params['nondisruptive'], - migration_check_params['preserve_snapshots'], - new_share_network=None) + share_api.API.share_server_migration_complete.assert_called_once_with( + context, 'fake_share_server', ) diff --git a/manila/tests/api/v2/test_shares.py b/manila/tests/api/v2/test_shares.py index 2b857dde3f..4cec4db800 100644 --- a/manila/tests/api/v2/test_shares.py +++ b/manila/tests/api/v2/test_shares.py @@ -664,6 +664,7 @@ class ShareAPITest(test.TestCase): "availability_zone": "zone1:host1", "share_network_id": "fakenetid" } + fake_network = {'id': 'fakenetid'} create_mock = mock.Mock(return_value=stubs.stub_share('1', display_name=shr['name'], display_description=shr['description'], @@ -673,7 +674,9 @@ class ShareAPITest(test.TestCase): share_network_id=shr['share_network_id'])) self.mock_object(share_api.API, 'create', create_mock) self.mock_object(share_api.API, 'get_share_network', mock.Mock( - return_value={'id': 'fakenetid'})) + return_value=fake_network)) + self.mock_object(common, 'check_share_network_is_active', + mock.Mock(return_value=True)) self.mock_object( db, 'share_network_subnet_get_by_availability_zone_id') @@ -687,6 +690,8 @@ class ShareAPITest(test.TestCase): # pylint: disable=unsubscriptable-object self.assertEqual("fakenetid", create_mock.call_args[1]['share_network_id']) + common.check_share_network_is_active.assert_called_once_with( + fake_network) @ddt.data("2.15", "2.16") def test_share_create_original_with_user_id(self, microversion): @@ -1268,6 +1273,7 @@ class ShareAPITest(test.TestCase): "share_network_id": None, } parent_share_net = 444 + fake_network = {'id': parent_share_net} create_mock = mock.Mock(return_value=stubs.stub_share('1', display_name=shr['name'], display_description=shr['description'], @@ -1280,13 +1286,15 @@ class ShareAPITest(test.TestCase): self.mock_object(share_api.API, 'create', create_mock) self.mock_object(share_api.API, 'get_snapshot', stubs.stub_snapshot_get) + self.mock_object(common, 'check_share_network_is_active', + mock.Mock(return_value=True)) parent_share = stubs.stub_share( '1', instance={'share_network_id': parent_share_net}, create_share_from_snapshot_support=True) self.mock_object(share_api.API, 'get', mock.Mock( return_value=parent_share)) self.mock_object(share_api.API, 'get_share_network', mock.Mock( - return_value={'id': parent_share_net})) + return_value=fake_network)) self.mock_object( db, 'share_network_subnet_get_by_availability_zone_id') @@ -1301,6 +1309,8 @@ class ShareAPITest(test.TestCase): # pylint: disable=unsubscriptable-object self.assertEqual(parent_share_net, create_mock.call_args[1]['share_network_id']) + common.check_share_network_is_active.assert_called_once_with( + fake_network) def test_share_create_from_snapshot_with_share_net_equals_parent(self): parent_share_net = 444 @@ -1332,6 +1342,8 @@ class ShareAPITest(test.TestCase): return_value=parent_share)) self.mock_object(share_api.API, 'get_share_network', mock.Mock( return_value={'id': parent_share_net})) + self.mock_object(common, 'check_share_network_is_active', + mock.Mock(return_value=True)) self.mock_object( db, 'share_network_subnet_get_by_availability_zone_id') diff --git a/manila/tests/api/views/test_share_networks.py b/manila/tests/api/views/test_share_networks.py index 61de70c968..4095bc8017 100644 --- a/manila/tests/api/views/test_share_networks.py +++ b/manila/tests/api/views/test_share_networks.py @@ -62,6 +62,9 @@ class ViewBuilderTestCase(test.TestCase): <= api_version.APIVersionRequest('2.49')) subnets_support = (api_version.APIVersionRequest(microversion) > api_version.APIVersionRequest('2.49')) + status_and_sec_serv_update = ( + api_version.APIVersionRequest(microversion) >= + api_version.APIVersionRequest('2.63')) req = fakes.HTTPRequest.blank('/share-networks', version=microversion) expected_keys = { 'id', 'name', 'project_id', 'created_at', 'updated_at', @@ -80,6 +83,8 @@ class ViewBuilderTestCase(test.TestCase): expected_keys.add('mtu') if nova_net_support: expected_keys.add('nova_net_id') + if status_and_sec_serv_update: + expected_keys.update({'status', 'security_service_update_support'}) result = self.builder.build_share_network(req, share_network_data) self.assertEqual(1, len(result)) @@ -129,6 +134,9 @@ class ViewBuilderTestCase(test.TestCase): <= api_version.APIVersionRequest('2.49')) subnets_support = (api_version.APIVersionRequest(microversion) > api_version.APIVersionRequest('2.49')) + status_and_sec_serv_update = ( + api_version.APIVersionRequest(microversion) >= + api_version.APIVersionRequest('2.63')) req = fakes.HTTPRequest.blank('/share-networks', version=microversion) expected_networks_list = [] for share_network in share_networks: @@ -166,6 +174,13 @@ class ViewBuilderTestCase(test.TestCase): if nova_net_support: share_network.update({'nova_net_id': 'fake_nova_net_id'}) expected_data.update({'nova_net_id': None}) + if status_and_sec_serv_update: + share_network.update( + {'status': 'active', + 'security_service_update_support': False}) + expected_data.update( + {'status': 'active', + 'security_service_update_support': False}) expected_networks_list.append(expected_data) expected = {'share_networks': expected_networks_list} diff --git a/manila/tests/db/migrations/alembic/migrations_data_checks.py b/manila/tests/db/migrations/alembic/migrations_data_checks.py index 74633487b1..196967018e 100644 --- a/manila/tests/db/migrations/alembic/migrations_data_checks.py +++ b/manila/tests/db/migrations/alembic/migrations_data_checks.py @@ -2971,3 +2971,76 @@ class ShareServerTaskState(BaseMigrationChecks): for ss in engine.execute(ss_table.select()): self.test_case.assertFalse(hasattr(ss, 'task_state')) self.test_case.assertFalse(hasattr(ss, 'source_share_server_id')) + + +@map_to_migration('478c445d8d3e') +class AddUpdateSecurityServiceControlFields(BaseMigrationChecks): + + def setup_upgrade_data(self, engine): + user_id = 'user_id' + project_id = 'project_id' + + # Create share network + share_network_data = { + 'id': uuidutils.generate_uuid(), + 'user_id': user_id, + 'project_id': project_id, + } + sn_table = utils.load_table('share_networks', engine) + engine.execute(sn_table.insert(share_network_data)) + + share_network_subnet_data = { + 'id': uuidutils.generate_uuid(), + 'share_network_id': share_network_data['id'] + } + + sns_table = utils.load_table('share_network_subnets', engine) + engine.execute(sns_table.insert(share_network_subnet_data)) + + # Create share server + share_server_data = { + 'id': uuidutils.generate_uuid(), + 'share_network_subnet_id': share_network_subnet_data['id'], + 'host': 'fake_host', + 'status': 'active', + } + ss_table = utils.load_table('share_servers', engine) + engine.execute(ss_table.insert(share_server_data)) + + def check_upgrade(self, engine, data): + ss_table = utils.load_table('share_servers', engine) + for ss in engine.execute(ss_table.select()): + self.test_case.assertTrue( + hasattr(ss, 'security_service_update_support')) + self.test_case.assertEqual( + False, ss.security_service_update_support) + + sn_table = utils.load_table('share_networks', engine) + for sn in engine.execute(sn_table.select()): + self.test_case.assertTrue(hasattr(sn, 'status')) + self.test_case.assertEqual(constants.STATUS_NETWORK_ACTIVE, + sn.status) + async_op_data = { + 'created_at': datetime.datetime(2021, 3, 12, 17, 40, 34), + 'updated_at': None, + 'deleted_at': None, + 'deleted': 0, + 'entity_uuid': uuidutils.generate_uuid(), + 'key': 't' * 255, + 'value': 'v' * 1023, + } + async_op_data_table = utils.load_table('async_operation_data', engine) + engine.execute(async_op_data_table.insert(async_op_data)) + + def check_downgrade(self, engine): + ss_table = utils.load_table('share_servers', engine) + for ss in engine.execute(ss_table.select()): + self.test_case.assertFalse( + hasattr(ss, 'security_service_update_support')) + sn_table = utils.load_table('share_networks', engine) + for sn in engine.execute(sn_table.select()): + self.test_case.assertFalse(hasattr(sn, 'status')) + + self.test_case.assertRaises( + sa_exc.NoSuchTableError, + utils.load_table, 'async_operation_data', engine) diff --git a/manila/tests/db/sqlalchemy/test_api.py b/manila/tests/db/sqlalchemy/test_api.py index 177e3ada8e..da27916002 100644 --- a/manila/tests/db/sqlalchemy/test_api.py +++ b/manila/tests/db/sqlalchemy/test_api.py @@ -2544,6 +2544,45 @@ class ShareNetworkDatabaseAPITestCase(BaseDatabaseAPITestCase): self.assertEqual(0, len(result['share_instances'])) + def test_association_get(self): + network = db_api.share_network_create( + self.fake_context, self.share_nw_dict) + security_service = db_api.security_service_create( + self.fake_context, security_service_dict) + network_id = network['id'] + security_service_id = security_service['id'] + + db_api.share_network_add_security_service( + self.fake_context, network_id, security_service_id) + result = db_api.share_network_security_service_association_get( + self.fake_context, network_id, security_service_id) + + self.assertEqual(result['share_network_id'], network_id) + self.assertEqual(result['security_service_id'], security_service_id) + + def test_share_network_update_security_service(self): + new_sec_service = copy.copy(security_service_dict) + new_sec_service['id'] = 'fakeid' + share_network_id = self.share_nw_dict['id'] + db_api.share_network_create( + self.fake_context, self.share_nw_dict) + db_api.security_service_create( + self.fake_context, security_service_dict) + db_api.security_service_create(self.fake_context, new_sec_service) + db_api.share_network_add_security_service( + self.fake_context, share_network_id, + security_service_dict['id']) + db_api.share_network_update_security_service( + self.fake_context, share_network_id, security_service_dict['id'], + new_sec_service['id']) + + association = db_api.share_network_security_service_association_get( + self.fake_context, share_network_id, new_sec_service['id']) + + self.assertEqual(association['share_network_id'], share_network_id) + self.assertEqual( + association['security_service_id'], new_sec_service['id']) + @ddt.ddt class ShareNetworkSubnetDatabaseAPITestCase(BaseDatabaseAPITestCase): diff --git a/manila/tests/db_utils.py b/manila/tests/db_utils.py index 768209a59d..8349f89b3d 100644 --- a/manila/tests/db_utils.py +++ b/manila/tests/db_utils.py @@ -251,7 +251,7 @@ def create_share_network(**kwargs): net = { 'user_id': 'fake', 'project_id': 'fake', - 'status': 'new', + 'status': 'active', 'name': 'whatever', 'description': 'fake description', } diff --git a/manila/tests/scheduler/test_host_manager.py b/manila/tests/scheduler/test_host_manager.py index 6a5a9ed2a5..efb5f6fefe 100644 --- a/manila/tests/scheduler/test_host_manager.py +++ b/manila/tests/scheduler/test_host_manager.py @@ -219,6 +219,7 @@ class HostManagerTestCase(test.TestCase): 'replication_type': None, 'replication_domain': None, 'sg_consistent_snapshot_support': None, + 'security_service_update_support': False, }, }, { 'name': 'host2@back1#BBB', @@ -247,6 +248,7 @@ class HostManagerTestCase(test.TestCase): 'replication_type': None, 'replication_domain': None, 'sg_consistent_snapshot_support': None, + 'security_service_update_support': False, }, }, { 'name': 'host2@back2#CCC', @@ -275,6 +277,7 @@ class HostManagerTestCase(test.TestCase): 'replication_type': None, 'replication_domain': None, 'sg_consistent_snapshot_support': None, + 'security_service_update_support': False, }, }, ] @@ -325,6 +328,7 @@ class HostManagerTestCase(test.TestCase): 'replication_type': None, 'replication_domain': None, 'sg_consistent_snapshot_support': None, + 'security_service_update_support': False, }, }, { 'name': 'host2@BBB#pool2', @@ -354,6 +358,7 @@ class HostManagerTestCase(test.TestCase): 'replication_type': None, 'replication_domain': None, 'sg_consistent_snapshot_support': None, + 'security_service_update_support': False, }, }, { 'name': 'host3@CCC#pool3', @@ -383,6 +388,7 @@ class HostManagerTestCase(test.TestCase): 'replication_type': None, 'replication_domain': None, 'sg_consistent_snapshot_support': None, + 'security_service_update_support': False, }, }, { 'name': 'host4@DDD#pool4a', @@ -412,6 +418,7 @@ class HostManagerTestCase(test.TestCase): 'replication_type': None, 'replication_domain': None, 'sg_consistent_snapshot_support': None, + 'security_service_update_support': False, }, }, { 'name': 'host4@DDD#pool4b', @@ -441,6 +448,7 @@ class HostManagerTestCase(test.TestCase): 'replication_type': None, 'replication_domain': None, 'sg_consistent_snapshot_support': None, + 'security_service_update_support': False, }, }, ] @@ -503,6 +511,7 @@ class HostManagerTestCase(test.TestCase): 'replication_type': None, 'replication_domain': None, 'sg_consistent_snapshot_support': None, + 'security_service_update_support': False, }, }, { 'name': 'host2@back1#BBB', @@ -531,6 +540,7 @@ class HostManagerTestCase(test.TestCase): 'replication_type': None, 'replication_domain': None, 'sg_consistent_snapshot_support': None, + 'security_service_update_support': False, }, }, ] @@ -587,6 +597,7 @@ class HostManagerTestCase(test.TestCase): 'replication_type': None, 'replication_domain': None, 'sg_consistent_snapshot_support': None, + 'security_service_update_support': False, }, }, ] diff --git a/manila/tests/share/drivers/dell_emc/test_driver.py b/manila/tests/share/drivers/dell_emc/test_driver.py index 341ba6419b..a9158ffafe 100644 --- a/manila/tests/share/drivers/dell_emc/test_driver.py +++ b/manila/tests/share/drivers/dell_emc/test_driver.py @@ -142,6 +142,7 @@ class EMCShareFrameworkTestCase(test.TestCase): data['ipv6_support'] = False data['max_shares_per_share_server'] = -1 data['max_share_server_size'] = -1 + data['security_service_update_support'] = False self.assertEqual(data, self.driver._stats) def _fake_safe_get(self, value): diff --git a/manila/tests/share/drivers/dummy.py b/manila/tests/share/drivers/dummy.py index 6cde41d656..c194ae77b9 100644 --- a/manila/tests/share/drivers/dummy.py +++ b/manila/tests/share/drivers/dummy.py @@ -140,6 +140,7 @@ class DummyDriver(driver.ShareDriver): self.backend_name = self.configuration.safe_get( "share_backend_name") or "DummyDriver" self.migration_progress = {} + self.security_service_update_support = True def _verify_configuration(self): allowed_driver_methods = [m for m in dir(self) if m[0] != '_'] @@ -852,3 +853,34 @@ class DummyDriver(driver.ShareDriver): 'export_locations': self.private_storage.get(share['id'], key='export_location') } + + @slow_me_down + def update_share_server_security_service(self, context, share_server, + network_info, share_instances, + share_instance_rules, + new_security_service, + current_security_service=None): + if current_security_service: + msg = _("Replacing security service %(cur_sec_serv_id)s by " + "security service %(new_sec_serv_id)s on share server " + "%(server_id)s." + ) % { + 'cur_sec_serv_id': current_security_service['id'], + 'new_sec_serv_id': new_security_service['id'], + 'server_id': share_server['id'] + } + else: + msg = _("Adding security service %(sec_serv_id)s on share server " + "%(server_id)s." + ) % { + 'sec_serv_id': new_security_service['id'], + 'server_id': share_server['id'] + } + + LOG.debug(msg) + + def check_update_share_server_security_service( + self, context, share_server, network_info, share_instances, + share_instance_rules, new_security_service, + current_security_service=None): + return True diff --git a/manila/tests/share/drivers/glusterfs/test_glusterfs_native.py b/manila/tests/share/drivers/glusterfs/test_glusterfs_native.py index f6c90ad1bc..07139919cf 100644 --- a/manila/tests/share/drivers/glusterfs/test_glusterfs_native.py +++ b/manila/tests/share/drivers/glusterfs/test_glusterfs_native.py @@ -268,6 +268,7 @@ class GlusterfsNativeShareDriverTestCase(test.TestCase): 'goodness_function': None, 'ipv4_support': True, 'ipv6_support': False, + 'security_service_update_support': False, } self.assertEqual(test_data, self._driver._stats) diff --git a/manila/tests/share/drivers/hpe/test_hpe_3par_driver.py b/manila/tests/share/drivers/hpe/test_hpe_3par_driver.py index 8aa44312ef..3e809bb05a 100644 --- a/manila/tests/share/drivers/hpe/test_hpe_3par_driver.py +++ b/manila/tests/share/drivers/hpe/test_hpe_3par_driver.py @@ -748,6 +748,7 @@ class HPE3ParDriverTestCase(test.TestCase): 'ipv6_support': False, 'max_share_server_size': -1, 'max_shares_per_share_server': -1, + 'security_service_update_support': False, } result = self.driver.get_share_stats(refresh=True) @@ -801,6 +802,8 @@ class HPE3ParDriverTestCase(test.TestCase): 'provisioned_capacity_gb': 0, 'reserved_percentage': 0, 'max_over_subscription_ratio': None, + 'max_share_server_size': -1, + 'max_shares_per_share_server': -1, 'qos': False, 'thin_provisioning': True, 'pools': [{ @@ -816,6 +819,7 @@ class HPE3ParDriverTestCase(test.TestCase): 'snapshot_support': True, 'create_share_from_snapshot_support': True, 'revert_to_snapshot_support': False, + 'security_service_update_support': False, 'mount_snapshot_support': False, 'share_group_stats': { 'consistent_snapshot_support': None, @@ -825,8 +829,6 @@ class HPE3ParDriverTestCase(test.TestCase): 'goodness_function': None, 'ipv4_support': True, 'ipv6_support': False, - 'max_share_server_size': -1, - 'max_shares_per_share_server': -1, } result = self.driver.get_share_stats(refresh=True) @@ -851,6 +853,8 @@ class HPE3ParDriverTestCase(test.TestCase): 'driver_version': expected_version, 'free_capacity_gb': 0, 'max_over_subscription_ratio': None, + 'max_share_server_size': -1, + 'max_shares_per_share_server': -1, 'pools': None, 'provisioned_capacity_gb': 0, 'reserved_percentage': 0, @@ -862,6 +866,7 @@ class HPE3ParDriverTestCase(test.TestCase): 'snapshot_support': True, 'create_share_from_snapshot_support': True, 'revert_to_snapshot_support': False, + 'security_service_update_support': False, 'mount_snapshot_support': False, 'share_group_stats': { 'consistent_snapshot_support': None, @@ -871,8 +876,6 @@ class HPE3ParDriverTestCase(test.TestCase): 'goodness_function': None, 'ipv4_support': True, 'ipv6_support': False, - 'max_share_server_size': -1, - 'max_shares_per_share_server': -1, } result = self.driver.get_share_stats(refresh=True) diff --git a/manila/tests/share/drivers/huawei/test_huawei_nas.py b/manila/tests/share/drivers/huawei/test_huawei_nas.py index 2dbf44ace1..d5a8ae2621 100644 --- a/manila/tests/share/drivers/huawei/test_huawei_nas.py +++ b/manila/tests/share/drivers/huawei/test_huawei_nas.py @@ -2434,6 +2434,7 @@ class HuaweiShareDriverTestCase(test.TestCase): "share_group_stats": {"consistent_snapshot_support": None}, "ipv4_support": True, "ipv6_support": False, + "security_service_update_support": False, } if replication_support: diff --git a/manila/tests/share/drivers/veritas/test_veritas_isa.py b/manila/tests/share/drivers/veritas/test_veritas_isa.py index 486fc50334..df8de690d2 100644 --- a/manila/tests/share/drivers/veritas/test_veritas_isa.py +++ b/manila/tests/share/drivers/veritas/test_veritas_isa.py @@ -446,6 +446,7 @@ class ACCESSShareDriverTestCase(test.TestCase): 'revert_to_snapshot_support': False, 'share_group_stats': {'consistent_snapshot_support': None}, 'snapshot_support': True, + 'security_service_update_support': False, } self.assertEqual(data, self._driver._stats) diff --git a/manila/tests/share/drivers/zfsonlinux/test_driver.py b/manila/tests/share/drivers/zfsonlinux/test_driver.py index de885e3db6..76241870c1 100644 --- a/manila/tests/share/drivers/zfsonlinux/test_driver.py +++ b/manila/tests/share/drivers/zfsonlinux/test_driver.py @@ -368,6 +368,7 @@ class ZFSonLinuxShareDriverTestCase(test.TestCase): 'goodness_function': None, 'ipv4_support': True, 'ipv6_support': False, + 'security_service_update_support': False, } if replication_domain: expected['replication_type'] = 'readable' diff --git a/manila/tests/share/test_api.py b/manila/tests/share/test_api.py index 2d1ac1c680..e05fb8fbc0 100644 --- a/manila/tests/share/test_api.py +++ b/manila/tests/share/test_api.py @@ -23,6 +23,7 @@ import ddt from oslo_config import cfg from oslo_utils import timeutils from oslo_utils import uuidutils +from webob import exc as webob_exc from manila.common import constants from manila import context @@ -1098,6 +1099,7 @@ class ShareAPITestCase(test.TestCase): share_server = db_utils.create_share_server( status=constants.STATUS_ACTIVE, id=share_server_id, share_network_subnet_id=fake_subnet['id']) + share_network = db_utils.create_share_network(id='fake') fake_share_data = { 'id': 'fakeid', 'status': constants.STATUS_CREATING, @@ -1130,6 +1132,8 @@ class ShareAPITestCase(test.TestCase): mock.Mock(return_value=fake_subnet)) self.mock_object(db_api, 'share_instances_get_all', mock.Mock(return_value=[])) + self.mock_object(db_api, 'share_network_get', + mock.Mock(return_value=share_network)) self.api.manage(self.context, copy.deepcopy(share_data), driver_options) @@ -5664,6 +5668,262 @@ class ShareAPITestCase(test.TestCase): self.api.share_rpcapi.migration_get_progress.assert_called_once_with( self.context, instance1, instance2['id']) + def test__share_network_update_initial_checks_network_not_active(self): + share_network = db_utils.create_share_network( + status=constants.STATUS_NETWORK_CHANGE) + new_sec_service = db_utils.create_security_service( + share_network_id=share_network['id'], type='ldap') + + self.assertRaises( + webob_exc.HTTPBadRequest, + self.api._share_network_update_initial_checks, + self.context, share_network, new_sec_service + ) + + def test__share_network_update_initial_checks_server_not_active(self): + db_utils.create_share_server( + share_network_subnet_id='fakeid', status=constants.STATUS_ERROR, + security_service_update_support=True) + db_utils.create_share_network_subnet( + id='fakeid', share_network_id='fakenetid') + share_network = db_utils.create_share_network(id='fakenetid') + new_sec_service = db_utils.create_security_service( + share_network_id='fakenetid', type='ldap') + + self.assertRaises( + exception.InvalidShareNetwork, + self.api._share_network_update_initial_checks, + self.context, share_network, new_sec_service, + ) + + def test__share_network_update_initial_checks_shares_not_available(self): + db_utils.create_share_server(share_network_subnet_id='fakeid', + security_service_update_support=True) + db_utils.create_share_network_subnet( + id='fakeid', share_network_id='fake_network_id') + share_network = db_utils.create_share_network( + id='fake_network_id') + new_sec_service = db_utils.create_security_service( + share_network_id='fake_network_id', type='ldap') + shares = [db_utils.create_share(status=constants.STATUS_ERROR)] + + self.mock_object(utils, 'validate_service_host') + self.mock_object( + self.api, 'get_all', mock.Mock(return_value=shares)) + + self.assertRaises( + exception.InvalidShareNetwork, + self.api._share_network_update_initial_checks, + self.context, share_network, new_sec_service + ) + utils.validate_service_host.assert_called_once_with( + utils.IsAMatcher(context.RequestContext), 'host1') + self.api.get_all.assert_called_once_with( + self.context, + search_opts={'share_network_id': share_network['id']}) + + def test__share_network_update_initial_checks_rules_in_error(self): + db_utils.create_share_server(share_network_subnet_id='fakeid', + security_service_update_support=True) + db_utils.create_share_network_subnet( + id='fakeid', share_network_id='fake_network_id') + share_network = db_utils.create_share_network( + id='fake_network_id') + new_sec_service = db_utils.create_security_service( + share_network_id='fake_network_id', type='ldap') + shares = [db_utils.create_share(status=constants.STATUS_AVAILABLE)] + shares[0]['instance']['access_rules_status'] = ( + constants.ACCESS_STATE_ERROR) + + self.mock_object(utils, 'validate_service_host') + self.mock_object( + self.api, 'get_all', mock.Mock(return_value=shares)) + + self.assertRaises( + exception.InvalidShareNetwork, + self.api._share_network_update_initial_checks, + self.context, share_network, new_sec_service + ) + utils.validate_service_host.assert_called_once_with( + utils.IsAMatcher(context.RequestContext), 'host1') + self.api.get_all.assert_called_once_with( + self.context, + search_opts={'share_network_id': share_network['id']}) + + def test__share_network_update_initial_checks_share_is_busy(self): + db_utils.create_share_server(share_network_subnet_id='fakeid', + security_service_update_support=True) + db_utils.create_share_network_subnet( + id='fakeid', share_network_id='fake_net_id') + share_network = db_utils.create_share_network(id='fake_net_id') + new_sec_service = db_utils.create_security_service( + share_network_id='fake_net_id', type='ldap') + shares = [db_utils.create_share(status=constants.STATUS_AVAILABLE)] + + self.mock_object(utils, 'validate_service_host') + self.mock_object( + self.api, 'get_all', mock.Mock(return_value=shares)) + self.mock_object( + self.api, '_check_is_share_busy', + mock.Mock(side_effect=exception.ShareBusyException(message='fake')) + ) + + self.assertRaises( + exception.InvalidShareNetwork, + self.api._share_network_update_initial_checks, + self.context, share_network, new_sec_service + ) + utils.validate_service_host.assert_called_once_with( + utils.IsAMatcher(context.RequestContext), 'host1') + self.api.get_all.assert_called_once_with( + self.context, + search_opts={'share_network_id': share_network['id']}) + self.api._check_is_share_busy.assert_called_once_with(shares[0]) + + def test__share_network_update_initial_checks_unsupported_server(self): + db_utils.create_share_server(share_network_subnet_id='fakeid', + security_service_update_support=False) + db_utils.create_share_network_subnet( + id='fakeid', share_network_id='fake_net_id') + share_network = db_utils.create_share_network(id='fake_net_id') + + self.assertRaises( + exception.InvalidShareNetwork, + self.api._share_network_update_initial_checks, + self.context, share_network, None + ) + + def test__share_network_update_initial_checks_update_different_types(self): + db_utils.create_share_server(share_network_subnet_id='fakeid', + security_service_update_support=True) + db_utils.create_share_network_subnet( + id='fakeid', share_network_id='fake_net_id') + share_network = db_utils.create_share_network(id='fake_net_id') + new_sec_service = db_utils.create_security_service( + share_network_id='fake_net_id', type='ldap') + curr_sec_service = db_utils.create_security_service( + share_network_id='fake_net_id', type='kerberos') + + self.assertRaises( + exception.InvalidSecurityService, + self.api._share_network_update_initial_checks, + self.context, share_network, new_sec_service, + current_security_service=curr_sec_service + ) + + def test__share_network_update_initial_checks_add_type_conflict(self): + db_utils.create_share_server(share_network_subnet_id='fakeid', + security_service_update_support=True) + db_utils.create_share_network_subnet( + id='fakeid', share_network_id='fake_net_id') + share_network = db_utils.create_share_network(id='fake_net_id') + db_utils.create_security_service( + share_network_id='fake_net_id', type='ldap') + share_network = db_api.share_network_get(self.context, + share_network['id']) + new_sec_service = db_utils.create_security_service( + share_network_id='fake_net_id', type='ldap') + + self.assertRaises( + exception.InvalidSecurityService, + self.api._share_network_update_initial_checks, + self.context, share_network, new_sec_service, + ) + + def test_update_share_network_security_service_backend_host_failure(self): + share_network = db_utils.create_share_network() + security_service = db_utils.create_security_service() + backend_host = 'fakehost' + + mock_initial_checks = self.mock_object( + self.api, '_share_network_update_initial_checks', + mock.Mock(return_value=(['fake_server'], [backend_host]))) + mock_get_update_key = self.mock_object( + self.api, 'get_security_service_update_key', + mock.Mock(return_value='fake_key')) + mock_db_async_op = self.mock_object( + db_api, 'async_operation_data_get', + mock.Mock(return_value='fake_update_value')) + mock_validate_host = self.mock_object( + self.api, '_security_service_update_validate_hosts', + mock.Mock(return_value=(False, None))) + + self.assertRaises( + exception.InvalidShareNetwork, + self.api.update_share_network_security_service, + self.context, share_network, security_service) + + mock_initial_checks.assert_called_once_with( + self.context, share_network, security_service, + current_security_service=None) + mock_db_async_op.assert_called_once_with( + self.context, share_network['id'], 'fake_key') + mock_get_update_key.assert_called_once_with( + 'hosts_check', security_service['id'], + current_security_service_id=None) + mock_validate_host.assert_called_once_with( + self.context, share_network, [backend_host], ['fake_server'], + new_security_service_id=security_service['id'], + current_security_service_id=None) + + def test_update_share_network_security_service(self): + share_network = db_utils.create_share_network() + security_service = db_utils.create_security_service() + backend_hosts = ['fakehost'] + fake_update_key = 'fake_key' + servers = [ + db_utils.create_share_server() for i in range(2)] + server_ids = [server['id'] for server in servers] + + mock_initial_checks = self.mock_object( + self.api, '_share_network_update_initial_checks', + mock.Mock(return_value=(servers, backend_hosts))) + mock_get_update_key = self.mock_object( + self.api, 'get_security_service_update_key', + mock.Mock(return_value=fake_update_key)) + mock_db_async_op = self.mock_object( + db_api, 'async_operation_data_get', + mock.Mock(return_value='fake_update_value')) + mock_validate_host = self.mock_object( + self.api, '_security_service_update_validate_hosts', + mock.Mock(return_value=(True, None))) + mock_network_update = self.mock_object( + db_api, 'share_network_update') + mock_servers_update = self.mock_object( + db_api, 'share_servers_update') + mock_update_security_services = self.mock_object( + self.share_rpcapi, 'update_share_network_security_service') + mock_db_async_op_del = self.mock_object( + db_api, 'async_operation_data_delete',) + + self.api.update_share_network_security_service( + self.context, share_network, security_service) + + mock_initial_checks.assert_called_once_with( + self.context, share_network, security_service, + current_security_service=None) + mock_db_async_op.assert_called_once_with( + self.context, share_network['id'], fake_update_key) + mock_get_update_key.assert_called_once_with( + 'hosts_check', security_service['id'], + current_security_service_id=None) + mock_validate_host.assert_called_once_with( + self.context, share_network, backend_hosts, servers, + new_security_service_id=security_service['id'], + current_security_service_id=None) + mock_network_update.assert_called_once_with( + self.context, share_network['id'], + {'status': constants.STATUS_NETWORK_CHANGE}) + mock_servers_update.assert_called_once_with( + self.context, server_ids, + {'status': constants.STATUS_SERVER_NETWORK_CHANGE} + ) + mock_update_security_services.assert_called_once_with( + self.context, backend_hosts[0], share_network['id'], + security_service['id'], current_security_service_id=None) + mock_db_async_op_del.assert_called_once_with( + self.context, share_network['id'], fake_update_key) + class OtherTenantsShareActionsTestCase(test.TestCase): def setUp(self): diff --git a/manila/tests/share/test_driver.py b/manila/tests/share/test_driver.py index 5158067996..3e009b2cee 100644 --- a/manila/tests/share/test_driver.py +++ b/manila/tests/share/test_driver.py @@ -945,6 +945,31 @@ class ShareDriverTestCase(test.TestCase): self.assertIsNone(share_group_update) self.assertEqual(expected_share_updates, share_update) + def test_update_share_server_security_service(self): + share_driver = self._instantiate_share_driver(None, True) + self.assertRaises(NotImplementedError, + share_driver.update_share_server_security_service, + 'fake_context', + {'id', 'share_server_id'}, + {'fake', 'fake_net_info'}, + [{"id": "fake_instance_id"}], + [{"id": "fake_rule_id"}], + {'id', 'fake_sec_service_id'}, + current_security_service=None) + + def test_check_update_share_server_security_service(self): + share_driver = self._instantiate_share_driver(None, True) + self.assertRaises( + NotImplementedError, + share_driver.check_update_share_server_security_service, + 'fake_context', + {'id', 'share_server_id'}, + {'fake', 'fake_net_info'}, + [{"id": "fake_instance_id"}], + [{"id": "fake_rule_id"}], + {'id', 'fake_sec_service_id'}, + current_security_service=None) + def test_create_share_group_from_sg_snapshot_with_no_members(self): share_driver = self._instantiate_share_driver(None, False) fake_share_group_dict = {} diff --git a/manila/tests/share/test_manager.py b/manila/tests/share/test_manager.py index 6874ab49d8..c5331f12b6 100644 --- a/manila/tests/share/test_manager.py +++ b/manila/tests/share/test_manager.py @@ -579,6 +579,7 @@ class ShareManagerTestCase(test.TestCase): 'source_share_group_snapshot_member_id'), 'availability_zone': share_instance.get('availability_zone'), 'export_locations': share_instance.get('export_locations') or [], + 'share_network_status': share_instance.get('share_network_status') } return share_instance_ref @@ -1115,7 +1116,7 @@ class ShareManagerTestCase(test.TestCase): self.mock_object(db, 'share_instance_access_get', mock.Mock(return_value=fake_access_rules[0])) mock_share_replica_access_update = self.mock_object( - self.share_manager, '_update_share_replica_access_rules_state') + self.share_manager, '_update_share_instance_access_rules_state') driver_call = self.mock_object( self.share_manager.driver, 'create_replica', mock.Mock(return_value=replica)) @@ -2828,6 +2829,7 @@ class ShareManagerTestCase(test.TestCase): 'host': self.share_manager.host, 'share_network_subnet_id': fake_data['fake_network_subnet']['id'], 'status': constants.STATUS_CREATING, + 'security_service_update_support': False, } fake_metadata = { 'migration_destination': True, @@ -9034,6 +9036,377 @@ class ShareManagerTestCase(test.TestCase): self.context, fake_source_share_server, fake_dest_share_server, fake_share_instances, fake_snapshot_instances) + @ddt.data([constants.STATUS_ERROR, constants.STATUS_ACTIVE], + [constants.STATUS_ACTIVE, constants.STATUS_ACTIVE]) + def test__check_share_network_update_finished(self, server_statuses): + share_servers = [ + db_utils.create_share_server(status=status) + for status in server_statuses] + share_network = db_utils.create_share_network( + status=constants.STATUS_SERVER_NETWORK_CHANGE) + all_servers_are_active = ( + all(server_statuses) == constants.STATUS_ACTIVE) + + self.mock_object(db, 'share_network_get', + mock.Mock(return_value=share_network)) + self.mock_object( + db, 'share_server_get_all_with_filters', + mock.Mock(return_value=share_servers)) + self.mock_object(db, 'share_network_update') + + self.share_manager._check_share_network_update_finished( + self.context, share_network['id']) + + db.share_server_get_all_with_filters.assert_called_once_with( + self.context, {'share_network_id': share_network['id']}) + db.share_network_get.assert_called_once_with( + self.context, share_network['id']) + if all_servers_are_active: + db.share_network_update.assert_called_once_with( + self.context, share_network['id'], + {'status': constants.STATUS_NETWORK_ACTIVE}) + + def test__check_share_network_update_finished_already_active(self): + share_network = db_utils.create_share_network() + + self.mock_object(db, 'share_network_get', + mock.Mock(return_value=share_network)) + self.mock_object(db, 'share_server_get_all_with_filters') + + self.share_manager._check_share_network_update_finished( + self.context, share_network['id']) + + db.share_network_get.assert_called_once_with( + self.context, share_network['id']) + db.share_server_get_all_with_filters.assert_not_called() + + def _setup_mocks_for_sec_service_update( + self, service_get_effect, share_network, share_servers, subnet, + network_info, share_instances, fake_rules, + driver_support_update=True, driver_update_action=mock.Mock()): + + self.mock_object( + db, 'security_service_get', + mock.Mock(side_effect=service_get_effect)) + self.mock_object( + db, 'share_network_get', + mock.Mock(return_value=share_network)) + self.mock_object( + db, 'share_server_get_all_by_host', + mock.Mock(return_value=share_servers)) + self.mock_object( + db, 'share_network_subnet_get', mock.Mock(return_value=subnet)) + self.mock_object( + self.share_manager, '_form_server_setup_info', + mock.Mock(return_value=network_info)) + self.mock_object( + db, 'share_instances_get_all_by_share_server', + mock.Mock(return_value=share_instances)) + self.mock_object( + db, 'share_access_get_all_for_instance', + mock.Mock(return_value=fake_rules)) + self.mock_object( + self.share_manager.driver, + 'check_update_share_server_security_service', + mock.Mock(return_value=driver_support_update)) + self.mock_object(db, 'share_server_backend_details_set') + self.mock_object( + self.share_manager.driver, + 'update_share_server_security_service', driver_update_action) + self.mock_object(db, 'share_server_update') + self.mock_object( + self.share_manager, '_check_share_network_update_finished') + self.mock_object( + self.share_manager.access_helper, + 'get_and_update_share_instance_access_rules') + self.mock_object( + self.share_manager.access_helper, + 'update_share_instances_access_rules_status') + self.mock_object( + self.share_manager.access_helper, 'process_driver_rule_updates') + + @ddt.data(False, True) + def test__update_share_network_security_service(self, is_check_only): + security_services = [ + db_utils.create_security_service() for i in range(2)] + share_network = db_utils.create_share_network() + share_network_subnet = db_utils.create_share_network_subnet() + share_servers = [ + db_utils.create_share_server( + share_network_subnet_id=share_network_subnet['id'])] + security_services_effect = mock.Mock(side_effect=security_services) + share_network_id = share_network['id'] + current_security_service_id = security_services[0]['id'] + new_security_service_id = security_services[1]['id'] + share_network_subnet_id = share_servers[0]['share_network_subnet_id'] + share_instances = [db_utils.create_share()['instance']] + fake_rules = ['fake_rules'] + network_info = {'fake': 'fake'} + backend_details_keys = [ + 'name', 'ou', 'domain', 'server', 'dns_ip', 'user', 'type', + 'password'] + backend_details_data = {} + [backend_details_data.update( + {key: security_services[0][key]}) for key in backend_details_keys] + backend_details_exp_update = { + 'security_service_' + security_services[0]['type']: + jsonutils.dumps(backend_details_data) + } + expected_instance_rules = [{ + 'share_instance_id': share_instances[0]['id'], + 'access_rules': fake_rules + }] + rule_updates = { + share_instances[0]['id']: { + 'access_rule_id': { + 'access_key': 'fake_access_key', + 'state': 'active', + }, + }, + + } + expected_rule_updates_value = rule_updates[share_instances[0]['id']] + driver_return = mock.Mock(return_value=rule_updates) + + self._setup_mocks_for_sec_service_update( + security_services_effect, share_network, share_servers, + share_network_subnet, network_info, share_instances, fake_rules, + driver_update_action=driver_return) + + result = self.share_manager._update_share_network_security_service( + self.context, share_network_id, new_security_service_id, + current_security_service_id=current_security_service_id, + check_only=is_check_only) + + db.security_service_get.assert_has_calls( + [mock.call(self.context, security_services[1]['id']), + mock.call(self.context, security_services[0]['id'])] + ) + db.share_network_get.assert_called_once_with( + self.context, share_network_id) + db.share_server_get_all_by_host.assert_called_once_with( + self.context, self.share_manager.host, + filters={'share_network_id': share_network_id}) + db.share_network_subnet_get.assert_called_once_with( + self.context, share_network_subnet_id) + self.share_manager._form_server_setup_info.assert_called_once_with( + self.context, share_servers[0], share_network, share_network_subnet + ) + db.share_instances_get_all_by_share_server.assert_called_once_with( + self.context, share_servers[0]['id'], with_share_data=True) + db.share_access_get_all_for_instance.assert_called_once_with( + self.context, share_instances[0]['id']) + if not is_check_only: + (self.share_manager.driver.update_share_server_security_service. + assert_called_once_with( + self.context, share_servers[0], network_info, + share_instances, + expected_instance_rules, + security_services[0], + current_security_service=security_services[1])) + db.share_server_backend_details_set.assert_called_once_with( + self.context, share_servers[0]['id'], + backend_details_exp_update) + db.share_server_update.assert_called_once_with( + self.context, share_servers[0]['id'], + {'status': constants.STATUS_ACTIVE}) + (self.share_manager.access_helper.process_driver_rule_updates. + assert_called_once_with( + self.context, expected_rule_updates_value, + share_instances[0]['id'])) + else: + (self.share_manager.driver. + check_update_share_server_security_service. + assert_called_once_with( + self.context, share_servers[0], network_info, + share_instances, + expected_instance_rules, + security_services[0], + current_security_service=security_services[1])) + self.assertEqual(result, True) + + def test__update_share_network_security_service_no_support(self): + security_services = [ + db_utils.create_security_service() for i in range(2)] + share_network = db_utils.create_share_network() + share_network_subnet = db_utils.create_share_network_subnet() + share_servers = [ + db_utils.create_share_server( + share_network_subnet_id=share_network_subnet['id'])] + security_services_effect = mock.Mock(side_effect=security_services) + share_network_id = share_network['id'] + current_security_service_id = security_services[0]['id'] + new_security_service_id = security_services[1]['id'] + share_network_subnet_id = share_servers[0]['share_network_subnet_id'] + network_info = {'fake': 'fake'} + share_instances = [db_utils.create_share()['instance']] + fake_rules = ['fake_rules'] + expected_instance_rules = [{ + 'share_instance_id': share_instances[0]['id'], + 'access_rules': fake_rules + }] + + self._setup_mocks_for_sec_service_update( + security_services_effect, share_network, share_servers, + share_network_subnet, network_info, share_instances, fake_rules, + driver_support_update=False) + + result = self.share_manager._update_share_network_security_service( + self.context, share_network_id, new_security_service_id, + current_security_service_id=current_security_service_id, + check_only=True) + + db.security_service_get.assert_has_calls( + [mock.call(self.context, security_services[1]['id']), + mock.call(self.context, security_services[0]['id'])] + ) + db.share_network_get.assert_called_once_with( + self.context, share_network_id) + db.share_server_get_all_by_host.assert_called_once_with( + self.context, self.share_manager.host, + filters={'share_network_id': share_network_id}) + db.share_network_subnet_get.assert_called_once_with( + self.context, share_network_subnet_id) + self.share_manager._form_server_setup_info.assert_called_once_with( + self.context, share_servers[0], share_network, share_network_subnet + ) + db.share_instances_get_all_by_share_server.assert_called_once_with( + self.context, share_servers[0]['id'], with_share_data=True) + db.share_access_get_all_for_instance.assert_called_once_with( + self.context, share_instances[0]['id']) + (self.share_manager.driver.check_update_share_server_security_service. + assert_called_once_with( + self.context, share_servers[0], network_info, + share_instances, + expected_instance_rules, + security_services[0], + current_security_service=security_services[1])) + self.assertEqual(result, False) + + def test__update_share_network_security_service_exception(self): + security_services = [ + db_utils.create_security_service() for i in range(2)] + share_network = db_utils.create_share_network() + share_network_subnet = db_utils.create_share_network_subnet() + share_servers = [ + db_utils.create_share_server( + share_network_subnet_id=share_network_subnet['id'])] + share_instances = [db_utils.create_share_instance(share_id='fake')] + share_instance_ids = [instance['id'] for instance in share_instances] + security_services_effect = mock.Mock(side_effect=security_services) + share_network_id = share_network['id'] + current_security_service_id = security_services[0]['id'] + new_security_service_id = security_services[1]['id'] + share_network_subnet_id = share_servers[0]['share_network_subnet_id'] + network_info = {'fake': 'fake'} + backend_details_keys = [ + 'name', 'ou', 'domain', 'server', 'dns_ip', 'user', 'type', + 'password'] + backend_details_data = {} + [backend_details_data.update( + {key: security_services[0][key]}) for key in backend_details_keys] + backend_details_exp_update = { + 'security_service_' + security_services[0]['type']: + jsonutils.dumps(backend_details_data) + } + driver_exception = mock.Mock(side_effect=Exception()) + share_instances = [db_utils.create_share()['instance']] + fake_rules = ['fake_rules'] + expected_instance_rules = [{ + 'share_instance_id': share_instances[0]['id'], + 'access_rules': fake_rules + }] + + self._setup_mocks_for_sec_service_update( + security_services_effect, share_network, share_servers, + share_network_subnet, network_info, share_instances, fake_rules, + driver_update_action=driver_exception) + + self.mock_object( + self.share_manager.access_helper, + 'update_share_instances_access_rules_status') + self.mock_object( + db, 'share_instances_get_all_by_share_server', + mock.Mock(return_value=share_instances)) + + self.share_manager._update_share_network_security_service( + self.context, share_network_id, new_security_service_id, + current_security_service_id=current_security_service_id) + + db.security_service_get.assert_has_calls( + [mock.call(self.context, security_services[1]['id']), + mock.call(self.context, security_services[0]['id'])] + ) + db.share_network_get.assert_called_once_with( + self.context, share_network_id) + db.share_server_get_all_by_host.assert_called_once_with( + self.context, self.share_manager.host, + filters={'share_network_id': share_network_id}) + db.share_network_subnet_get.assert_called_once_with( + self.context, share_network_subnet_id) + self.share_manager._form_server_setup_info.assert_called_once_with( + self.context, share_servers[0], share_network, share_network_subnet + ) + (self.share_manager.driver.update_share_server_security_service. + assert_called_once_with( + self.context, share_servers[0], network_info, + share_instances, + expected_instance_rules, + security_services[0], + current_security_service=security_services[1])) + db.share_server_backend_details_set.assert_called_once_with( + self.context, share_servers[0]['id'], + backend_details_exp_update) + db.share_server_update.assert_called_once_with( + self.context, share_servers[0]['id'], + {'status': constants.STATUS_ERROR}) + db.share_instances_get_all_by_share_server.assert_called_once_with( + self.context, share_servers[0]['id'], with_share_data=True) + db.share_access_get_all_for_instance.assert_called_once_with( + self.context, share_instances[0]['id']) + (self.share_manager.access_helper. + update_share_instances_access_rules_status( + self.context, constants.SHARE_INSTANCE_RULES_ERROR, + share_instance_ids)) + (self.share_manager.access_helper. + get_and_update_share_instance_access_rules( + self.context, updates={'state': constants.STATUS_ERROR}, + share_instance_id=share_instances[0]['id'])) + + def test_update_share_network_security_service(self): + share_network_id = 'fake_sn_id' + new_security_service_id = 'new_sec_service_id' + current_security_service_id = 'current_sec_service_id' + + self.mock_object( + self.share_manager, '_update_share_network_security_service') + + self.share_manager.update_share_network_security_service( + self.context, share_network_id, new_security_service_id, + current_security_service_id=current_security_service_id) + (self.share_manager._update_share_network_security_service. + assert_called_once_with( + self.context, share_network_id, new_security_service_id, + current_security_service_id=current_security_service_id, + check_only=False)) + + def test_check_update_share_network_security_service(self): + share_network_id = 'fake_sn_id' + new_security_service_id = 'new_sec_service_id' + current_security_service_id = 'current_sec_service_id' + + self.mock_object( + self.share_manager, '_update_share_network_security_service') + + self.share_manager.check_update_share_network_security_service( + self.context, share_network_id, new_security_service_id, + current_security_service_id=current_security_service_id) + (self.share_manager._update_share_network_security_service. + assert_called_once_with( + self.context, share_network_id, new_security_service_id, + current_security_service_id=current_security_service_id, + check_only=True)) + @ddt.ddt class HookWrapperTestCase(test.TestCase): diff --git a/manila/tests/share/test_rpcapi.py b/manila/tests/share/test_rpcapi.py index 9a73378077..41f6f5af7d 100644 --- a/manila/tests/share/test_rpcapi.py +++ b/manila/tests/share/test_rpcapi.py @@ -457,3 +457,22 @@ class ShareRpcAPITestCase(test.TestCase): dest_host=self.fake_host, share_instance_ids=[self.fake_share['instance']['id']], share_server_id=self.fake_share_server['id']) + + def test_update_share_network_security_service(self): + self._test_share_api( + 'update_share_network_security_service', + rpc_method='cast', + version='1.22', + dest_host=self.fake_host, + share_network_id='fake_net_id', + new_security_service_id='fake_sec_service_id', + current_security_service_id='fake_sec_service_id') + + def test_check_update_share_network_security_service(self): + self._test_share_api('check_update_share_network_security_service', + rpc_method='cast', + version='1.22', + dest_host=self.fake_host, + share_network_id='fake_net_id', + new_security_service_id='fake_sec_service_id', + current_security_service_id='fake_sec_service_id') diff --git a/manila/tests/share_group/test_api.py b/manila/tests/share_group/test_api.py index 4773a30c47..48e18c15ec 100644 --- a/manila/tests/share_group/test_api.py +++ b/manila/tests/share_group/test_api.py @@ -21,6 +21,7 @@ from unittest import mock import ddt from oslo_config import cfg from oslo_utils import timeutils +from webob import exc as webob_exc from manila.common import constants from manila import context @@ -460,6 +461,10 @@ class ShareGroupsAPITestCase(test.TestCase): host='fake_original_host', share_network_id='fake_network_id', share_server_id='fake_server_id') + share_network = { + 'id': 'fakeid', + 'status': constants.STATUS_NETWORK_ACTIVE + } expected_values = share_group.copy() for name in ('id', 'created_at', 'share_network_id', 'share_server_id'): @@ -484,7 +489,8 @@ class ShareGroupsAPITestCase(test.TestCase): self.mock_object( share_types, 'get_share_type', mock.Mock(return_value={"id": self.fake_share_type['id']})) - self.mock_object(db_driver, 'share_network_get') + self.mock_object(db_driver, 'share_network_get', + mock.Mock(return_value=share_network)) self.mock_object( db_driver, 'share_group_snapshot_members_get_all', mock.Mock(return_value=[])) @@ -502,6 +508,44 @@ class ShareGroupsAPITestCase(test.TestCase): self.context, share_group_api.QUOTAS.reserve.return_value) share_group_api.QUOTAS.rollback.assert_not_called() + def test_create_share_group_network_not_active(self): + fake_share_type_mapping = {'share_type_id': self.fake_share_type['id']} + share_group = fake_share_group( + 'fakeid', user_id=self.context.user_id, + project_id=self.context.project_id, + share_types=[fake_share_type_mapping], + status=constants.STATUS_CREATING, + host='fake_original_host', + share_network_id='fake_network_id', + share_server_id='fake_server_id') + network_id = 'fake_sn' + share_network = { + 'id': network_id, + 'status': constants.STATUS_SERVER_NETWORK_CHANGE + } + expected_values = share_group.copy() + for name in ('id', 'created_at', 'share_network_id', + 'share_server_id'): + expected_values.pop(name, None) + expected_values['share_types'] = [self.fake_share_type['id']] + expected_values['share_network_id'] = 'fake_network_id' + expected_values['share_server_id'] = 'fake_server_id' + + self.mock_object( + share_types, 'get_share_type', + mock.Mock(return_value={"id": self.fake_share_type['id']})) + self.mock_object(db_driver, 'share_network_get', + mock.Mock(return_value=share_network)) + + self.assertRaises( + webob_exc.HTTPBadRequest, + self.api.create, + self.context, share_type_ids=[fake_share_type_mapping], + share_network_id="fake_sn") + + db_driver.share_network_get.assert_called_once_with( + self.context, network_id) + def test_create_with_source_share_group_snapshot_id_with_member(self): snap = fake_share_group_snapshot( "fake_source_share_group_snapshot_id", @@ -524,6 +568,10 @@ class ShareGroupsAPITestCase(test.TestCase): share_network_id='fake_network_id', share_server_id='fake_server_id') expected_values = share_group.copy() + share_network = { + 'id': 'fakeid', + 'status': constants.STATUS_NETWORK_ACTIVE + } for name in ('id', 'created_at', 'fake_network_id', 'fake_share_server_id'): expected_values.pop(name, None) @@ -547,7 +595,8 @@ class ShareGroupsAPITestCase(test.TestCase): self.mock_object( share_types, 'get_share_type', mock.Mock(return_value={"id": self.fake_share_type['id']})) - self.mock_object(db_driver, 'share_network_get') + self.mock_object(db_driver, 'share_network_get', + mock.Mock(return_value=share_network)) self.mock_object( db_driver, 'share_instance_get', mock.Mock(return_value=share)) self.mock_object( @@ -593,6 +642,10 @@ class ShareGroupsAPITestCase(test.TestCase): status=constants.STATUS_CREATING, share_network_id='fake_network_id', share_server_id='fake_server_id') + share_network = { + 'id': 'fakeid', + 'status': constants.STATUS_NETWORK_ACTIVE + } expected_values = share_group.copy() for name in ('id', 'created_at', 'share_network_id', 'share_server_id'): @@ -606,7 +659,8 @@ class ShareGroupsAPITestCase(test.TestCase): mock.Mock(return_value=snap)) self.mock_object(db_driver, 'share_group_get', mock.Mock(return_value=orig_share_group)) - self.mock_object(db_driver, 'share_network_get') + self.mock_object(db_driver, 'share_network_get', + mock.Mock(return_value=share_network)) self.mock_object(db_driver, 'share_instance_get', mock.Mock(return_value=share)) self.mock_object(db_driver, 'share_group_create', diff --git a/releasenotes/notes/add-update-security-service-for-in-use-share-networks-c60d82898c71eb4a.yaml b/releasenotes/notes/add-update-security-service-for-in-use-share-networks-c60d82898c71eb4a.yaml new file mode 100644 index 0000000000..339dcb241e --- /dev/null +++ b/releasenotes/notes/add-update-security-service-for-in-use-share-networks-c60d82898c71eb4a.yaml @@ -0,0 +1,20 @@ +--- +features: + - | + Added the possibility to add and update an entire security service when + a share network is already being used. + A new field called ``status`` was added to the share network model and its + default value is ``active``. Some operations might be blocked depending on + the share network status. + A boolean field called ``security_service_update_support`` was added to the + share server's model. This field defaults to ``False``, and all of the + already deployed share servers are going to get the default value even if + their backend support it. Administrators will be able to update the field + value using ``manila-manage`` commands. + The scheduler will filter out backend that does not handle this request + during some operations. +upgrade: + - | + ``manila-manage`` now supports share server commands, which allow + administrators to modify the field value of some share server's + capabilities.