API for ensure shares

Introduces a new API to allow OpenStack administrators to start
the share manager's ensure shares operation.

Also, introduced a new configuration option named
`update_shares_status_on_ensure`, so administrators can define
whether the shares status should be updated during the ensure
shares operation or not.

A new column was added to the services table, named `ensuring`.
Through this field, we can identify whether there is an undergoing
ensure shares operation or not.

Closes-Bug: #1996793
Partially-Implements: bp ensure-shares-api
Change-Id: If7bf059eb8581f20a3ceb7c1af93558774f4ef5e
This commit is contained in:
Carlos Eduardo 2024-07-16 16:01:34 -03:00
parent 4bf505404a
commit 101becf9b4
20 changed files with 556 additions and 34 deletions

View File

@ -203,13 +203,14 @@ REST_API_VERSION_HISTORY = """
* 2.83 - Added 'disabled_reason' field to services. * 2.83 - Added 'disabled_reason' field to services.
* 2.84 - Added mount_point_name to shares. * 2.84 - Added mount_point_name to shares.
* 2.85 - Added backup_type field to share backups. * 2.85 - Added backup_type field to share backups.
* 2.86 - Add ensure share API.
""" """
# The minimum and maximum versions of the API supported # The minimum and maximum versions of the API supported
# The default api version request is defined to be the # The default api version request is defined to be the
# minimum version of the API supported. # minimum version of the API supported.
_MIN_API_VERSION = "2.0" _MIN_API_VERSION = "2.0"
_MAX_API_VERSION = "2.85" _MAX_API_VERSION = "2.86"
DEFAULT_API_VERSION = _MIN_API_VERSION DEFAULT_API_VERSION = _MIN_API_VERSION

View File

@ -458,3 +458,7 @@ user documentation.
2.85 2.85
---- ----
Added ``backup_type`` field to share backup object. Added ``backup_type`` field to share backup object.
2.86
----
Added ensure shares API.

View File

@ -102,6 +102,13 @@ class APIRouter(manila.api.openstack.APIRouter):
mapper.resource("service", mapper.resource("service",
"services", "services",
controller=self.resources["services"]) controller=self.resources["services"])
for path_prefix in ['/{project_id}', '']:
# project_id is optional
mapper.connect("services",
"%s/services/ensure-shares" % path_prefix,
controller=self.resources["services"],
action="ensure_shares",
conditions={"method": ["POST"]})
self.resources["quota_sets_legacy"] = ( self.resources["quota_sets_legacy"] = (
quota_sets.create_resource_legacy()) quota_sets.create_resource_legacy())

View File

@ -14,13 +14,17 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import http.client as http_client
from oslo_utils import strutils from oslo_utils import strutils
import webob.exc import webob.exc
from manila.api.openstack import wsgi from manila.api.openstack import wsgi
from manila.api.views import services as services_views from manila.api.views import services as services_views
from manila import db from manila import db
from manila import exception
from manila.i18n import _ from manila.i18n import _
from manila import policy
from manila.services import api as service_api
class ServiceMixin(object): class ServiceMixin(object):
@ -34,7 +38,7 @@ class ServiceMixin(object):
_view_builder_class = services_views.ViewBuilder _view_builder_class = services_views.ViewBuilder
@wsgi.Controller.authorize("index") @wsgi.Controller.authorize("index")
def _index(self, req): def _index(self, req, support_ensure_shares=False):
"""Return a list of all running services.""" """Return a list of all running services."""
context = req.environ['manila.context'] context = req.environ['manila.context']
@ -42,7 +46,7 @@ class ServiceMixin(object):
services = [] services = []
for service in all_services: for service in all_services:
service = { service_data = {
'id': service['id'], 'id': service['id'],
'binary': service['binary'], 'binary': service['binary'],
'host': service['host'], 'host': service['host'],
@ -52,7 +56,9 @@ class ServiceMixin(object):
'state': service['state'], 'state': service['state'],
'updated_at': service['updated_at'], 'updated_at': service['updated_at'],
} }
services.append(service) if support_ensure_shares:
service_data['ensuring'] = service['ensuring']
services.append(service_data)
search_opts = [ search_opts = [
'host', 'host',
@ -141,10 +147,18 @@ class ServiceController(ServiceMixin, wsgi.Controller):
Registered under API URL 'services'. Registered under API URL 'services'.
""" """
@wsgi.Controller.api_version('2.7') def __init__(self):
super().__init__()
self.service_api = service_api.API()
@wsgi.Controller.api_version('2.7', '2.85')
def index(self, req): def index(self, req):
return self._index(req) return self._index(req)
@wsgi.Controller.api_version('2.86') # noqa
def index(self, req): # pylint: disable=function-redefined # noqa F811
return self._index(req, support_ensure_shares=True)
@wsgi.Controller.api_version('2.7', '2.82') @wsgi.Controller.api_version('2.7', '2.82')
def update(self, req, id, body): def update(self, req, id, body):
return self._update(req, id, body, support_disabled_reason=False) return self._update(req, id, body, support_disabled_reason=False)
@ -153,6 +167,32 @@ class ServiceController(ServiceMixin, wsgi.Controller):
def update(self, req, id, body): # pylint: disable=function-redefined # noqa F811 def update(self, req, id, body): # pylint: disable=function-redefined # noqa F811
return self._update(req, id, body) return self._update(req, id, body)
@wsgi.Controller.api_version('2.86')
@wsgi.Controller.authorize
def ensure_shares(self, req, body):
"""Starts ensure shares for a given manila-share binary."""
context = req.environ['manila.context']
policy.check_policy(context, 'service', 'ensure_shares')
host = body.get('host', None)
if not host:
raise webob.exc.HTTPBadRequest('Missing host parameter.')
try:
# The only binary supported is Manila share.
service = db.service_get_by_args(context, host, 'manila-share')
except exception.NotFound:
raise webob.exc.HTTPNotFound(
"manila-share binary for '%s' host not found" % id
)
try:
self.service_api.ensure_shares(context, service, host)
except webob.exc.HTTPConflict:
raise
return webob.Response(status_int=http_client.ACCEPTED)
def create_resource_legacy(): def create_resource_legacy():
return wsgi.Resource(ServiceControllerLegacy()) return wsgi.Resource(ServiceControllerLegacy())

View File

@ -21,6 +21,7 @@ class ViewBuilder(common.ViewBuilder):
_collection_name = "services" _collection_name = "services"
_detail_version_modifiers = [ _detail_version_modifiers = [
"add_disabled_reason_field", "add_disabled_reason_field",
"add_ensuring_field",
] ]
def summary(self, request, service): def summary(self, request, service):
@ -49,3 +50,7 @@ class ViewBuilder(common.ViewBuilder):
service_dict.pop('disabled', None) service_dict.pop('disabled', None)
service_dict['status'] = service.get('status') service_dict['status'] = service.get('status')
service_dict['disabled_reason'] = service.get('disabled_reason') service_dict['disabled_reason'] = service.get('disabled_reason')
@common.ViewBuilder.versioned_method("2.86")
def add_ensuring_field(self, context, service_dict, service):
service_dict['ensuring'] = service.get('ensuring')

View File

@ -147,6 +147,11 @@ global_opts = [
'(element of the list is <driver_updatable_key>, ' '(element of the list is <driver_updatable_key>, '
'i.e max_files) can be passed to share drivers as part ' 'i.e max_files) can be passed to share drivers as part '
'of metadata create/update operations.'), 'of metadata create/update operations.'),
cfg.BoolOpt('update_shares_status_on_ensure',
default=True,
help='Whether Manila should update the status of all shares '
'within a backend during ongoing ensure_shares '
'run.'),
] ]
CONF.register_opts(global_opts) CONF.register_opts(global_opts)

View File

@ -53,6 +53,7 @@ STATUS_AWAITING_TRANSFER = 'awaiting_transfer'
STATUS_BACKUP_CREATING = 'backup_creating' STATUS_BACKUP_CREATING = 'backup_creating'
STATUS_BACKUP_RESTORING = 'backup_restoring' STATUS_BACKUP_RESTORING = 'backup_restoring'
STATUS_BACKUP_RESTORING_ERROR = 'backup_restoring_error' STATUS_BACKUP_RESTORING_ERROR = 'backup_restoring_error'
STATUS_ENSURING = 'ensuring'
# Transfer resource type # Transfer resource type
SHARE_RESOURCE_TYPE = 'share' SHARE_RESOURCE_TYPE = 'share'
@ -144,6 +145,7 @@ TRANSITIONAL_STATUSES = (
STATUS_RESTORING, STATUS_REVERTING, STATUS_RESTORING, STATUS_REVERTING,
STATUS_SERVER_MIGRATING, STATUS_SERVER_MIGRATING_TO, STATUS_SERVER_MIGRATING, STATUS_SERVER_MIGRATING_TO,
STATUS_BACKUP_RESTORING, STATUS_BACKUP_CREATING, STATUS_BACKUP_RESTORING, STATUS_BACKUP_CREATING,
STATUS_ENSURING,
) )
INVALID_SHARE_INSTANCE_STATUSES_FOR_ACCESS_RULE_UPDATES = ( INVALID_SHARE_INSTANCE_STATUSES_FOR_ACCESS_RULE_UPDATES = (

View File

@ -0,0 +1,48 @@
# 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-ensuring-field-to-services
Revision ID: cdefa6287df8
Revises: 2f27d904214c
Create Date: 2024-07-15 14:29:16.733696
"""
# revision identifiers, used by Alembic.
revision = 'cdefa6287df8'
down_revision = '2f27d904214c'
from alembic import op
from oslo_log import log
import sqlalchemy as sa
LOG = log.getLogger(__name__)
def upgrade():
try:
op.add_column('services', sa.Column(
'ensuring', sa.Boolean,
nullable=False, server_default=sa.sql.false()))
except Exception:
LOG.error("Column services.ensuring not created!")
raise
def downgrade():
try:
op.drop_column('services', 'ensuring')
except Exception:
LOG.error("Column shares.ensuring not dropped!")
raise

