Add support for replica export location APIs

- Add share_replica_export_locations module to the
  manilaclient SDK.
- Add 'share-replica-export-location-list' and
  'share-replica-export-location-show' commands to
  the manilaclient shell module.
- Add export_locations attr to 'share-replica-show'
  command in the shell module.

Change-Id: I0edb0d56f5c8f439932c18d510aad0457112b75e
Depends-On: https://review.openstack.org/#/c/628069/
Implements: bp export-locations-az
This commit is contained in:
Goutham Pacha Ravi 2019-01-03 20:27:42 -08:00
parent 6a4ad5cfee
commit 359a96ba58
13 changed files with 549 additions and 16 deletions

View File

@ -27,7 +27,7 @@ from manilaclient import utils
LOG = logging.getLogger(__name__)
MAX_VERSION = '2.46'
MAX_VERSION = '2.47'
MIN_VERSION = '2.0'
DEPRECATED_VERSION = '1.0'
_VERSIONED_METHOD_MAP = {}

View File

@ -145,6 +145,17 @@ share_opts = [
default="stack",
help="Username, that will be used in share access tests for "
"user type of access."),
cfg.StrOpt("replication_type",
default="readable",
choices=["readable", "writable", "dr"],
help="Replication type to be used when running replication "
"tests. This option is ignored if run_replication_tests "
"is set to False."),
cfg.BoolOpt("run_replication_tests",
default=True,
help="Defines whether to run tests for share replication "
"or not. Disable this feature if manila driver used "
"doesn't support share replication."),
cfg.BoolOpt("run_snapshot_tests",
default=True,
help="Defines whether to run tests that use share snapshots "

View File

@ -13,6 +13,7 @@
# License for the specific language governing permissions and limitations
# under the License.
import re
import traceback
from oslo_log import log
@ -111,6 +112,11 @@ class BaseTestCase(base.ClientTestBase):
res_id, microversion=res["microversion"])
client.wait_for_snapshot_deletion(
res_id, microversion=res["microversion"])
elif res["type"] is "share_replica":
client.delete_share_replica(
res_id, microversion=res["microversion"])
client.wait_for_share_replica_deletion(
res_id, microversion=res["microversion"])
else:
LOG.warning("Provided unsupported resource type for "
"cleanup '%s'. Skipping.", res["type"])
@ -238,8 +244,7 @@ class BaseTestCase(base.ClientTestBase):
public=False, snapshot=None, metadata=None,
client=None, cleanup_in_class=False,
wait_for_creation=True, microversion=None):
if client is None:
client = cls.get_admin_client()
client = client or cls.get_admin_client()
data = {
'share_protocol': share_protocol or client.share_protocol,
'size': size or 1,
@ -250,12 +255,13 @@ class BaseTestCase(base.ClientTestBase):
'metadata': metadata,
'microversion': microversion,
}
share_network = share_network or client.share_network
share_type = share_type or CONF.share_type
if share_network:
data['share_network'] = share_network
if share_type:
share_network = share_network or cls._determine_share_network_to_use(
client, share_type, microversion=microversion)
data['share_type'] = share_type
data['share_network'] = share_network
share = client.create_share(**data)
resource = {
"type": "share",
@ -271,6 +277,18 @@ class BaseTestCase(base.ClientTestBase):
client.wait_for_share_status(share['id'], 'available')
return share
@classmethod
def _determine_share_network_to_use(cls, client, share_type,
microversion=None):
"""Determine what share network we need from the share type."""
# Get share type, determine if we need the share network
share_type = client.get_share_type(share_type,
microversion=microversion)
dhss_pattern = re.compile('driver_handles_share_servers : ([a-zA-Z]+)')
dhss = dhss_pattern.search(share_type['required_extra_specs']).group(1)
return client.share_network if dhss.lower() == 'true' else None
@classmethod
def create_security_service(cls, type='ldap', name=None, description=None,
dns_ip=None, server=None, domain=None,
@ -366,3 +384,27 @@ class BaseTestCase(base.ClientTestBase):
else:
cls.method_resources.insert(0, resource)
return message
@classmethod
def create_share_replica(cls, share_id, client=None,
wait_for_creation=True, cleanup_in_class=False,
microversion=None):
client = client or cls.get_user_client()
share_replica = client.create_share_replica(
share_id, microversion=microversion)
if wait_for_creation:
share_replica = client.wait_for_share_replica_status(
share_replica['id'])
resource = {
"type": "share_replica",
"id": share_replica["id"],
"client": client,
"microversion": microversion,
}
if cleanup_in_class:
cls.class_resources.insert(0, resource)
else:
cls.method_resources.insert(0, resource)
return share_replica

View File

@ -35,6 +35,7 @@ SHARE_TYPE = 'share_type'
SHARE_NETWORK = 'share_network'
SHARE_SERVER = 'share_server'
SNAPSHOT = 'snapshot'
SHARE_REPLICA = 'share_replica'
def not_found_wrapper(f):
@ -136,6 +137,8 @@ class ManilaCLIClient(base.CLIClient):
func = self.is_snapshot_deleted
elif res_type == MESSAGE:
func = self.is_message_deleted
elif res_type == SHARE_REPLICA:
func = self.is_share_replica_deleted
else:
raise exceptions.InvalidResource(message=res_type)
@ -274,12 +277,15 @@ class ManilaCLIClient(base.CLIClient):
def get_share_type(self, share_type, microversion=None):
"""Get share type.
:param share_type: str -- Name or ID of share type
:param share_type: str -- Name or ID of share type, or None to
retrieve default share type
"""
share_types = self.list_share_types(True, microversion=microversion)
for st in share_types:
if share_type in (st['ID'], st['Name']):
return st
for stype in share_types:
if share_type is None and stype["is_default"] == 'YES':
return stype
elif share_type in (stype['ID'], stype['Name']):
return stype
raise tempest_lib_exc.NotFound()
def is_share_type_deleted(self, share_type, microversion=None):
@ -1496,3 +1502,118 @@ class ManilaCLIClient(base.CLIClient):
self.wait_for_resource_deletion(
MESSAGE, res_id=message, interval=3, timeout=60,
microversion=microversion)
# Share replicas
def create_share_replica(self, share, microversion=None):
"""Create a share replica.
:param share: str -- Name or ID of a share to create a replica of
"""
cmd = "share-replica-create %s" % share
replica = self.manila(cmd, microversion=microversion)
return output_parser.details(replica)
@not_found_wrapper
def get_share_replica(self, replica, microversion=None):
cmd = "share-replica-show %s" % replica
replica = self.manila(cmd, microversion=microversion)
return output_parser.details(replica)
@not_found_wrapper
@forbidden_wrapper
def delete_share_replica(self, share_replica, microversion=None):
"""Deletes share replica by ID."""
return self.manila(
"share-replica-delete %s" % share_replica,
microversion=microversion)
def is_share_replica_deleted(self, replica, microversion=None):
"""Indicates whether a share replica is deleted or not.
:param replica: str -- ID of share replica
"""
try:
self.get_share_replica(replica, microversion=microversion)
return False
except tempest_lib_exc.NotFound:
return True
def wait_for_share_replica_deletion(self, replica, microversion=None):
"""Wait for share replica deletion by its ID.
:param replica: text -- ID of share replica
"""
self.wait_for_resource_deletion(
SHARE_REPLICA, res_id=replica, interval=3, timeout=60,
microversion=microversion)
def wait_for_share_replica_status(self, share_replica,
status="available",
microversion=None):
"""Waits for a share replica to reach a given status."""
replica = self.get_share_replica(share_replica,
microversion=microversion)
share_replica_status = replica['status']
start = int(time.time())
while share_replica_status != status:
time.sleep(self.build_interval)
replica = self.get_share_replica(share_replica,
microversion=microversion)
share_replica_status = replica['status']
if share_replica_status == status:
return replica
elif 'error' in share_replica_status.lower():
raise exceptions.ShareReplicaBuildErrorException(
replica=share_replica)
if int(time.time()) - start >= self.build_timeout:
message = (
"Share replica %(id)s failed to reach %(status)s "
"status within the required time "
"(%(build_timeout)s s)." % {
"id": share_replica, "status": status,
"build_timeout": self.build_timeout})
raise tempest_lib_exc.TimeoutException(message)
return replica
@not_found_wrapper
@forbidden_wrapper
def list_share_replica_export_locations(self, share_replica,
columns=None, microversion=None):
"""List share replica export locations.
:param share_replica: str -- ID of share replica.
:param columns: str -- comma separated string of columns.
Example, "--columns id,path".
:param microversion: API microversion to be used for request.
"""
cmd = "share-replica-export-location-list %s" % share_replica
if columns is not None:
cmd += " --columns " + columns
export_locations_raw = self.manila(cmd, microversion=microversion)
export_locations = utils.listing(export_locations_raw)
return export_locations
@not_found_wrapper
@forbidden_wrapper
def get_share_replica_export_location(self, share_replica,
export_location_uuid,
microversion=None):
"""Returns an export location by share replica and export location ID.
:param share_replica: str -- ID of share replica.
:param export_location_uuid: str -- UUID of an export location.
:param microversion: API microversion to be used for request.
"""
export_raw = self.manila(
'share-replica-export-location-show '
'%(share_replica)s %(el_uuid)s' % {
'share_replica': share_replica,
'el_uuid': export_location_uuid,
},
microversion=microversion)
export = output_parser.details(export_raw)
return export

View File

@ -44,6 +44,11 @@ class ShareBuildErrorException(exceptions.TempestException):
message = "Share %(share)s failed to build and is in ERROR status."
class ShareReplicaBuildErrorException(exceptions.TempestException):
message = ("Share replica %(replica)s failed to build and is in ERROR "
"status.")
class SnapshotBuildErrorException(exceptions.TempestException):
message = "Snapshot %(snapshot)s failed to build and is in ERROR status."

View File

@ -0,0 +1,107 @@
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import ddt
from oslo_utils import uuidutils
import testtools
from manilaclient import config
from manilaclient.tests.functional import base
from manilaclient.tests.functional import utils
CONF = config.CONF
@ddt.ddt
@testtools.skipUnless(CONF.run_replication_tests,
"Replication tests are disabled.")
@utils.skip_if_microversion_not_supported('2.47')
class ShareReplicaExportLocationsTest(base.BaseTestCase):
@classmethod
def setUpClass(cls):
super(ShareReplicaExportLocationsTest, cls).setUpClass()
def _create_share_and_replica(self):
replication_type = CONF.replication_type
share_type = self.create_share_type(
driver_handles_share_servers=False,
extra_specs={'replication_type': replication_type},
cleanup_in_class=False)
share = self.create_share(share_type=share_type['ID'],
client=self.get_user_client())
share_replica = self.create_share_replica(share['id'])
return share, share_replica
@ddt.data('admin', 'user')
def test_list_share_export_locations(self, role):
share, share_replica = self._create_share_and_replica()
client = self.admin_client if role == 'admin' else self.user_client
export_locations = client.list_share_replica_export_locations(
share_replica['id'])
self.assertGreater(len(export_locations), 0)
expected_keys = ['ID', 'Path', 'Preferred', 'Replica State',
'Availability Zone']
for el in export_locations:
for key in expected_keys:
self.assertIn(key, el)
self.assertTrue(uuidutils.is_uuid_like(el['ID']))
self.assertIn(el['Preferred'], ('True', 'False'))
@ddt.data('admin', 'user')
def test_list_share_export_locations_with_columns(self, role):
share, share_replica = self._create_share_and_replica()
client = self.admin_client if role == 'admin' else self.user_client
export_locations = client.list_share_replica_export_locations(
share_replica['id'], columns='id,path')
self.assertGreater(len(export_locations), 0)
expected_keys = ('Id', 'Path')
unexpected_keys = ('Updated At', 'Created At')
for el in export_locations:
for key in expected_keys:
self.assertIn(key, el)
for key in unexpected_keys:
self.assertNotIn(key, el)
self.assertTrue(uuidutils.is_uuid_like(el['Id']))
@ddt.data('admin', 'user')
def test_get_share_replica_export_location(self, role):
share, share_replica = self._create_share_and_replica()
client = self.admin_client if role == 'admin' else self.user_client
export_locations = client.list_share_replica_export_locations(
share_replica['id'])
el = client.get_share_replica_export_location(
share_replica['id'], export_locations[0]['ID'])
expected_keys = ['path', 'updated_at', 'created_at', 'id',
'preferred', 'replica_state', 'availability_zone']
if role == 'admin':
expected_keys.extend(['is_admin_only', 'share_instance_id'])
for key in expected_keys:
self.assertIn(key, el)
if role == 'admin':
self.assertTrue(uuidutils.is_uuid_like(el['share_instance_id']))
self.assertIn(el['is_admin_only'], ('True', 'False'))
self.assertTrue(uuidutils.is_uuid_like(el['id']))
self.assertIn(el['preferred'], ('True', 'False'))
for list_k, get_k in (
('ID', 'id'), ('Path', 'path'), ('Preferred', 'preferred'),
('Replica State', 'replica_state'),
('Availability Zone', 'availability_zone')):
self.assertEqual(
export_locations[0][list_k], el[get_k])

View File

@ -830,6 +830,26 @@ class FakeHTTPClient(fakes.FakeHTTPClient):
replicas = {'share_replica': self.fake_share_replica}
return (200, {}, replicas)
def get_share_replicas_5678_export_locations(self, **kw):
export_locations = {
'export_locations': [
get_fake_export_location(),
]
}
return (200, {}, export_locations)
def get_share_replicas_1234_export_locations(self, **kw):
export_locations = {
'export_locations': [
get_fake_export_location(),
]
}
return (200, {}, export_locations)
def get_share_replicas_1234_export_locations_fake_el_uuid(self, **kw):
export_location = {'export_location': get_fake_export_location()}
return (200, {}, export_location)
def post_share_replicas(self, **kw):
return (202, {}, {'share_replica': self.fake_share_replica})

View File

@ -0,0 +1,50 @@
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import ddt
import mock
from manilaclient import api_versions
from manilaclient.tests.unit import utils
from manilaclient.tests.unit.v2 import fakes
from manilaclient.v2 import share_replica_export_locations
cs = fakes.FakeClient()
@ddt.ddt
class ShareReplicaExportLocationsTest(utils.TestCase):
def _get_manager(self, microversion):
version = api_versions.APIVersion(microversion)
mock_microversion = mock.Mock(api_version=version)
return (
share_replica_export_locations.ShareReplicaExportLocationManager(
api=mock_microversion)
)
def test_list_share_replica_export_locations(self):
share_replica_id = '1234'
cs.share_replica_export_locations.list(share_replica_id)
cs.assert_called(
'GET', '/share-replicas/%s/export-locations' % share_replica_id)
def test_get_share_replica_export_location(self):
share_replica_id = '1234'
el_uuid = 'fake_el_uuid'
cs.share_replica_export_locations.get(share_replica_id, el_uuid)
url = ('/share-replicas/%(share_replica_id)s/export-locations/'
'%(el_uuid)s')
payload = {'share_replica_id': share_replica_id, 'el_uuid': el_uuid}
cs.assert_called('GET', url % payload)

View File

@ -2751,7 +2751,7 @@ class ShellTest(test_utils.TestCase):
self.run_command('share-replica-show 5678')
self.assert_called('GET', '/share-replicas/5678')
self.assert_called_anytime('GET', '/share-replicas/5678')
@ddt.data('promote', 'resync')
@mock.patch.object(shell_v2, '_find_share_replica', mock.Mock())
@ -2766,6 +2766,38 @@ class ShellTest(test_utils.TestCase):
'POST', '/share-replicas/1234/action',
body={action.replace('-', '_'): None})
@mock.patch.object(shell_v2, '_find_share_replica', mock.Mock())
@mock.patch.object(cliutils, 'print_list', mock.Mock())
@ddt.data(None, "replica_state,path")
def test_share_replica_export_location_list(self, columns):
fake_replica = type('FakeShareReplica', (object,), {'id': '1234'})
shell_v2._find_share_replica.return_value = fake_replica
cmd = 'share-replica-export-location-list ' + fake_replica.id
if columns is not None:
cmd = cmd + ' --columns=%s' % columns
expected_columns = list(map(lambda x: x.strip().title(),
columns.split(",")))
else:
expected_columns = [
'ID', 'Availability Zone', 'Replica State',
'Preferred', 'Path'
]
self.run_command(cmd)
self.assert_called(
'GET', '/share-replicas/1234/export-locations')
cliutils.print_list.assert_called_with(mock.ANY, expected_columns)
@mock.patch.object(shell_v2, '_find_share_replica', mock.Mock())
def test_share_replica_export_location_show(self):
fake_replica = type('FakeShareReplica', (object,), {'id': '1234'})
shell_v2._find_share_replica.return_value = fake_replica
self.run_command(
'share-replica-export-location-show 1234 fake-el-uuid')
self.assert_called(
'GET', '/share-replicas/1234/export-locations/fake-el-uuid')
@ddt.data('reset-state', 'reset-replica-state')
@mock.patch.object(shell_v2, '_find_share_replica', mock.Mock())
def test_share_replica_reset_state_cmds(self, action):

View File

@ -37,6 +37,7 @@ from manilaclient.v2 import share_groups
from manilaclient.v2 import share_instance_export_locations
from manilaclient.v2 import share_instances
from manilaclient.v2 import share_networks
from manilaclient.v2 import share_replica_export_locations
from manilaclient.v2 import share_replicas
from manilaclient.v2 import share_servers
from manilaclient.v2 import share_snapshot_export_locations
@ -237,6 +238,9 @@ class Client(object):
self.share_type_access = share_type_access.ShareTypeAccessManager(self)
self.share_servers = share_servers.ShareServerManager(self)
self.share_replicas = share_replicas.ShareReplicaManager(self)
self.share_replica_export_locations = (
share_replica_export_locations.ShareReplicaExportLocationManager(
self))
self.pools = scheduler_stats.PoolManager(self)
self.share_access_rules = (
share_access_rules.ShareAccessRuleManager(self))

View File

@ -0,0 +1,55 @@
# All Rights Reserved.
#
# 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 manilaclient import api_versions
from manilaclient import base
from manilaclient.common.apiclient import base as common_base
class ShareReplicaExportLocation(common_base.Resource):
"""Resource class for a share replica export location."""
def __repr__(self):
return "<ShareReplicaExportLocation: %s>" % self.id
def __getitem__(self, key):
return self._info[key]
class ShareReplicaExportLocationManager(base.ManagerWithFind):
"""Manage :class:`ShareInstanceExportLocation` resources."""
resource_class = ShareReplicaExportLocation
@api_versions.wraps("2.47")
@api_versions.experimental_api
def list(self, share_replica, search_opts=None):
"""List all share replica export locations."""
share_replica_id = common_base.getid(share_replica)
return self._list(
"/share-replicas/%s/export-locations" % share_replica_id,
"export_locations")
@api_versions.wraps("2.47")
@api_versions.experimental_api
def get(self, share_replica, export_location):
"""Get a share replica export location."""
share_replica_id = common_base.getid(share_replica)
export_location_id = common_base.getid(export_location)
return self._get(
("/share-replicas/%(share_replica_id)s/export-locations/"
"%(export_location_id)s") % {
"share_replica_id": share_replica_id,
"export_location_id": export_location_id,
},
"export_location")

View File

@ -68,12 +68,17 @@ def _find_share(cs, share):
def _transform_export_locations_to_string_view(export_locations):
export_locations_string_view = ''
replica_export_location_ignored_keys = (
'replica_state', 'availability_zone', 'share_replica_id')
for el in export_locations:
if hasattr(el, '_info'):
export_locations_dict = el._info
else:
export_locations_dict = el
for k, v in export_locations_dict.items():
# NOTE(gouthamr): We don't want to show replica related info
# twice in the output, so ignore those.
if k not in replica_export_location_ignored_keys:
export_locations_string_view += '\n%(k)s = %(v)s' % {
'k': k, 'v': v}
return export_locations_string_view
@ -195,12 +200,24 @@ def _find_share_replica(cs, replica):
return apiclient_utils.find_resource(cs.share_replicas, replica)
@api_versions.wraps("2.11", "2.46")
def _print_share_replica(cs, replica):
info = replica._info.copy()
info.pop('links', None)
cliutils.print_dict(info)
@api_versions.wraps("2.47") # noqa
def _print_share_replica(cs, replica):
info = replica._info.copy()
info.pop('links', None)
if info.get('export_locations'):
info['export_locations'] = (
_transform_export_locations_to_string_view(
info['export_locations']))
cliutils.print_dict(info)
@api_versions.experimental_api
@api_versions.wraps("2.31")
def _find_share_group(cs, share_group):
@ -5006,7 +5023,7 @@ def do_share_replica_create(cs, args):
'replica',
metavar='<replica>',
help='ID of the share replica.')
@api_versions.wraps("2.11")
@api_versions.wraps("2.11", "2.46")
def do_share_replica_show(cs, args):
"""Show details about a replica (Experimental)."""
@ -5014,6 +5031,20 @@ def do_share_replica_show(cs, args):
_print_share_replica(cs, replica)
@api_versions.wraps("2.47") # noqa
@cliutils.arg(
'replica',
metavar='<replica>',
help='ID of the share replica.')
def do_share_replica_show(cs, args):
"""Show details about a replica (Experimental)."""
replica = cs.share_replicas.get(args.replica)
export_locations = cs.share_replica_export_locations.list(replica)
replica._info['export_locations'] = export_locations
_print_share_replica(cs, replica)
@cliutils.arg(
'replica',
metavar='<replica>',
@ -5059,6 +5090,55 @@ def do_share_replica_promote(cs, args):
cs.share_replicas.promote(replica)
@api_versions.wraps("2.47")
@api_versions.experimental_api
@cliutils.arg(
'replica',
metavar='<replica>',
help='ID of the share replica.')
@cliutils.arg(
'--columns',
metavar='<columns>',
type=str,
default=None,
help='Comma separated list of columns to be displayed '
'example --columns "id,path,replica_state".')
def do_share_replica_export_location_list(cs, args):
"""List export locations of a share replica (Experimental)."""
if args.columns is not None:
list_of_keys = _split_columns(columns=args.columns)
else:
list_of_keys = [
'ID',
'Availability Zone',
'Replica State',
'Preferred',
'Path',
]
replica = _find_share_replica(cs, args.replica)
export_locations = cs.share_replica_export_locations.list(replica)
cliutils.print_list(export_locations, list_of_keys)
@api_versions.wraps("2.47")
@api_versions.experimental_api
@cliutils.arg(
'replica',
metavar='<replica>',
help='Name or ID of the share instance.')
@cliutils.arg(
'export_location',
metavar='<export_location>',
help='ID of the share instance export location.')
def do_share_replica_export_location_show(cs, args):
"""Show details of a share replica's export location (Experimental)."""
replica = _find_share_replica(cs, args.replica)
export_location = cs.share_replica_export_locations.get(
replica, args.export_location)
view_data = export_location._info.copy()
cliutils.print_dict(view_data)
@cliutils.arg(
'replica',
metavar='<replica>',

View File

@ -0,0 +1,6 @@
---
features:
- |
Share replica export locations APIs are now supported within the v2
client and the manilaclient shell. These commands are available with API
version 2.47 and beyond.