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:
parent
6a4ad5cfee
commit
359a96ba58
@ -27,7 +27,7 @@ from manilaclient import utils
|
|||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
MAX_VERSION = '2.46'
|
MAX_VERSION = '2.47'
|
||||||
MIN_VERSION = '2.0'
|
MIN_VERSION = '2.0'
|
||||||
DEPRECATED_VERSION = '1.0'
|
DEPRECATED_VERSION = '1.0'
|
||||||
_VERSIONED_METHOD_MAP = {}
|
_VERSIONED_METHOD_MAP = {}
|
||||||
|
@ -145,6 +145,17 @@ share_opts = [
|
|||||||
default="stack",
|
default="stack",
|
||||||
help="Username, that will be used in share access tests for "
|
help="Username, that will be used in share access tests for "
|
||||||
"user type of access."),
|
"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",
|
cfg.BoolOpt("run_snapshot_tests",
|
||||||
default=True,
|
default=True,
|
||||||
help="Defines whether to run tests that use share snapshots "
|
help="Defines whether to run tests that use share snapshots "
|
||||||
|
@ -13,6 +13,7 @@
|
|||||||
# 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 re
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
from oslo_log import log
|
from oslo_log import log
|
||||||
@ -111,6 +112,11 @@ class BaseTestCase(base.ClientTestBase):
|
|||||||
res_id, microversion=res["microversion"])
|
res_id, microversion=res["microversion"])
|
||||||
client.wait_for_snapshot_deletion(
|
client.wait_for_snapshot_deletion(
|
||||||
res_id, microversion=res["microversion"])
|
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:
|
else:
|
||||||
LOG.warning("Provided unsupported resource type for "
|
LOG.warning("Provided unsupported resource type for "
|
||||||
"cleanup '%s'. Skipping.", res["type"])
|
"cleanup '%s'. Skipping.", res["type"])
|
||||||
@ -238,8 +244,7 @@ class BaseTestCase(base.ClientTestBase):
|
|||||||
public=False, snapshot=None, metadata=None,
|
public=False, snapshot=None, metadata=None,
|
||||||
client=None, cleanup_in_class=False,
|
client=None, cleanup_in_class=False,
|
||||||
wait_for_creation=True, microversion=None):
|
wait_for_creation=True, microversion=None):
|
||||||
if client is None:
|
client = client or cls.get_admin_client()
|
||||||
client = cls.get_admin_client()
|
|
||||||
data = {
|
data = {
|
||||||
'share_protocol': share_protocol or client.share_protocol,
|
'share_protocol': share_protocol or client.share_protocol,
|
||||||
'size': size or 1,
|
'size': size or 1,
|
||||||
@ -250,12 +255,13 @@ class BaseTestCase(base.ClientTestBase):
|
|||||||
'metadata': metadata,
|
'metadata': metadata,
|
||||||
'microversion': microversion,
|
'microversion': microversion,
|
||||||
}
|
}
|
||||||
share_network = share_network or client.share_network
|
|
||||||
share_type = share_type or CONF.share_type
|
share_type = share_type or CONF.share_type
|
||||||
if share_network:
|
share_network = share_network or cls._determine_share_network_to_use(
|
||||||
data['share_network'] = share_network
|
client, share_type, microversion=microversion)
|
||||||
if share_type:
|
|
||||||
data['share_type'] = share_type
|
data['share_type'] = share_type
|
||||||
|
data['share_network'] = share_network
|
||||||
share = client.create_share(**data)
|
share = client.create_share(**data)
|
||||||
resource = {
|
resource = {
|
||||||
"type": "share",
|
"type": "share",
|
||||||
@ -271,6 +277,18 @@ class BaseTestCase(base.ClientTestBase):
|
|||||||
client.wait_for_share_status(share['id'], 'available')
|
client.wait_for_share_status(share['id'], 'available')
|
||||||
return share
|
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
|
@classmethod
|
||||||
def create_security_service(cls, type='ldap', name=None, description=None,
|
def create_security_service(cls, type='ldap', name=None, description=None,
|
||||||
dns_ip=None, server=None, domain=None,
|
dns_ip=None, server=None, domain=None,
|
||||||
@ -366,3 +384,27 @@ class BaseTestCase(base.ClientTestBase):
|
|||||||
else:
|
else:
|
||||||
cls.method_resources.insert(0, resource)
|
cls.method_resources.insert(0, resource)
|
||||||
return message
|
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
|
||||||
|
@ -35,6 +35,7 @@ SHARE_TYPE = 'share_type'
|
|||||||
SHARE_NETWORK = 'share_network'
|
SHARE_NETWORK = 'share_network'
|
||||||
SHARE_SERVER = 'share_server'
|
SHARE_SERVER = 'share_server'
|
||||||
SNAPSHOT = 'snapshot'
|
SNAPSHOT = 'snapshot'
|
||||||
|
SHARE_REPLICA = 'share_replica'
|
||||||
|
|
||||||
|
|
||||||
def not_found_wrapper(f):
|
def not_found_wrapper(f):
|
||||||
@ -136,6 +137,8 @@ class ManilaCLIClient(base.CLIClient):
|
|||||||
func = self.is_snapshot_deleted
|
func = self.is_snapshot_deleted
|
||||||
elif res_type == MESSAGE:
|
elif res_type == MESSAGE:
|
||||||
func = self.is_message_deleted
|
func = self.is_message_deleted
|
||||||
|
elif res_type == SHARE_REPLICA:
|
||||||
|
func = self.is_share_replica_deleted
|
||||||
else:
|
else:
|
||||||
raise exceptions.InvalidResource(message=res_type)
|
raise exceptions.InvalidResource(message=res_type)
|
||||||
|
|
||||||
@ -274,12 +277,15 @@ class ManilaCLIClient(base.CLIClient):
|
|||||||
def get_share_type(self, share_type, microversion=None):
|
def get_share_type(self, share_type, microversion=None):
|
||||||
"""Get share type.
|
"""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)
|
share_types = self.list_share_types(True, microversion=microversion)
|
||||||
for st in share_types:
|
for stype in share_types:
|
||||||
if share_type in (st['ID'], st['Name']):
|
if share_type is None and stype["is_default"] == 'YES':
|
||||||
return st
|
return stype
|
||||||
|
elif share_type in (stype['ID'], stype['Name']):
|
||||||
|
return stype
|
||||||
raise tempest_lib_exc.NotFound()
|
raise tempest_lib_exc.NotFound()
|
||||||
|
|
||||||
def is_share_type_deleted(self, share_type, microversion=None):
|
def is_share_type_deleted(self, share_type, microversion=None):
|
||||||
@ -1496,3 +1502,118 @@ class ManilaCLIClient(base.CLIClient):
|
|||||||
self.wait_for_resource_deletion(
|
self.wait_for_resource_deletion(
|
||||||
MESSAGE, res_id=message, interval=3, timeout=60,
|
MESSAGE, res_id=message, interval=3, timeout=60,
|
||||||
microversion=microversion)
|
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
|
||||||
|
@ -44,6 +44,11 @@ class ShareBuildErrorException(exceptions.TempestException):
|
|||||||
message = "Share %(share)s failed to build and is in ERROR status."
|
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):
|
class SnapshotBuildErrorException(exceptions.TempestException):
|
||||||
message = "Snapshot %(snapshot)s failed to build and is in ERROR status."
|
message = "Snapshot %(snapshot)s failed to build and is in ERROR status."
|
||||||
|
|
||||||
|
@ -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])
|
@ -830,6 +830,26 @@ class FakeHTTPClient(fakes.FakeHTTPClient):
|
|||||||
replicas = {'share_replica': self.fake_share_replica}
|
replicas = {'share_replica': self.fake_share_replica}
|
||||||
return (200, {}, replicas)
|
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):
|
def post_share_replicas(self, **kw):
|
||||||
return (202, {}, {'share_replica': self.fake_share_replica})
|
return (202, {}, {'share_replica': self.fake_share_replica})
|
||||||
|
|
||||||
|
@ -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)
|
@ -2751,7 +2751,7 @@ class ShellTest(test_utils.TestCase):
|
|||||||
|
|
||||||
self.run_command('share-replica-show 5678')
|
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')
|
@ddt.data('promote', 'resync')
|
||||||
@mock.patch.object(shell_v2, '_find_share_replica', mock.Mock())
|
@mock.patch.object(shell_v2, '_find_share_replica', mock.Mock())
|
||||||
@ -2766,6 +2766,38 @@ class ShellTest(test_utils.TestCase):
|
|||||||
'POST', '/share-replicas/1234/action',
|
'POST', '/share-replicas/1234/action',
|
||||||
body={action.replace('-', '_'): None})
|
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')
|
@ddt.data('reset-state', 'reset-replica-state')
|
||||||
@mock.patch.object(shell_v2, '_find_share_replica', mock.Mock())
|
@mock.patch.object(shell_v2, '_find_share_replica', mock.Mock())
|
||||||
def test_share_replica_reset_state_cmds(self, action):
|
def test_share_replica_reset_state_cmds(self, action):
|
||||||
|
@ -37,6 +37,7 @@ from manilaclient.v2 import share_groups
|
|||||||
from manilaclient.v2 import share_instance_export_locations
|
from manilaclient.v2 import share_instance_export_locations
|
||||||
from manilaclient.v2 import share_instances
|
from manilaclient.v2 import share_instances
|
||||||
from manilaclient.v2 import share_networks
|
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_replicas
|
||||||
from manilaclient.v2 import share_servers
|
from manilaclient.v2 import share_servers
|
||||||
from manilaclient.v2 import share_snapshot_export_locations
|
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_type_access = share_type_access.ShareTypeAccessManager(self)
|
||||||
self.share_servers = share_servers.ShareServerManager(self)
|
self.share_servers = share_servers.ShareServerManager(self)
|
||||||
self.share_replicas = share_replicas.ShareReplicaManager(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.pools = scheduler_stats.PoolManager(self)
|
||||||
self.share_access_rules = (
|
self.share_access_rules = (
|
||||||
share_access_rules.ShareAccessRuleManager(self))
|
share_access_rules.ShareAccessRuleManager(self))
|
||||||
|
55
manilaclient/v2/share_replica_export_locations.py
Normal file
55
manilaclient/v2/share_replica_export_locations.py
Normal 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")
|
@ -68,14 +68,19 @@ def _find_share(cs, share):
|
|||||||
|
|
||||||
def _transform_export_locations_to_string_view(export_locations):
|
def _transform_export_locations_to_string_view(export_locations):
|
||||||
export_locations_string_view = ''
|
export_locations_string_view = ''
|
||||||
|
replica_export_location_ignored_keys = (
|
||||||
|
'replica_state', 'availability_zone', 'share_replica_id')
|
||||||
for el in export_locations:
|
for el in export_locations:
|
||||||
if hasattr(el, '_info'):
|
if hasattr(el, '_info'):
|
||||||
export_locations_dict = el._info
|
export_locations_dict = el._info
|
||||||
else:
|
else:
|
||||||
export_locations_dict = el
|
export_locations_dict = el
|
||||||
for k, v in export_locations_dict.items():
|
for k, v in export_locations_dict.items():
|
||||||
export_locations_string_view += '\n%(k)s = %(v)s' % {
|
# NOTE(gouthamr): We don't want to show replica related info
|
||||||
'k': k, 'v': v}
|
# 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
|
return export_locations_string_view
|
||||||
|
|
||||||
|
|
||||||
@ -195,12 +200,24 @@ def _find_share_replica(cs, replica):
|
|||||||
return apiclient_utils.find_resource(cs.share_replicas, replica)
|
return apiclient_utils.find_resource(cs.share_replicas, replica)
|
||||||
|
|
||||||
|
|
||||||
|
@api_versions.wraps("2.11", "2.46")
|
||||||
def _print_share_replica(cs, replica):
|
def _print_share_replica(cs, replica):
|
||||||
info = replica._info.copy()
|
info = replica._info.copy()
|
||||||
info.pop('links', None)
|
info.pop('links', None)
|
||||||
cliutils.print_dict(info)
|
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.experimental_api
|
||||||
@api_versions.wraps("2.31")
|
@api_versions.wraps("2.31")
|
||||||
def _find_share_group(cs, share_group):
|
def _find_share_group(cs, share_group):
|
||||||
@ -5006,7 +5023,7 @@ def do_share_replica_create(cs, args):
|
|||||||
'replica',
|
'replica',
|
||||||
metavar='<replica>',
|
metavar='<replica>',
|
||||||
help='ID of the share 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):
|
def do_share_replica_show(cs, args):
|
||||||
"""Show details about a replica (Experimental)."""
|
"""Show details about a replica (Experimental)."""
|
||||||
|
|
||||||
@ -5014,6 +5031,20 @@ def do_share_replica_show(cs, args):
|
|||||||
_print_share_replica(cs, replica)
|
_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(
|
@cliutils.arg(
|
||||||
'replica',
|
'replica',
|
||||||
metavar='<replica>',
|
metavar='<replica>',
|
||||||
@ -5059,6 +5090,55 @@ def do_share_replica_promote(cs, args):
|
|||||||
cs.share_replicas.promote(replica)
|
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(
|
@cliutils.arg(
|
||||||
'replica',
|
'replica',
|
||||||
metavar='<replica>',
|
metavar='<replica>',
|
||||||
|
@ -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.
|
Loading…
Reference in New Issue
Block a user