View File

@ -72,6 +72,7 @@ class Service(BASE, ManilaBase):
availability_zone_id = Column(String(36), availability_zone_id = Column(String(36),
ForeignKey('availability_zones.id'), ForeignKey('availability_zones.id'),
nullable=True) nullable=True)
ensuring = Column(Boolean, default=False)
availability_zone = orm.relationship( availability_zone = orm.relationship(
"AvailabilityZone", "AvailabilityZone",

View File

@ -34,6 +34,12 @@ deprecated_service_update = policy.DeprecatedRule(
deprecated_reason=DEPRECATED_REASON, deprecated_reason=DEPRECATED_REASON,
deprecated_since=versionutils.deprecated.WALLABY deprecated_since=versionutils.deprecated.WALLABY
) )
deprecated_service_ensure = policy.DeprecatedRule(
name=BASE_POLICY_NAME % 'ensure_shares',
check_str=base.RULE_ADMIN_API,
deprecated_reason=DEPRECATED_REASON,
deprecated_since='2024.2/Dalmatian'
)
service_policies = [ service_policies = [
@ -79,6 +85,19 @@ service_policies = [
], ],
deprecated_rule=deprecated_service_update deprecated_rule=deprecated_service_update
), ),
policy.DocumentedRuleDefault(
name=BASE_POLICY_NAME % 'ensure_shares',
check_str=base.ADMIN,
scope_types=['project'],
description="Run ensure shares for a manila-share binary.",
operations=[
{
'method': 'POST',
'path': '/services/ensure',
}
],
deprecated_rule=deprecated_service_ensure
),
] ]

View File

38
manila/services/api.py Normal file
View File

@ -0,0 +1,38 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from oslo_config import cfg
from webob import exc
from manila.db import base
from manila.share import rpcapi as share_rpcapi
CONF = cfg.CONF
class API(base.Base):
"""API for handling service actions."""
def __init__(self):
super(API, self).__init__()
self.share_rpcapi = share_rpcapi.ShareAPI()
def ensure_shares(self, context, service, host):
"""Start the ensure shares in a given host."""
if service['state'] != "up":
raise exc.HTTPConflict(
"The service must have its state set to 'up' prior to running "
"ensure shares.")
self.share_rpcapi.ensure_driver_resources(context, host)

View File

@ -264,7 +264,7 @@ def add_hooks(f):
class ShareManager(manager.SchedulerDependentManager): class ShareManager(manager.SchedulerDependentManager):
"""Manages NAS storages.""" """Manages NAS storages."""
RPC_API_VERSION = '1.28' RPC_API_VERSION = '1.29'
def __init__(self, share_driver=None, service_name=None, *args, **kwargs): def __init__(self, share_driver=None, service_name=None, *args, **kwargs):
"""Load the driver from args, or from flags.""" """Load the driver from args, or from flags."""
@ -401,7 +401,8 @@ class ShareManager(manager.SchedulerDependentManager):
""" """
return self.driver.initialized return self.driver.initialized
def ensure_driver_resources(self, ctxt): def ensure_driver_resources(self, ctxt, skip_backend_info_check=False):
update_instances_status = CONF.update_shares_status_on_ensure
old_backend_info = self.db.backend_info_get(ctxt, self.host) old_backend_info = self.db.backend_info_get(ctxt, self.host)
old_backend_info_hash = (old_backend_info.get('info_hash') old_backend_info_hash = (old_backend_info.get('info_hash')
if old_backend_info is not None else None) if old_backend_info is not None else None)
@ -409,31 +410,33 @@ class ShareManager(manager.SchedulerDependentManager):
new_backend_info_hash = None new_backend_info_hash = None
backend_info_implemented = True backend_info_implemented = True
update_share_instances = [] update_share_instances = []
try: if not skip_backend_info_check:
new_backend_info = self.driver.get_backend_info(ctxt) try:
except Exception as e: new_backend_info = self.driver.get_backend_info(ctxt)
if not isinstance(e, NotImplementedError): except Exception as e:
LOG.exception( if not isinstance(e, NotImplementedError):
"The backend %(host)s could not get backend info.", LOG.exception(
{'host': self.host}) "The backend %(host)s could not get backend info.",
raise {'host': self.host})
else: raise
backend_info_implemented = False else:
LOG.debug( backend_info_implemented = False
("The backend %(host)s does not support get backend" LOG.debug(
" info method."), ("The backend %(host)s does not support get backend"
{'host': self.host}) " info method."),
{'host': self.host})
if new_backend_info: if new_backend_info:
new_backend_info_hash = hashlib.sha1(str( new_backend_info_hash = hashlib.sha1(
sorted(new_backend_info.items())).encode('utf-8')).hexdigest() str(sorted(new_backend_info.items())).encode(
if (old_backend_info_hash == new_backend_info_hash and 'utf-8')).hexdigest()
backend_info_implemented): if ((old_backend_info_hash == new_backend_info_hash and
LOG.debug( backend_info_implemented) and not skip_backend_info_check):
("Ensure shares is being skipped because the %(host)s's old " LOG.debug(
"backend info is the same as its new backend info."), ("Ensure shares is being skipped because the %(host)s's "
{'host': self.host}) "old backend info is the same as its new backend info."),
return {'host': self.host})
return
share_instances = self.db.share_instance_get_all_by_host( share_instances = self.db.share_instance_get_all_by_host(
ctxt, self.host) ctxt, self.host)
@ -467,7 +470,19 @@ class ShareManager(manager.SchedulerDependentManager):
ctxt, share_instance) ctxt, share_instance)
update_share_instances.append(share_instance_dict) update_share_instances.append(share_instance_dict)
do_service_status_update = False
if update_share_instances: if update_share_instances:
# No reason to update the shares status if nothing will be done.
do_service_status_update = True
service = self.db.service_get_by_args(
ctxt, self.host, 'manila-share')
self.db.service_update(ctxt, service['id'], {'ensuring': True})
if update_instances_status:
for instance in update_share_instances:
self.db.share_instance_update(
ctxt, instance['id'],
{'status': constants.STATUS_ENSURING}
)
try: try:
update_share_instances = self.driver.ensure_shares( update_share_instances = self.driver.ensure_shares(
ctxt, update_share_instances) or {} ctxt, update_share_instances) or {}
@ -494,10 +509,11 @@ class ShareManager(manager.SchedulerDependentManager):
share_instance_update_dict = ( share_instance_update_dict = (
update_share_instances[share_instance['id']] update_share_instances[share_instance['id']]
) )
if share_instance_update_dict.get('status'): backend_provided_status = share_instance_update_dict.get('status')
if backend_provided_status:
self.db.share_instance_update( self.db.share_instance_update(
ctxt, share_instance['id'], ctxt, share_instance['id'],
{'status': share_instance_update_dict.get('status'), {'status': backend_provided_status,
'host': share_instance['host']} 'host': share_instance['host']}
) )
metadata_updates = share_instance_update_dict.get('metadata') metadata_updates = share_instance_update_dict.get('metadata')
@ -568,6 +584,13 @@ class ShareManager(manager.SchedulerDependentManager):
"Unexpected error occurred while updating " "Unexpected error occurred while updating "
"access rules for snapshot instance %s.", "access rules for snapshot instance %s.",
snap_instance['id']) snap_instance['id'])
if not backend_provided_status and update_instances_status:
self.db.share_instance_update(
ctxt, share_instance['id'],
{'status': constants.STATUS_AVAILABLE}
)
if do_service_status_update:
self.db.service_update(ctxt, service['id'], {'ensuring': False})
def _ensure_share(self, ctxt, share_instance): def _ensure_share(self, ctxt, share_instance):
export_locations = None export_locations = None

View File

@ -89,6 +89,7 @@ class ShareAPI(object):
restore_backup() methods restore_backup() methods
1.27 - Update delete_share_instance() and delete_snapshot() methods 1.27 - Update delete_share_instance() and delete_snapshot() methods
1.28 - Add update_share_from_metadata() method 1.28 - Add update_share_from_metadata() method
1.29 - Add ensure_shares()
""" """
BASE_RPC_API_VERSION = '1.0' BASE_RPC_API_VERSION = '1.0'
@ -97,7 +98,7 @@ class ShareAPI(object):
super(ShareAPI, self).__init__() super(ShareAPI, self).__init__()
target = messaging.Target(topic=CONF.share_topic, target = messaging.Target(topic=CONF.share_topic,
version=self.BASE_RPC_API_VERSION) version=self.BASE_RPC_API_VERSION)
self.client = rpc.get_client(target, version_cap='1.28') self.client = rpc.get_client(target, version_cap='1.29')
def create_share_instance(self, context, share_instance, host, def create_share_instance(self, context, share_instance, host,
request_spec, filter_properties, request_spec, filter_properties,
@ -540,3 +541,12 @@ class ShareAPI(object):
'update_share_from_metadata', 'update_share_from_metadata',
share_id=share['id'], share_id=share['id'],
metadata=metadata) metadata=metadata)
def ensure_driver_resources(self, context, host):
host = utils.extract_host(host)
call_context = self.client.prepare(server=host, version='1.29')
return call_context.cast(
context,
'ensure_driver_resources',
skip_backend_info_check=True
)

View File

@ -17,6 +17,7 @@
import datetime import datetime
from unittest import mock from unittest import mock
import webob
import ddt import ddt
from oslo_utils import timeutils from oslo_utils import timeutils
@ -158,6 +159,7 @@ fake_response_service_list_with_disabled_reason = {'services': [
'updated_at': datetime.datetime(2012, 9, 18, 8, 3, 38), 'updated_at': datetime.datetime(2012, 9, 18, 8, 3, 38),
}, },
]} ]}
ENSURE_SHARES_VERSION = "2.86"
def fake_service_get_all(context): def fake_service_get_all(context):
@ -386,3 +388,99 @@ class ServicesTest(test.TestCase):
self.assertRaises( self.assertRaises(
exception.VersionNotFoundForAPIMethod, controller().index, req) exception.VersionNotFoundForAPIMethod, controller().index, req)
def test_ensure_shares_no_host_param(self):
req = fakes.HTTPRequest.blank(
'/fooproject/services/ensure', version=ENSURE_SHARES_VERSION)
body = {}
self.assertRaises(
webob.exc.HTTPBadRequest,
self.controller.ensure_shares,
req,
body
)
def test_ensure_shares_host_not_found(self):
req = fakes.HTTPRequest.blank(
'/fooproject/services/ensure', version=ENSURE_SHARES_VERSION)
req_context = req.environ['manila.context']
body = {'host': 'host1'}
mock_service_get = self.mock_object(
db, 'service_get_by_args',
mock.Mock(side_effect=exception.NotFound())
)
self.assertRaises(
webob.exc.HTTPNotFound,
self.controller.ensure_shares,
req,
body
)
mock_service_get.assert_called_once_with(
req_context,
body['host'],
'manila-share'
)
def test_ensure_shares_conflict(self):
req = fakes.HTTPRequest.blank(
'/fooproject/services/ensure', version=ENSURE_SHARES_VERSION)
req_context = req.environ['manila.context']
body = {'host': 'host1'}
fake_service = {'id': 'fake_service_id'}
mock_service_get = self.mock_object(
db,
'service_get_by_args',
mock.Mock(return_value=fake_service)
)
mock_ensure = self.mock_object(
self.controller.service_api,
'ensure_shares',
mock.Mock(side_effect=webob.exc.HTTPConflict)
)
self.assertRaises(
webob.exc.HTTPConflict,
self.controller.ensure_shares,
req,
body
)
mock_service_get.assert_called_once_with(
req_context,
body['host'],
'manila-share'
)
mock_ensure.assert_called_once_with(
req_context, fake_service, body['host']
)
def test_ensure_shares(self):
req = fakes.HTTPRequest.blank(
'/fooproject/services/ensure', version=ENSURE_SHARES_VERSION)
req_context = req.environ['manila.context']
body = {'host': 'host1'}
fake_service = {'id': 'fake_service_id'}
mock_service_get = self.mock_object(
db,
'service_get_by_args',
mock.Mock(return_value=fake_service)
)
mock_ensure = self.mock_object(
self.controller.service_api, 'ensure_shares',
)
response = self.controller.ensure_shares(req, body)
self.assertEqual(202, response.status_int)
mock_service_get.assert_called_once_with(
req_context,
body['host'],
'manila-share'
)
mock_ensure.assert_called_once_with(
req_context, fake_service, body['host']
)

View File

View File

@ -0,0 +1,61 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from unittest import mock
from webob import exc
from manila import context
from manila.services import api as service_api
from manila import test
class ServicesApiTest(test.TestCase):
def setUp(self):
super(ServicesApiTest, self).setUp()
self.context = context.get_admin_context()
self.share_rpcapi = mock.Mock()
self.share_rpcapi.ensure_shares = mock.Mock()
self.services_api = service_api.API()
self.mock_object(
self.services_api, 'share_rpcapi', self.share_rpcapi
)
def test_ensure_shares(self):
host = 'fake_host@fakebackend'
fake_service = {
'id': 'fake_service_id',
'state': 'up'
}
self.services_api.ensure_shares(self.context, fake_service, host)
self.share_rpcapi.ensure_driver_resources.assert_called_once_with(
self.context, host
)
def test_ensure_shares_host_down(self):
host = 'fake_host@fakebackend'
fake_service = {
'id': 'fake_service_id',
'state': 'down'
}
self.assertRaises(
exc.HTTPConflict,
self.services_api.ensure_shares,
self.context,
fake_service,
host
)
self.share_rpcapi.ensure_shares.assert_not_called()

View File

@ -208,6 +208,11 @@ class ShareManagerTestCase(test.TestCase):
'reapply_access_rules': driver_needs_to_reapply_rules, 'reapply_access_rules': driver_needs_to_reapply_rules,
}, },
} }
fake_service = {'id': 'fake_service_id', 'binary': 'manila-share'}
self.mock_object(self.share_manager.db,
'service_get_by_args',
mock.Mock(return_value=fake_service))
self.mock_object(self.share_manager.db, 'service_update')
mock_backend_info_update = self.mock_object( mock_backend_info_update = self.mock_object(
self.share_manager.db, 'backend_info_update') self.share_manager.db, 'backend_info_update')
mock_share_get_all_by_host = self.mock_object( mock_share_get_all_by_host = self.mock_object(
@ -263,6 +268,23 @@ class ShareManagerTestCase(test.TestCase):
mock.call(utils.IsAMatcher(context.RequestContext), mock.call(utils.IsAMatcher(context.RequestContext),
instances[2]), instances[2]),
]) ])
self.share_manager.db.service_get_by_args.assert_called_once_with(
utils.IsAMatcher(context.RequestContext),
self.share_manager.host,
'manila-share'
)
self.share_manager.db.service_update.assert_has_calls([
mock.call(
utils.IsAMatcher(context.RequestContext),
fake_service['id'],
{'ensuring': True}
),
mock.call(
utils.IsAMatcher(context.RequestContext),
fake_service['id'],
{'ensuring': False}
)
])
if driver_needs_to_reapply_rules: if driver_needs_to_reapply_rules:
# don't care if share_instance['access_rules_status'] is "syncing" # don't care if share_instance['access_rules_status'] is "syncing"
mock_reset_rules_method.assert_has_calls([ mock_reset_rules_method.assert_has_calls([
@ -301,6 +323,11 @@ class ShareManagerTestCase(test.TestCase):
'metadata': metadata_updates, 'metadata': metadata_updates,
}, },
} }
fake_service = {'id': 'fake_service_id', 'binary': 'manila-share'}
self.mock_object(self.share_manager.db,
'service_get_by_args',
mock.Mock(return_value=fake_service))
self.mock_object(self.share_manager.db, 'service_update')
mock_backend_info_update = self.mock_object( mock_backend_info_update = self.mock_object(
self.share_manager.db, 'backend_info_update') self.share_manager.db, 'backend_info_update')
mock_share_get_all_by_host = self.mock_object( mock_share_get_all_by_host = self.mock_object(
@ -359,6 +386,23 @@ class ShareManagerTestCase(test.TestCase):
]) ])
# none of the share instances in the fake data have syncing rules # none of the share instances in the fake data have syncing rules
mock_reset_rules_method.assert_not_called() mock_reset_rules_method.assert_not_called()
self.share_manager.db.service_get_by_args.assert_called_once_with(
utils.IsAMatcher(context.RequestContext),
self.share_manager.host,
'manila-share'
)
self.share_manager.db.service_update.assert_has_calls([
mock.call(
utils.IsAMatcher(context.RequestContext),
fake_service['id'],
{'ensuring': True}
),
mock.call(
utils.IsAMatcher(context.RequestContext),
fake_service['id'],
{'ensuring': False}
)
])
def test_init_host_with_no_shares(self): def test_init_host_with_no_shares(self):
self.mock_object(self.share_manager.db, self.mock_object(self.share_manager.db,
@ -520,6 +564,7 @@ class ShareManagerTestCase(test.TestCase):
} }
instances[0]['access_rules_status'] = '' instances[0]['access_rules_status'] = ''
instances[2]['access_rules_status'] = '' instances[2]['access_rules_status'] = ''
fake_service = {'id': 'fake_service_id', 'binary': 'manila-share'}
self.mock_object(self.share_manager.db, self.mock_object(self.share_manager.db,
'backend_info_get', 'backend_info_get',
mock.Mock(return_value=old_backend_info)) mock.Mock(return_value=old_backend_info))
@ -530,6 +575,10 @@ class ShareManagerTestCase(test.TestCase):
mock_share_get_all_by_host = self.mock_object( mock_share_get_all_by_host = self.mock_object(
self.share_manager.db, 'share_instance_get_all_by_host', self.share_manager.db, 'share_instance_get_all_by_host',
mock.Mock(return_value=instances)) mock.Mock(return_value=instances))
self.mock_object(self.share_manager.db,
'service_get_by_args',
mock.Mock(return_value=fake_service))
self.mock_object(self.share_manager.db, 'service_update')
self.mock_object(self.share_manager.db, 'share_instance_get', self.mock_object(self.share_manager.db, 'share_instance_get',
mock.Mock(side_effect=[instances[0], instances[2], mock.Mock(side_effect=[instances[0], instances[2],
instances[4]])) instances[4]]))
@ -607,6 +656,23 @@ class ShareManagerTestCase(test.TestCase):
mock.call(mock.ANY, instances[2]['id'], mock.call(mock.ANY, instances[2]['id'],
share_server=share_server), share_server=share_server),
])) ]))
self.share_manager.db.service_get_by_args.assert_called_once_with(
utils.IsAMatcher(context.RequestContext),
self.share_manager.host,
'manila-share'
)
self.share_manager.db.service_update.assert_has_calls([
mock.call(
utils.IsAMatcher(context.RequestContext),
fake_service['id'],
{'ensuring': True}
),
mock.call(
utils.IsAMatcher(context.RequestContext),
fake_service['id'],
{'ensuring': False}
)
])
@ddt.data(("some_hash", {"db_version": "test_version"}), @ddt.data(("some_hash", {"db_version": "test_version"}),
("ddd86ec90923b686597501e2f2431f3af59238c0", ("ddd86ec90923b686597501e2f2431f3af59238c0",
@ -623,6 +689,7 @@ class ShareManagerTestCase(test.TestCase):
new_backend_info else None) new_backend_info else None)
mock_backend_info_update = self.mock_object( mock_backend_info_update = self.mock_object(
self.share_manager.db, 'backend_info_update') self.share_manager.db, 'backend_info_update')
fake_service = {'id': 'fake_service_id', 'binary': 'manila-share'}
self.mock_object( self.mock_object(
self.share_manager.db, 'backend_info_get', self.share_manager.db, 'backend_info_get',
mock.Mock(return_value=old_backend_info)) mock.Mock(return_value=old_backend_info))
@ -630,6 +697,10 @@ class ShareManagerTestCase(test.TestCase):
mock.Mock(return_value=new_backend_info)) mock.Mock(return_value=new_backend_info))
self.mock_object(self.share_manager, 'publish_service_capabilities', self.mock_object(self.share_manager, 'publish_service_capabilities',
mock.Mock()) mock.Mock())
self.mock_object(self.share_manager.db,
'service_get_by_args',
mock.Mock(return_value=fake_service))
self.mock_object(self.share_manager.db, 'service_update')
mock_ensure_shares = self.mock_object( mock_ensure_shares = self.mock_object(
self.share_manager.driver, 'ensure_shares') self.share_manager.driver, 'ensure_shares')
mock_share_instance_get_all_by_host = self.mock_object( mock_share_instance_get_all_by_host = self.mock_object(
@ -665,6 +736,7 @@ class ShareManagerTestCase(test.TestCase):
instances = self._setup_init_mocks(setup_access_rules=False) instances = self._setup_init_mocks(setup_access_rules=False)
share_server = fakes.fake_share_server_get() share_server = fakes.fake_share_server_get()
fake_service = {'id': 'fake_service_id', 'binary': 'manila-share'}
self.mock_object(self.share_manager.db, self.mock_object(self.share_manager.db,
'share_instance_get_all_by_host', 'share_instance_get_all_by_host',
mock.Mock(return_value=instances)) mock.Mock(return_value=instances))
@ -683,6 +755,10 @@ class ShareManagerTestCase(test.TestCase):
self.mock_object(self.share_manager, '_get_share_server_dict', self.mock_object(self.share_manager, '_get_share_server_dict',
mock.Mock(return_value=share_server)) mock.Mock(return_value=share_server))
self.mock_object(self.share_manager, 'publish_service_capabilities') self.mock_object(self.share_manager, 'publish_service_capabilities')
self.mock_object(self.share_manager.db,
'service_get_by_args',
mock.Mock(return_value=fake_service))
self.mock_object(self.share_manager.db, 'service_update')
self.mock_object(manager.LOG, 'error') self.mock_object(manager.LOG, 'error')
self.mock_object(manager.LOG, 'info') self.mock_object(manager.LOG, 'info')
@ -730,6 +806,23 @@ class ShareManagerTestCase(test.TestCase):
mock.ANY, mock.ANY,
{'id': instances[1]['id'], 'status': instances[1]['status']}, {'id': instances[1]['id'], 'status': instances[1]['status']},
) )
self.share_manager.db.service_get_by_args.assert_called_once_with(
utils.IsAMatcher(context.RequestContext),
self.share_manager.host,
'manila-share'
)
self.share_manager.db.service_update.assert_has_calls([
mock.call(
utils.IsAMatcher(context.RequestContext),
fake_service['id'],
{'ensuring': True}
),
mock.call(
utils.IsAMatcher(context.RequestContext),
fake_service['id'],
{'ensuring': False}
)
])
def _get_share_instance_dict(self, share_instance, **kwargs): def _get_share_instance_dict(self, share_instance, **kwargs):
# TODO(gouthamr): remove method when the db layer returns primitives # TODO(gouthamr): remove method when the db layer returns primitives
@ -775,11 +868,16 @@ class ShareManagerTestCase(test.TestCase):
raise exception.ManilaException(message="Fake raise") raise exception.ManilaException(message="Fake raise")
instances = self._setup_init_mocks(setup_access_rules=False) instances = self._setup_init_mocks(setup_access_rules=False)
fake_service = {'id': 'fake_service_id', 'binary': 'manila-share'}
mock_ensure_share = self.mock_object( mock_ensure_share = self.mock_object(
self.share_manager.driver, 'ensure_share') self.share_manager.driver, 'ensure_share')
self.mock_object(self.share_manager.db, self.mock_object(self.share_manager.db,
'share_instance_get_all_by_host', 'share_instance_get_all_by_host',
mock.Mock(return_value=instances)) mock.Mock(return_value=instances))
self.mock_object(self.share_manager.db,
'service_get_by_args',
mock.Mock(return_value=fake_service))
self.mock_object(self.share_manager.db, 'service_update')
self.mock_object(self.share_manager.db, 'share_instance_get', self.mock_object(self.share_manager.db, 'share_instance_get',
mock.Mock(side_effect=[instances[0], instances[2], mock.Mock(side_effect=[instances[0], instances[2],
instances[3]])) instances[3]]))
@ -808,6 +906,20 @@ class ShareManagerTestCase(test.TestCase):
mock.call(utils.IsAMatcher(context.RequestContext), instances[0]), mock.call(utils.IsAMatcher(context.RequestContext), instances[0]),
mock.call(utils.IsAMatcher(context.RequestContext), instances[2]), mock.call(utils.IsAMatcher(context.RequestContext), instances[2]),
]) ])
self.share_manager.db.service_get_by_args.assert_called_once_with(
utils.IsAMatcher(context.RequestContext), self.share_manager.host,
'manila-share'
)
self.share_manager.db.service_update.assert_has_calls([
mock.call(
utils.IsAMatcher(context.RequestContext), fake_service['id'],
{'ensuring': True}
),
mock.call(
utils.IsAMatcher(context.RequestContext), fake_service['id'],
{'ensuring': False}
)
])
self.share_manager.driver.ensure_shares.assert_called_once_with( self.share_manager.driver.ensure_shares.assert_called_once_with(
utils.IsAMatcher(context.RequestContext), utils.IsAMatcher(context.RequestContext),
[dict_instances[0], dict_instances[2], dict_instances[3]]) [dict_instances[0], dict_instances[2], dict_instances[3]])
@ -854,11 +966,16 @@ class ShareManagerTestCase(test.TestCase):
instances[4]['id']: {'status': 'available'} instances[4]['id']: {'status': 'available'}
} }
smanager = self.share_manager smanager = self.share_manager
fake_service = {'id': 'fake_service_id', 'binary': 'manila-share'}
self.mock_object(smanager.db, 'share_instance_get_all_by_host', self.mock_object(smanager.db, 'share_instance_get_all_by_host',
mock.Mock(return_value=instances)) mock.Mock(return_value=instances))
self.mock_object(self.share_manager.db, 'share_instance_get', self.mock_object(self.share_manager.db, 'share_instance_get',
mock.Mock(side_effect=[instances[0], instances[2], mock.Mock(side_effect=[instances[0], instances[2],
instances[4]])) instances[4]]))
self.mock_object(self.share_manager.db,
'service_get_by_args',
mock.Mock(return_value=fake_service))
self.mock_object(self.share_manager.db, 'service_update')
self.mock_object(self.share_manager.driver, 'ensure_share', self.mock_object(self.share_manager.driver, 'ensure_share',
mock.Mock(return_value=None)) mock.Mock(return_value=None))
self.mock_object(self.share_manager.driver, 'ensure_shares', self.mock_object(self.share_manager.driver, 'ensure_shares',
@ -915,6 +1032,23 @@ class ShareManagerTestCase(test.TestCase):
manager.LOG.exception.assert_has_calls([ manager.LOG.exception.assert_has_calls([
mock.call(mock.ANY, mock.ANY), mock.call(mock.ANY, mock.ANY),
]) ])
self.share_manager.db.service_get_by_args.assert_called_once_with(
utils.IsAMatcher(context.RequestContext),
self.share_manager.host,
'manila-share'
)
self.share_manager.db.service_update.assert_has_calls([
mock.call(
utils.IsAMatcher(context.RequestContext),
fake_service['id'],
{'ensuring': True}
),
mock.call(
utils.IsAMatcher(context.RequestContext),
fake_service['id'],
{'ensuring': False}
)
])
def test_create_share_instance_from_snapshot_with_server(self): def test_create_share_instance_from_snapshot_with_server(self):
"""Test share can be created from snapshot if server exists.""" """Test share can be created from snapshot if server exists."""

View File

@ -147,6 +147,8 @@ class ShareRpcAPITestCase(test.TestCase):
and method in src_dest_share_server_methods): and method in src_dest_share_server_methods):
share_server = expected_msg.pop('dest_share_server', None) share_server = expected_msg.pop('dest_share_server', None)
expected_msg['dest_share_server_id'] = share_server['id'] expected_msg['dest_share_server_id'] = share_server['id']
if method == 'ensure_driver_resources':
expected_msg['skip_backend_info_check'] = True
if 'host' in kwargs: if 'host' in kwargs:
host = kwargs['host'] host = kwargs['host']
@ -527,3 +529,11 @@ class ShareRpcAPITestCase(test.TestCase):
dest_host=self.fake_host, dest_host=self.fake_host,
share_network_id='fake_net_id', share_network_id='fake_net_id',
new_share_network_subnet_id='new_share_network_subnet_id') new_share_network_subnet_id='new_share_network_subnet_id')
def test_ensure_driver_resources(self):
self._test_share_api(
'ensure_driver_resources',
rpc_method='cast',
version='1.29',
host=self.fake_host,
)

View File

@ -0,0 +1,16 @@
---
features:
- |
A new API to start the ensure shares procedure for Manila has been added.
Through this API, OpenStack administrators will be able to recalculate the
shares' export location without restarting the shares manager service.
Additionally, a new configuration option named
`update_shares_status_on_ensure` is now available to help OpenStack
administrators determine whether the shares' status should be modified
during the ensure shares procedure or not.
upgrade:
- |
When restarting the service on an upgrade, when ensure shares is being run
it will automatically transition the shares status to `ensuring`. In case
you would like to prevent it, please change the value of the
`update_shares_status_on_ensure` configuration option.