Add share group support to Manila client

Manila needs a grouping construct that, like shares, is a 1st-class
atomic data type. Our experience with CGs has demonstrated the
complexity of adding a grouping capability, yet there are other use
cases such as migration, replication, and backup in which some
storage controllers could only offer such features on share groups.
CGs also highlighted the poor optics of an advanced feature with
comparatively little potential for vendor support. And adding new
grouping constructs for each new feature is not technically
feasible. All of the above may be addressed by generic groups,
which we think is a clean extension to the original architecture of
Manila.

Implements: blueprint manila-share-groups
Change-Id: I8e29baed62355fc31caeec9c7a66eaebfcbdf184
This commit is contained in:
Clinton Knight 2016-05-31 20:49:30 -07:00 committed by Valeriy Ponomaryov
parent ff9963a712
commit b4250866ea
22 changed files with 2896 additions and 1272 deletions

View File

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

View File

@ -22,6 +22,7 @@ Base utilities to build API operation managers and objects on top of.
import contextlib
import hashlib
import os
from six.moves.urllib import parse
from manilaclient.common import cliutils
from manilaclient import exceptions
@ -174,6 +175,11 @@ class Manager(utils.HookableMixin):
else:
return self.resource_class(self, body)
def _build_query_string(self, search_opts):
q_string = parse.urlencode(
sorted([(k, v) for (k, v) in search_opts.items() if v]))
return "?%s" % q_string if q_string else q_string
class ManagerWithFind(Manager):
"""Like a `Manager`, but with additional `find()`/`findall()` methods."""

View File

@ -41,7 +41,7 @@ SNAPSHOT_SORT_KEY_VALUES = (
'display_name',
)
CONSISTENCY_GROUP_SORT_KEY_VALUES = (
SHARE_GROUP_SORT_KEY_VALUES = (
'id',
'name',
'status',
@ -49,10 +49,15 @@ CONSISTENCY_GROUP_SORT_KEY_VALUES = (
'user_id',
'project_id',
'created_at',
'source_cgsnapshot_id',
'availability_zone',
'share_network',
'share_network_id',
'share_group_type',
'share_group_type_id',
'source_share_group_snapshot_id',
)
CG_SNAPSHOT_SORT_KEY_VALUES = (
SHARE_GROUP_SNAPSHOT_SORT_KEY_VALUES = (
'id',
'name',
'status',
@ -60,17 +65,7 @@ CG_SNAPSHOT_SORT_KEY_VALUES = (
'user_id',
'project_id',
'created_at',
)
CG_SNAPSHOT_MEMBERS_SORT_KEY_VALUES = (
'id',
'name',
'created_at',
'size',
'share_protocol',
'project_id',
'share_type_id',
'cgsnapshot_id',
'share_group_id',
)
TASK_STATE_MIGRATION_SUCCESS = 'migration_success'

View File

@ -654,96 +654,85 @@ class FakeHTTPClient(fakes.FakeHTTPClient):
}
return (200, {}, pools)
def get_consistency_groups_detail(self, **kw):
consistency_groups = {
'consistency_groups': [
{
'id': 1234,
'status': 'available',
'name': 'cgname',
'description': 'my cg'
}
]
}
return (200, {}, consistency_groups)
fake_share_group = {
'id': '1234',
'availability_zone': 'nova',
'share_network_id': None,
'status': 'available',
'name': 'share group name',
'description': 'my share group',
}
def delete_consistency_groups_1234(self, **kw):
return (202, {}, None)
def get_share_groups_detail(self, **kw):
share_groups = {'share_groups': [self.fake_share_group]}
return 200, {}, share_groups
def post_consistency_groups_1234_action(self, **kw):
return (202, {}, None)
def get_share_groups_1234(self, **kw):
share_group = {'share_group': self.fake_share_group}
return 200, {}, share_group
def post_consistency_groups(self, body, **kw):
return (202, {}, {
'consistency_group': {
'id': 'fake-cg-id',
'name': 'fake_name'
def put_share_groups_1234(self, **kwargs):
share_group = {'share_group': self.fake_share_group}
return 200, {}, share_group
def delete_share_groups_1234(self, **kw):
return 202, {}, None
def post_share_groups_1234_action(self, **kw):
return 202, {}, None
def post_share_groups(self, body, **kw):
share_group = {
'share_group': {
'id': 'fake-sg-id',
'name': 'fake_name',
}
})
def get_cgsnapshots_fake_cg_id_members(self, **kw):
members = {
'cgsnapshot_members': [
{
'id': 1234,
'name': 'fake name',
'created_at': '05050505',
'size': '50PB',
'share_protocol': 'NFS',
'project_id': '2221234',
'share_type_id': '3331234',
},
{
'id': 4321,
'name': 'fake name 2',
'created_at': '03030303',
'size': '50PB',
'share_protocol': 'NFS',
'project_id': '2224321',
'share_type_id': '3334321',
}
]
}
return(200, {}, members)
return 202, {}, share_group
def get_cgsnapshots(self, **kw):
cg_snapshots = {
'cgsnapshots': [
{
'id': 1234,
'status': 'available',
'name': 'cgsnapshotname',
}
]
fake_share_group_snapshot = {
'id': '1234',
'status': 'available',
'name': 'share group name',
'description': 'my share group',
}
def get_share_group_snapshots(self, **kw):
sg_snapshots = {
'share_group_snapshots': [self.fake_share_group_snapshot],
}
return (200, {}, cg_snapshots)
return 200, {}, sg_snapshots
def get_cgsnapshots_detail(self, **kw):
cg_snapshots = {
'cgsnapshots': [
{
'id': 1234,
'status': 'available',
'name': 'cgsnapshotname',
'description': 'my cgsnapshot'
}
]
def get_share_group_snapshots_detail(self, **kw):
sg_snapshots = {
'share_group_snapshots': [self.fake_share_group_snapshot],
}
return (200, {}, cg_snapshots)
return 200, {}, sg_snapshots
def delete_cgsnapshots_1234(self, **kw):
return (202, {}, None)
def get_share_group_snapshots_1234(self, **kw):
sg_snapshot = {'share_group_snapshot': self.fake_share_group_snapshot}
return 200, {}, sg_snapshot
def post_cgsnapshots_1234_action(self, **kw):
return (202, {}, None)
def put_share_group_snapshots_1234(self, **kwargs):
sg_snapshot = {
'share_group_snapshot': self.fake_share_group_snapshot,
}
return 200, {}, sg_snapshot
def post_cgsnapshots(self, body, **kw):
return (202, {}, {
'cgsnapshot': {
def delete_share_group_snapshots_1234(self, **kw):
return 202, {}, None
def post_share_group_snapshots_1234_action(self, **kw):
return 202, {}, None
def post_share_group_snapshots(self, body, **kw):
sg_snapshot = {
'share_group_snapshot': {
'id': 3,
'name': 'cust_snapshot',
}
})
}
return 202, {}, sg_snapshot
fake_share_replica = {
"id": "5678",
@ -953,6 +942,114 @@ class FakeHTTPClient(fakes.FakeHTTPClient):
raise AssertionError("Unexpected share action: %s" % action)
return (resp, {}, _body)
def get_share_group_types_default(self, **kw):
return self.get_share_group_types_1(**kw)
def get_share_group_types(self, **kw):
share_group_types = {
'share_group_types': [
{
'id': 1,
'name': 'test-group-type-1',
'group_specs': {
'key1': 'value1',
},
'share_types': [
'type1',
'type2',
],
'is_public': True,
}, {
'id': 2,
'name': 'test-type-2',
'group_specs': {
'key2': 'value2',
},
'share_types': [
'type3',
'type4',
],
'is_public': False,
},
],
}
return 200, {}, share_group_types
def get_share_group_types_1(self, **kw):
share_group_type = {
'share_group_type': {
'id': 1,
'name': 'test-group-type-1',
'group_specs': {
'key1': 'value1',
},
'share_types': [
'type1',
'type2',
],
'is_public': True,
},
}
return 200, {}, share_group_type
def get_share_group_types_2(self, **kw):
share_group_type = {
'share_type': {
'id': 2,
'name': 'test-group-type-2',
'group_specs': {
'key2': 'value2',
},
'share_types': [
'type3',
'type4',
],
'is_public': True,
},
}
return 200, {}, share_group_type
def post_share_group_types(self, body, **kw):
share_group_type = {
'share_group_type': {
'id': 1,
'name': 'test-group-type-1',
'share_types': body['share_group_type']['share_types'],
'is_public': True,
},
}
return 202, {}, share_group_type
def post_share_group_types_1234_action(self, body, **kw):
assert len(list(body)) == 1
action = list(body)[0]
if action == 'addProjectAccess':
assert 'project' in body['addProjectAccess']
elif action == 'removeProjectAccess':
assert 'project' in body['removeProjectAccess']
else:
raise AssertionError('Unexpected action: %s' % action)
return 202, {}, None
def post_share_group_types_1_specs(self, body, **kw):
assert list(body) == ['group_specs']
return 200, {}, {'group_specs': {'k': 'v'}}
def delete_share_group_types_1_specs_k(self, **kw):
return 204, {}, None
def delete_share_group_types_1234(self, **kw):
return 202, {}, None
def get_share_group_types_1234_access(self, **kw):
sg_type_access = {
'share_group_type_access': [{
'group_type_id': '11111111-1111-1111-1111-111111111111',
'project_id': '00000000-0000-0000-0000-000000000000',
}],
}
return 200, {}, sg_type_access
def fake_create(url, body, response_key):
return {'url': url, 'body': body, 'resp_key': response_key}
@ -969,3 +1066,43 @@ class FakeQuotaSet(object):
def to_dict(self):
return self.dictionary
class ShareNetwork(object):
id = 'fake share network id'
name = 'fake share network name'
class ShareType(object):
id = 'fake share type id'
name = 'fake share type name'
class ShareGroupType(object):
id = 'fake group type id'
name = 'fake group type name'
share_types = [ShareType().id]
is_public = False
class ShareGroupTypeAccess(object):
id = 'fake group type access id'
name = 'fake group type access name'
class ShareGroup(object):
id = 'fake group id'
share_types = [ShareType().id]
group_type_id = ShareGroupType().id
share_network_id = ShareNetwork().id
name = 'fake name'
description = 'fake description'
availability_zone = 'fake az'
class ShareGroupSnapshot(object):
id = 'fake group snapshot id'
share_group_id = ShareGroup().id,
share_network_id = ShareNetwork().id
name = 'fake name'
description = 'fake description'

View File

@ -1,183 +0,0 @@
# Copyright 2015 Chuck Fouts.
# 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 mock
import manilaclient
from manilaclient import exceptions
from manilaclient.tests.unit import utils
from manilaclient.tests.unit.v2 import fakes
from manilaclient.v2 import consistency_group_snapshots as cg_snapshots
FAKE_CG = 'fake cg snapshot'
FAKE_CG_ID = 'fake-cg-id'
class ConsistencyGroupSnapshotsTest(utils.TestCase):
class _FakeConsistencyGroupSnapshot(object):
id = 'fake_cg_snapshot_id'
def setUp(self):
super(ConsistencyGroupSnapshotsTest, self).setUp()
self.manager = cg_snapshots.ConsistencyGroupSnapshotManager(
api=fakes.FakeClient())
self.values = {
'consistency_group_id': 'fake_cg_id',
'name': 'fake snapshot name',
'description': 'new cg snapshot',
}
def test_snapshot_create(self):
body_expected = {cg_snapshots.RESOURCE_NAME: self.values}
mock_create = mock.Mock()
mock_create.side_effect = fakes.fake_create
with mock.patch.object(self.manager, '_create', mock_create):
result = self.manager.create(**self.values)
self.manager._create.assert_called_once_with(
cg_snapshots.RESOURCES_PATH,
body_expected,
cg_snapshots.RESOURCE_NAME)
self.assertEqual(result['url'], cg_snapshots.RESOURCES_PATH)
self.assertEqual(result['resp_key'], cg_snapshots.RESOURCE_NAME)
self.assertEqual(result['body'], body_expected)
def test_snapshot_create_invalid_version(self):
self.manager.api.api_version = manilaclient.API_MIN_VERSION
self.assertRaises(
exceptions.UnsupportedVersion, self.manager.create, **self.values)
def test_snapshot_get(self):
with mock.patch.object(self.manager, '_get', mock.Mock()):
self.manager.get(FAKE_CG_ID)
self.manager._get.assert_called_once_with(
cg_snapshots.RESOURCE_PATH % FAKE_CG_ID,
cg_snapshots.RESOURCE_NAME)
def test_snapshot_update_str(self):
body_expected = {cg_snapshots.RESOURCE_NAME: self.values}
mock_update = mock.Mock()
mock_update.side_effect = fakes.fake_update
with mock.patch.object(self.manager, '_update', mock_update):
result = self.manager.update(FAKE_CG, **self.values)
self.manager._update.assert_called_once_with(
cg_snapshots.RESOURCES_PATH + '/' + FAKE_CG,
body_expected,
cg_snapshots.RESOURCE_NAME
)
self.assertEqual(
result['url'],
cg_snapshots.RESOURCE_PATH % FAKE_CG)
self.assertEqual(
result['resp_key'],
cg_snapshots.RESOURCE_NAME)
self.assertEqual(result['body'], body_expected)
def test_snapshot_update_obj(self):
cg_snapshot = self._FakeConsistencyGroupSnapshot()
body_expected = {cg_snapshots.RESOURCE_NAME: self.values}
mock_update = mock.Mock()
mock_update.side_effect = fakes.fake_update
with mock.patch.object(self.manager, '_update', mock_update):
result = self.manager.update(cg_snapshot, **self.values)
self.manager._update.assert_called_once_with(
cg_snapshots.RESOURCES_PATH + '/' + cg_snapshot.id,
body_expected,
cg_snapshots.RESOURCE_NAME
)
self.assertEqual(
result['url'],
cg_snapshots.RESOURCE_PATH % cg_snapshot.id)
self.assertEqual(
result['resp_key'],
cg_snapshots.RESOURCE_NAME)
self.assertEqual(result['body'], body_expected)
def test_snapshot_list_not_detailed(self):
with mock.patch.object(self.manager, '_list',
mock.Mock(return_value=None)):
self.manager.list(detailed=False)
self.manager._list.assert_called_once_with(
cg_snapshots.RESOURCES_PATH,
cg_snapshots.RESOURCES_NAME)
def test_snapshot_list(self):
with mock.patch.object(self.manager, '_list',
mock.Mock(return_value=None)):
self.manager.list()
self.manager._list.assert_called_once_with(
cg_snapshots.RESOURCES_PATH + '/detail',
cg_snapshots.RESOURCES_NAME)
def test_snapshot_list_with_filters(self):
filters = {'all_tenants': 1, 'status': 'ERROR'}
expected_path = ("%s/detail?all_tenants=1&status="
"ERROR" % cg_snapshots.RESOURCES_PATH)
with mock.patch.object(self.manager, '_list',
mock.Mock(return_value=None)):
self.manager.list(search_opts=filters)
self.manager._list.assert_called_once_with(
expected_path,
cg_snapshots.RESOURCES_NAME)
def test_snapshot_members(self):
with mock.patch.object(self.manager, '_list',
mock.Mock(return_value=None)):
self.manager.members(FAKE_CG_ID)
self.manager._list.assert_called_once_with(
cg_snapshots.RESOURCES_PATH + '/' + FAKE_CG_ID + '/members',
'cgsnapshot_members')
def test_snapshot_members_with_filters(self):
search_opts = {'fake_str': 'fake_str_value', 'fake_int': 1}
query_str = FAKE_CG_ID + '/members?fake_int=1&fake_str=fake_str_value'
with mock.patch.object(self.manager, '_list',
mock.Mock(return_value=None)):
self.manager.members(FAKE_CG_ID, search_opts=search_opts)
self.manager._list.assert_called_once_with(
cg_snapshots.RESOURCES_PATH + '/' + query_str,
cg_snapshots.MEMBERS_RESOURCE_NAME)
def test_snapshot_members_with_offset(self):
with mock.patch.object(self.manager, '_list',
mock.Mock(return_value=None)):
keys = {'offset': 22}
url = FAKE_CG_ID + '/members?offset=22'
self.manager.members(FAKE_CG_ID, keys)
self.manager._list.assert_called_once_with(
cg_snapshots.RESOURCES_PATH + '/' + url,
cg_snapshots.MEMBERS_RESOURCE_NAME)
def test_snapshot_delete_str(self):
fake_cg_snapshot = 'fake cgsnapshot'
with mock.patch.object(self.manager, '_delete', mock.Mock()):
self.manager.delete(fake_cg_snapshot)
self.manager._delete.assert_called_once_with(
cg_snapshots.RESOURCE_PATH % fake_cg_snapshot)
def test_snapshot_delete_obj(self):
cg_snapshot = self._FakeConsistencyGroupSnapshot()
with mock.patch.object(self.manager, '_delete', mock.Mock()):
self.manager.delete(cg_snapshot)
self.manager._delete.assert_called_once_with(
cg_snapshots.RESOURCE_PATH % cg_snapshot.id)

View File

@ -1,161 +0,0 @@
# Copyright 2015 Chuck Fouts.
# 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 mock
import manilaclient
from manilaclient import exceptions
from manilaclient.tests.unit import utils
from manilaclient.tests.unit.v2 import fakes
from manilaclient.v2 import consistency_groups
FAKE_CG = 'fake consistency group'
class ConsistencyGroupsTest(utils.TestCase):
class _FakeConsistencyGroupSnapshot(object):
id = 'fake_cg_snapshot_id'
def setUp(self):
super(ConsistencyGroupsTest, self).setUp()
self.manager = consistency_groups.ConsistencyGroupManager(
api=fakes.FakeClient())
self.values = {'name': 'fake name', 'description': 'new cg'}
def test_create(self):
body_expected = {consistency_groups.RESOURCE_NAME: self.values}
mock_create = mock.Mock()
mock_create.side_effect = fakes.fake_create
with mock.patch.object(self.manager, '_create', mock_create):
result = self.manager.create(**self.values)
self.manager._create.assert_called_once_with(
consistency_groups.RESOURCES_PATH,
body_expected,
consistency_groups.RESOURCE_NAME
)
self.assertEqual(result['url'], consistency_groups.RESOURCES_PATH)
self.assertEqual(result['resp_key'],
consistency_groups.RESOURCE_NAME)
self.assertEqual(result['body'], body_expected)
def test_invalid_create(self):
self.manager.api.api_version = manilaclient.API_MIN_VERSION
self.assertRaises(
exceptions.UnsupportedVersion, self.manager.create, **self.values)
def test_create_with_share_network_id(self):
resource_name = consistency_groups.RESOURCE_NAME
body_expected = {resource_name: dict(list(self.values.items()))}
body_expected[resource_name]['share_network_id'] = '050505050505'
self.values['share_network'] = '050505050505'
mock_create = mock.Mock()
mock_create.side_effect = fakes.fake_create
with mock.patch.object(self.manager, '_create', mock_create):
result = self.manager.create(**self.values)
self.manager._create.assert_called_once_with(
consistency_groups.RESOURCES_PATH,
body_expected,
consistency_groups.RESOURCE_NAME
)
self.assertEqual(result['url'], consistency_groups.RESOURCES_PATH)
self.assertEqual(result['resp_key'], resource_name)
self.assertEqual(result['body'], body_expected)
def test_list_not_detailed(self):
with mock.patch.object(self.manager, '_list',
mock.Mock(return_value=None)):
self.manager.list(detailed=False)
self.manager._list.assert_called_once_with(
consistency_groups.RESOURCES_PATH,
consistency_groups.RESOURCES_NAME)
def test_list(self):
with mock.patch.object(self.manager, '_list',
mock.Mock(return_value=None)):
self.manager.list()
self.manager._list.assert_called_once_with(
consistency_groups.RESOURCES_PATH + '/detail',
consistency_groups.RESOURCES_NAME)
def test_list_with_filters(self):
filters = {'all_tenants': 1, 'status': 'ERROR'}
expected_path = ("%s/detail?all_tenants=1&status="
"ERROR" % consistency_groups.RESOURCES_PATH)
with mock.patch.object(self.manager, '_list',
mock.Mock(return_value=None)):
self.manager.list(search_opts=filters)
self.manager._list.assert_called_once_with(
expected_path,
consistency_groups.RESOURCES_NAME)
def test_delete_str(self):
with mock.patch.object(self.manager, '_delete', mock.Mock()):
self.manager.delete(FAKE_CG)
self.manager._delete.assert_called_once_with(
consistency_groups.RESOURCE_PATH % FAKE_CG)
def test_get(self):
with mock.patch.object(self.manager, '_get', mock.Mock()):
self.manager.get(FAKE_CG)
self.manager._get.assert_called_once_with(
consistency_groups.RESOURCE_PATH % FAKE_CG,
consistency_groups.RESOURCE_NAME)
def test_update_str(self):
body_expected = {
consistency_groups.RESOURCE_NAME: self.values}
mock_update = mock.Mock()
mock_update.side_effect = fakes.fake_update
with mock.patch.object(self.manager, '_update', mock_update):
result = self.manager.update(FAKE_CG, **self.values)
self.manager._update.assert_called_once_with(
consistency_groups.RESOURCES_PATH + '/' + FAKE_CG,
body_expected,
consistency_groups.RESOURCE_NAME
)
self.assertEqual(
result['url'],
consistency_groups.RESOURCE_PATH % FAKE_CG)
self.assertEqual(
result['resp_key'],
consistency_groups.RESOURCE_NAME)
self.assertEqual(result['body'], body_expected)
def test_snapshot_update_obj(self):
cg_snapshot = self._FakeConsistencyGroupSnapshot()
body_expected = {consistency_groups.RESOURCE_NAME: self.values}
mock_update = mock.Mock()
mock_update.side_effect = fakes.fake_update
with mock.patch.object(self.manager, '_update', mock_update):
result = self.manager.update(cg_snapshot, **self.values)
self.manager._update.assert_called_once_with(
consistency_groups.RESOURCES_PATH + '/' + cg_snapshot.id,
body_expected,
consistency_groups.RESOURCE_NAME
)
self.assertEqual(
result['url'],
consistency_groups.RESOURCE_PATH % cg_snapshot.id)
self.assertEqual(
result['resp_key'],
consistency_groups.RESOURCE_NAME)
self.assertEqual(result['body'], body_expected)

View File

@ -0,0 +1,265 @@
# Copyright 2016 Clinton Knight
# 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 mock
import ddt
import six
import manilaclient
from manilaclient import exceptions
from manilaclient.tests.unit import utils
from manilaclient.tests.unit.v2 import fakes as fake
from manilaclient.v2 import share_group_snapshots as snapshots
class ShareGroupSnapshotTest(utils.TestCase):
def setUp(self):
super(ShareGroupSnapshotTest, self).setUp()
self.manager = snapshots.ShareGroupSnapshotManager(fake.FakeClient())
self.share_group_snapshot = snapshots.ShareGroupSnapshot(
self.manager, {'id': 'fake_id'})
self.fake_kwargs = {'key': 'value'}
def test_repr(self):
result = six.text_type(self.share_group_snapshot)
self.assertEqual('<Share Group Snapshot: fake_id>', result)
def test_update(self):
mock_manager_update = self.mock_object(self.manager, 'update')
self.share_group_snapshot.update(**self.fake_kwargs)
mock_manager_update.assert_called_once_with(
self.share_group_snapshot, **self.fake_kwargs)
def test_delete(self):
mock_manager_delete = self.mock_object(self.manager, 'delete')
self.share_group_snapshot.delete()
mock_manager_delete.assert_called_once_with(self.share_group_snapshot)
def test_reset_state(self):
mock_manager_reset_state = self.mock_object(
self.manager, 'reset_state')
self.share_group_snapshot.reset_state('fake_state')
mock_manager_reset_state.assert_called_once_with(
self.share_group_snapshot, 'fake_state')
@ddt.ddt
class ShareGroupSnapshotManagerTest(utils.TestCase):
def setUp(self):
super(ShareGroupSnapshotManagerTest, self).setUp()
self.manager = snapshots.ShareGroupSnapshotManager(fake.FakeClient())
def test_create(self):
fake_share_group_snapshot = fake.ShareGroupSnapshot()
mock_create = self.mock_object(
self.manager, '_create',
mock.Mock(return_value=fake_share_group_snapshot))
create_args = {
'name': fake.ShareGroupSnapshot.name,
'description': fake.ShareGroupSnapshot.description,
}
result = self.manager.create(fake.ShareGroupSnapshot, **create_args)
self.assertIs(fake_share_group_snapshot, result)
expected_body = {
snapshots.RESOURCE_NAME: {
'name': fake.ShareGroupSnapshot.name,
'description': fake.ShareGroupSnapshot.description,
'share_group_id': fake.ShareGroupSnapshot().id,
},
}
mock_create.assert_called_once_with(
snapshots.RESOURCES_PATH, expected_body, snapshots.RESOURCE_NAME)
def test_create_minimal_args(self):
fake_share_group_snapshot = fake.ShareGroupSnapshot()
mock_create = self.mock_object(
self.manager, '_create',
mock.Mock(return_value=fake_share_group_snapshot))
result = self.manager.create(fake.ShareGroupSnapshot)
self.assertIs(fake_share_group_snapshot, result)
expected_body = {
snapshots.RESOURCE_NAME: {
'share_group_id': fake.ShareGroupSnapshot().id,
},
}
mock_create.assert_called_once_with(
snapshots.RESOURCES_PATH, expected_body, snapshots.RESOURCE_NAME)
def test_create_using_unsupported_microversion(self):
self.manager.api.api_version = manilaclient.API_MIN_VERSION
self.assertRaises(
exceptions.UnsupportedVersion,
self.manager.create, fake.ShareGroupSnapshot)
def test_get(self):
fake_share_group_snapshot = fake.ShareGroupSnapshot()
mock_get = self.mock_object(
self.manager, '_get',
mock.Mock(return_value=fake_share_group_snapshot))
result = self.manager.get(fake.ShareGroupSnapshot.id)
self.assertIs(fake_share_group_snapshot, result)
mock_get.assert_called_once_with(
snapshots.RESOURCE_PATH % fake.ShareGroupSnapshot.id,
snapshots.RESOURCE_NAME)
def test_list(self):
fake_share_group_snapshot = fake.ShareGroupSnapshot()
mock_list = self.mock_object(
self.manager, '_list',
mock.Mock(return_value=[fake_share_group_snapshot]))
result = self.manager.list()
self.assertEqual([fake_share_group_snapshot], result)
mock_list.assert_called_once_with(
snapshots.RESOURCES_PATH + '/detail',
snapshots.RESOURCES_NAME)
def test_list_no_detail(self):
fake_share_group_snapshot = fake.ShareGroupSnapshot()
mock_list = self.mock_object(
self.manager, '_list',
mock.Mock(return_value=[fake_share_group_snapshot]))
result = self.manager.list(detailed=False)
self.assertEqual([fake_share_group_snapshot], result)
mock_list.assert_called_once_with(
snapshots.RESOURCES_PATH, snapshots.RESOURCES_NAME)
def test_list_with_filters(self):
fake_share_group_snapshot = fake.ShareGroupSnapshot()
mock_list = self.mock_object(
self.manager, '_list',
mock.Mock(return_value=[fake_share_group_snapshot]))
filters = {'all_tenants': 1, 'status': 'ERROR'}
result = self.manager.list(detailed=False, search_opts=filters)
self.assertEqual([fake_share_group_snapshot], result)
expected_path = (snapshots.RESOURCES_PATH +
'?all_tenants=1&status=ERROR')
mock_list.assert_called_once_with(
expected_path, snapshots.RESOURCES_NAME)
def test_list_with_sorting(self):
fake_share_group_snapshot = fake.ShareGroupSnapshot()
mock_list = self.mock_object(
self.manager, '_list',
mock.Mock(return_value=[fake_share_group_snapshot]))
result = self.manager.list(
detailed=False, sort_dir='asc', sort_key='name')
self.assertEqual([fake_share_group_snapshot], result)
expected_path = (
snapshots.RESOURCES_PATH + '?sort_dir=asc&sort_key=name')
mock_list.assert_called_once_with(
expected_path, snapshots.RESOURCES_NAME)
@ddt.data({'sort_key': 'name', 'sort_dir': 'invalid'},
{'sort_key': 'invalid', 'sort_dir': 'asc'})
@ddt.unpack
def test_list_with_invalid_sorting(self, sort_key, sort_dir):
self.assertRaises(
ValueError,
self.manager.list, sort_dir=sort_dir, sort_key=sort_key)
def test_update(self):
fake_share_group_snapshot = fake.ShareGroupSnapshot()
mock_get = self.mock_object(
self.manager, '_get',
mock.Mock(return_value=fake_share_group_snapshot))
mock_update = self.mock_object(
self.manager, '_update',
mock.Mock(return_value=fake_share_group_snapshot))
update_args = {
'name': fake.ShareGroupSnapshot.name,
'description': fake.ShareGroupSnapshot.description,
}
result = self.manager.update(fake.ShareGroupSnapshot(), **update_args)
self.assertIs(fake_share_group_snapshot, result)
self.assertFalse(mock_get.called)
mock_update.assert_called_once_with(
snapshots.RESOURCE_PATH % fake.ShareGroupSnapshot.id,
{snapshots.RESOURCE_NAME: update_args},
snapshots.RESOURCE_NAME)
def test_update_no_data(self):
fake_share_group_snapshot = fake.ShareGroupSnapshot()
mock_get = self.mock_object(
self.manager, '_get',
mock.Mock(return_value=fake_share_group_snapshot))
mock_update = self.mock_object(
self.manager, '_update',
mock.Mock(return_value=fake_share_group_snapshot))
update_args = {}
result = self.manager.update(fake.ShareGroupSnapshot(), **update_args)
self.assertIs(fake_share_group_snapshot, result)
mock_get.assert_called_once_with(
snapshots.RESOURCE_PATH % fake.ShareGroupSnapshot.id,
snapshots.RESOURCE_NAME)
self.assertFalse(mock_update.called)
def test_delete(self):
mock_delete = self.mock_object(self.manager, '_delete')
mock_post = self.mock_object(self.manager.api.client, 'post')
self.manager.delete(fake.ShareGroupSnapshot())
mock_delete.assert_called_once_with(
snapshots.RESOURCE_PATH % fake.ShareGroupSnapshot.id)
self.assertFalse(mock_post.called)
def test_delete_force(self):
mock_delete = self.mock_object(self.manager, '_delete')
mock_post = self.mock_object(self.manager.api.client, 'post')
self.manager.delete(fake.ShareGroupSnapshot.id, force=True)
self.assertFalse(mock_delete.called)
mock_post.assert_called_once_with(
snapshots.RESOURCE_PATH_ACTION % fake.ShareGroupSnapshot.id,
body={'force_delete': None})
def test_reset_state(self):
mock_post = self.mock_object(self.manager.api.client, 'post')
self.manager.reset_state(fake.ShareGroupSnapshot(), 'fake_state')
mock_post.assert_called_once_with(
snapshots.RESOURCE_PATH_ACTION % fake.ShareGroupSnapshot.id,
body={'reset_status': {'status': 'fake_state'}})

View File

@ -0,0 +1,116 @@
# Copyright 2016 Clinton Knight
# 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 mock
import ddt
import six
import manilaclient
from manilaclient import exceptions
from manilaclient.tests.unit import utils
from manilaclient.tests.unit.v2 import fakes as fake
from manilaclient.v2 import share_group_type_access as type_access
@ddt.ddt
class ShareGroupTypeAccessTest(utils.TestCase):
def setUp(self):
super(ShareGroupTypeAccessTest, self).setUp()
self.manager = type_access.ShareGroupTypeAccessManager(
fake.FakeClient())
fake_group_type_access_info = {'id': fake.ShareGroupTypeAccess.id}
self.share_group_type_access = type_access.ShareGroupTypeAccess(
self.manager, fake_group_type_access_info, loaded=True)
def test_repr(self):
result = six.text_type(self.share_group_type_access)
self.assertEqual(
'<Share Group Type Access: %s>' % fake.ShareGroupTypeAccess.id,
result)
@ddt.ddt
class ShareGroupTypeAccessManagerTest(utils.TestCase):
def setUp(self):
super(ShareGroupTypeAccessManagerTest, self).setUp()
self.manager = type_access.ShareGroupTypeAccessManager(
fake.FakeClient())
def test_list(self):
fake_share_group_type_access = fake.ShareGroupTypeAccess()
mock_list = self.mock_object(
self.manager, '_list',
mock.Mock(return_value=[fake_share_group_type_access]))
result = self.manager.list(fake.ShareGroupType())
self.assertEqual([fake_share_group_type_access], result)
mock_list.assert_called_once_with(
type_access.RESOURCE_PATH % fake.ShareGroupType.id,
type_access.RESOURCE_NAME)
def test_list_public(self):
fake_share_group_type_access = fake.ShareGroupTypeAccess()
mock_list = self.mock_object(
self.manager, '_list',
mock.Mock(return_value=[fake_share_group_type_access]))
fake_share_group_type = fake.ShareGroupType()
fake_share_group_type.is_public = True
result = self.manager.list(fake_share_group_type)
self.assertIsNone(result)
self.assertFalse(mock_list.called)
def test_list_using_unsupported_microversion(self):
fake_share_group_type_access = fake.ShareGroupTypeAccess()
self.manager.api.api_version = manilaclient.API_MIN_VERSION
self.assertRaises(
exceptions.UnsupportedVersion,
self.manager.list, fake_share_group_type_access)
def test_add_project_access(self):
mock_post = self.mock_object(self.manager.api.client, 'post')
self.manager.add_project_access(fake.ShareGroupType(), 'fake_project')
expected_body = {
'addProjectAccess': {
'project': 'fake_project',
}
}
mock_post.assert_called_once_with(
type_access.RESOURCE_PATH_ACTION % fake.ShareGroupType.id,
body=expected_body)
def test_remove_project_access(self):
mock_post = self.mock_object(self.manager.api.client, 'post')
self.manager.remove_project_access(
fake.ShareGroupType(), 'fake_project')
expected_body = {
'removeProjectAccess': {
'project': 'fake_project',
}
}
mock_post.assert_called_once_with(
type_access.RESOURCE_PATH_ACTION % fake.ShareGroupType.id,
body=expected_body)

View File

@ -0,0 +1,215 @@
# Copyright 2016 Clinton Knight
# 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 mock
import ddt
import six
import manilaclient
from manilaclient import exceptions
from manilaclient.tests.unit import utils
from manilaclient.tests.unit.v2 import fakes as fake
from manilaclient.v2 import share_group_types as types
@ddt.ddt
class ShareGroupTypeTest(utils.TestCase):
def setUp(self):
super(ShareGroupTypeTest, self).setUp()
self.manager = types.ShareGroupTypeManager(fake.FakeClient())
self.fake_group_specs = {'key1': 'value1', 'key2': 'value2'}
self.fake_share_group_type_info = {
'id': fake.ShareGroupType.id,
'share_types': [fake.ShareType.id],
'name': fake.ShareGroupType.name,
'is_public': fake.ShareGroupType.is_public,
'group_specs': self.fake_group_specs,
}
self.share_group_type = types.ShareGroupType(
self.manager, self.fake_share_group_type_info, loaded=True)
def test_repr(self):
result = six.text_type(self.share_group_type)
self.assertEqual(
'<Share Group Type: %s>' % fake.ShareGroupType.name, result)
@ddt.data((True, True), (False, False), (None, 'N/A'))
@ddt.unpack
def test_is_public(self, is_public, expected):
fake_share_group_type_info = {'name': 'fake_name'}
if is_public is not None:
fake_share_group_type_info['is_public'] = is_public
share_group_type = types.ShareGroupType(
self.manager, fake_share_group_type_info, loaded=True)
result = share_group_type.is_public
self.assertEqual(expected, result)
def test_get_keys(self):
self.mock_object(self.manager.api.client, 'get')
result = self.share_group_type.get_keys()
self.assertEqual(self.fake_group_specs, result)
self.assertFalse(self.manager.api.client.get.called)
def test_get_keys_force_api_call(self):
share_group_type = types.ShareGroupType(
self.manager, self.fake_share_group_type_info, loaded=True)
share_group_type._group_specs = {}
self.manager.api.client.get = mock.Mock(return_value=(
None, self.fake_share_group_type_info))
result = share_group_type.get_keys(prefer_resource_data=False)
self.assertEqual(self.fake_group_specs, result)
self.manager.api.client.get.assert_called_once_with(
types.GROUP_SPECS_RESOURCES_PATH % fake.ShareGroupType.id)
def test_set_keys(self):
mock_manager_create = self.mock_object(
self.manager, '_create',
mock.Mock(return_value=self.fake_group_specs))
result = self.share_group_type.set_keys(self.fake_group_specs)
self.assertEqual(result, self.fake_group_specs)
expected_body = {'group_specs': self.fake_group_specs}
mock_manager_create.assert_called_once_with(
types.GROUP_SPECS_RESOURCES_PATH % fake.ShareGroupType.id,
expected_body, types.GROUP_SPECS_RESOURCES_NAME, return_raw=True)
def test_unset_keys(self):
mock_manager_delete = self.mock_object(
self.manager, '_delete', mock.Mock(return_value=None))
result = self.share_group_type.unset_keys(self.fake_group_specs.keys())
self.assertIsNone(result)
mock_manager_delete.assert_has_calls([
mock.call(types.GROUP_SPECS_RESOURCE_PATH %
(fake.ShareGroupType.id, 'key1')),
mock.call(types.GROUP_SPECS_RESOURCE_PATH %
(fake.ShareGroupType.id, 'key2')),
], any_order=True)
def test_unset_keys_error(self):
mock_manager_delete = self.mock_object(
self.manager, '_delete', mock.Mock(return_value='error'))
result = self.share_group_type.unset_keys(
sorted(self.fake_group_specs.keys()))
self.assertEqual('error', result)
mock_manager_delete.assert_called_once_with(
types.GROUP_SPECS_RESOURCE_PATH % (fake.ShareGroupType.id, 'key1'))
@ddt.ddt
class ShareGroupTypeManagerTest(utils.TestCase):
def setUp(self):
super(ShareGroupTypeManagerTest, self).setUp()
self.manager = types.ShareGroupTypeManager(fake.FakeClient())
self.fake_group_specs = {'key1': 'value1', 'key2': 'value2'}
def test_create(self):
fake_share_group_type = fake.ShareGroupType()
mock_create = self.mock_object(
self.manager, '_create',
mock.Mock(return_value=fake_share_group_type))
create_args = {
'name': fake.ShareGroupType.name,
'share_types': [fake.ShareType()],
'is_public': False,
'group_specs': self.fake_group_specs,
}
result = self.manager.create(**create_args)
self.assertIs(fake_share_group_type, result)
expected_body = {
types.RESOURCE_NAME: {
'name': fake.ShareGroupType.name,
'share_types': [fake.ShareType().id],
'is_public': False,
'group_specs': self.fake_group_specs,
},
}
mock_create.assert_called_once_with(
types.RESOURCES_PATH, expected_body, types.RESOURCE_NAME)
def test_create_no_share_type(self):
create_args = {
'name': fake.ShareGroupType.name,
'share_types': [],
'is_public': False,
'group_specs': self.fake_group_specs,
}
self.assertRaises(ValueError, self.manager.create, **create_args)
def test_create_using_unsupported_microversion(self):
self.manager.api.api_version = manilaclient.API_MIN_VERSION
self.assertRaises(exceptions.UnsupportedVersion, self.manager.create)
def test_get(self):
fake_share_group_type = fake.ShareGroupType()
mock_get = self.mock_object(
self.manager, '_get',
mock.Mock(return_value=fake_share_group_type))
result = self.manager.get(fake.ShareGroupType.id)
self.assertIs(fake_share_group_type, result)
mock_get.assert_called_once_with(
types.RESOURCE_PATH % fake.ShareGroupType.id, types.RESOURCE_NAME)
def test_list(self):
fake_share_group_type = fake.ShareGroupType()
mock_list = self.mock_object(
self.manager, '_list',
mock.Mock(return_value=[fake_share_group_type]))
result = self.manager.list()
self.assertEqual([fake_share_group_type], result)
mock_list.assert_called_once_with(
types.RESOURCES_PATH + '?is_public=all', types.RESOURCES_NAME)
def test_list_no_public(self):
fake_share_group_type = fake.ShareGroupType()
mock_list = self.mock_object(
self.manager, '_list',
mock.Mock(return_value=[fake_share_group_type]))
result = self.manager.list(show_all=False)
self.assertEqual([fake_share_group_type], result)
mock_list.assert_called_once_with(
types.RESOURCES_PATH, types.RESOURCES_NAME)
def test_delete(self):
mock_delete = self.mock_object(self.manager, '_delete')
self.manager.delete(fake.ShareGroupType())
mock_delete.assert_called_once_with(
types.RESOURCE_PATH % fake.ShareGroupType.id)

View File

@ -0,0 +1,320 @@
# Copyright 2016 Clinton Knight
# 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 mock
import ddt
import six
import manilaclient
from manilaclient import exceptions
from manilaclient.tests.unit import utils
from manilaclient.tests.unit.v2 import fakes as fake
from manilaclient.v2 import share_groups
@ddt.ddt
class ShareGroupTest(utils.TestCase):
def setUp(self):
super(ShareGroupTest, self).setUp()
self.manager = share_groups.ShareGroupManager(fake.FakeClient())
self.share_group = share_groups.ShareGroup(
self.manager, {'id': 'fake_id'})
self.fake_kwargs = {'key': 'value'}
def test_repr(self):
result = six.text_type(self.share_group)
self.assertEqual('<Share Group: fake_id>', result)
def test_update(self):
mock_manager_update = self.mock_object(self.manager, 'update')
self.share_group.update(**self.fake_kwargs)
mock_manager_update.assert_called_once_with(
self.share_group, **self.fake_kwargs)
def test_delete(self):
mock_manager_delete = self.mock_object(self.manager, 'delete')
self.share_group.delete()
mock_manager_delete.assert_called_once_with(
self.share_group, force=False)
@ddt.data(True, False)
def test_delete_force(self, force):
mock_manager_delete = self.mock_object(self.manager, 'delete')
self.share_group.delete(force=force)
mock_manager_delete.assert_called_once_with(
self.share_group, force=force)
def test_reset_state(self):
mock_manager_reset_state = self.mock_object(
self.manager, 'reset_state')
self.share_group.reset_state('fake_state')
mock_manager_reset_state.assert_called_once_with(
self.share_group, 'fake_state')
@ddt.ddt
class ShareGroupManagerTest(utils.TestCase):
def setUp(self):
super(ShareGroupManagerTest, self).setUp()
self.manager = share_groups.ShareGroupManager(fake.FakeClient())
def test_create(self):
fake_share_group = fake.ShareGroup()
mock_create = self.mock_object(
self.manager, '_create', mock.Mock(return_value=fake_share_group))
create_args = {
'name': fake.ShareGroup.name,
'description': fake.ShareGroup.description,
'availability_zone': fake.ShareGroup.availability_zone,
'share_group_type': fake.ShareGroupType(),
'share_types': [fake.ShareType()],
'share_network': fake.ShareNetwork(),
}
result = self.manager.create(**create_args)
self.assertIs(fake_share_group, result)
expected_body = {
share_groups.RESOURCE_NAME: {
'name': fake.ShareGroup.name,
'description': fake.ShareGroup.description,
'share_group_type_id': fake.ShareGroupType().id,
'share_network_id': fake.ShareNetwork().id,
'share_types': [fake.ShareType().id],
'availability_zone': fake.ShareGroup.availability_zone,
},
}
mock_create.assert_called_once_with(
share_groups.RESOURCES_PATH,
expected_body,
share_groups.RESOURCE_NAME)
def test_create_default_type(self):
fake_share_group = fake.ShareGroup()
mock_create = self.mock_object(
self.manager, '_create', mock.Mock(return_value=fake_share_group))
create_args = {
'name': fake.ShareGroup.name,
'description': fake.ShareGroup.description,
'availability_zone': fake.ShareGroup.availability_zone,
}
result = self.manager.create(**create_args)
self.assertIs(fake_share_group, result)
expected_body = {share_groups.RESOURCE_NAME: create_args}
mock_create.assert_called_once_with(
share_groups.RESOURCES_PATH,
expected_body,
share_groups.RESOURCE_NAME)
def test_create_from_snapshot(self):
fake_share_group = fake.ShareGroup()
mock_create = self.mock_object(
self.manager, '_create', mock.Mock(return_value=fake_share_group))
create_args = {
'name': fake.ShareGroup.name,
'description': fake.ShareGroup.description,
'availability_zone': fake.ShareGroup.availability_zone,
'source_share_group_snapshot': fake.ShareGroupSnapshot(),
}
result = self.manager.create(**create_args)
self.assertIs(fake_share_group, result)
expected_body = {
share_groups.RESOURCE_NAME: {
'name': fake.ShareGroup.name,
'description': fake.ShareGroup.description,
'availability_zone': fake.ShareGroup.availability_zone,
'source_share_group_snapshot_id': fake.ShareGroupSnapshot().id,
},
}
mock_create.assert_called_once_with(
share_groups.RESOURCES_PATH,
expected_body,
share_groups.RESOURCE_NAME)
def test_create_using_unsupported_microversion(self):
self.manager.api.api_version = manilaclient.API_MIN_VERSION
self.assertRaises(exceptions.UnsupportedVersion, self.manager.create)
def test_create_invalid_arguments(self):
create_args = {
'name': fake.ShareGroup.name,
'description': fake.ShareGroup.description,
'share_types': [fake.ShareType().id],
'source_share_group_snapshot': fake.ShareGroupSnapshot(),
}
self.assertRaises(ValueError, self.manager.create, **create_args)
def test_get(self):
fake_share_group = fake.ShareGroup()
mock_get = self.mock_object(
self.manager, '_get', mock.Mock(return_value=fake_share_group))
result = self.manager.get(fake.ShareGroup.id)
self.assertIs(fake_share_group, result)
mock_get.assert_called_once_with(
share_groups.RESOURCE_PATH % fake.ShareGroup.id,
share_groups.RESOURCE_NAME)
def test_list(self):
fake_share_group = fake.ShareGroup()
mock_list = self.mock_object(
self.manager, '_list', mock.Mock(return_value=[fake_share_group]))
result = self.manager.list()
self.assertEqual([fake_share_group], result)
mock_list.assert_called_once_with(
share_groups.RESOURCES_PATH + '/detail',
share_groups.RESOURCES_NAME)
def test_list_no_detail(self):
fake_share_group = fake.ShareGroup()
mock_list = self.mock_object(
self.manager, '_list', mock.Mock(return_value=[fake_share_group]))
result = self.manager.list(detailed=False)
self.assertEqual([fake_share_group], result)
mock_list.assert_called_once_with(
share_groups.RESOURCES_PATH, share_groups.RESOURCES_NAME)
def test_list_with_filters(self):
fake_share_group = fake.ShareGroup()
mock_list = self.mock_object(
self.manager, '_list', mock.Mock(return_value=[fake_share_group]))
filters = {'all_tenants': 1}
result = self.manager.list(detailed=False, search_opts=filters)
self.assertEqual([fake_share_group], result)
expected_path = (share_groups.RESOURCES_PATH + '?all_tenants=1')
mock_list.assert_called_once_with(
expected_path, share_groups.RESOURCES_NAME)
@ddt.data(
('name', 'name'),
('share_group_type', 'share_group_type_id'),
('share_network', 'share_network_id'),
)
@ddt.unpack
def test_list_with_sorting(self, key, expected_key):
fake_share_group = fake.ShareGroup()
mock_list = self.mock_object(
self.manager, '_list', mock.Mock(return_value=[fake_share_group]))
result = self.manager.list(
detailed=False, sort_dir='asc', sort_key=key)
self.assertEqual([fake_share_group], result)
expected_path = (
share_groups.RESOURCES_PATH + '?sort_dir=asc&sort_key=' +
expected_key)
mock_list.assert_called_once_with(
expected_path, share_groups.RESOURCES_NAME)
@ddt.data(
('name', 'invalid'),
('invalid', 'asc'),
)
@ddt.unpack
def test_list_with_invalid_sorting(self, sort_key, sort_dir):
self.assertRaises(
ValueError,
self.manager.list, sort_dir=sort_dir, sort_key=sort_key)
def test_update(self):
fake_share_group = fake.ShareGroup()
mock_get = self.mock_object(
self.manager, '_get', mock.Mock(return_value=fake_share_group))
mock_update = self.mock_object(
self.manager, '_update', mock.Mock(return_value=fake_share_group))
update_args = {
'name': fake.ShareGroup.name,
'description': fake.ShareGroup.description,
}
result = self.manager.update(fake.ShareGroup(), **update_args)
self.assertIs(fake_share_group, result)
self.assertFalse(mock_get.called)
mock_update.assert_called_once_with(
share_groups.RESOURCE_PATH % fake.ShareGroup.id,
{share_groups.RESOURCE_NAME: update_args},
share_groups.RESOURCE_NAME)
def test_update_no_data(self):
fake_share_group = fake.ShareGroup()
mock_get = self.mock_object(
self.manager, '_get', mock.Mock(return_value=fake_share_group))
mock_update = self.mock_object(
self.manager, '_update', mock.Mock(return_value=fake_share_group))
update_args = {}
result = self.manager.update(fake.ShareGroup(), **update_args)
self.assertIs(fake_share_group, result)
mock_get.assert_called_once_with(
share_groups.RESOURCE_PATH % fake.ShareGroup.id,
share_groups.RESOURCE_NAME)
self.assertFalse(mock_update.called)
def test_delete(self):
mock_delete = self.mock_object(self.manager, '_delete')
mock_post = self.mock_object(self.manager.api.client, 'post')
self.manager.delete(fake.ShareGroup())
mock_delete.assert_called_once_with(
share_groups.RESOURCE_PATH % fake.ShareGroup.id)
self.assertFalse(mock_post.called)
def test_delete_force(self):
mock_delete = self.mock_object(self.manager, '_delete')
mock_post = self.mock_object(self.manager.api.client, 'post')
self.manager.delete(fake.ShareGroup.id, force=True)
self.assertFalse(mock_delete.called)
mock_post.assert_called_once_with(
share_groups.RESOURCE_PATH_ACTION % fake.ShareGroup.id,
body={'force_delete': None})
def test_reset_state(self):
mock_post = self.mock_object(self.manager.api.client, 'post')
self.manager.reset_state(fake.ShareGroup(), 'fake_state')
mock_post.assert_called_once_with(
share_groups.RESOURCE_PATH_ACTION % fake.ShareGroup.id,
body={'reset_status': {'status': 'fake_state'}})

View File

@ -66,7 +66,6 @@ class SharesTest(utils.TestCase):
'share_type': None,
'is_public': False,
'availability_zone': None,
'consistency_group_id': None,
}
cs.shares.create(protocol, 1)
cs.assert_called('POST', '/shares', {'share': expected})
@ -87,7 +86,6 @@ class SharesTest(utils.TestCase):
'share_type': None,
'is_public': False,
'availability_zone': None,
'consistency_group_id': None,
}
cs.shares.create('nfs', 1, share_network=share_network)
cs.assert_called('POST', '/shares', {'share': expected})
@ -108,7 +106,6 @@ class SharesTest(utils.TestCase):
'share_type': 'fake_st',
'is_public': False,
'availability_zone': None,
'consistency_group_id': None,
}
cs.shares.create('nfs', 1, share_type=share_type)
cs.assert_called('POST', '/shares', {'share': expected})
@ -132,7 +129,6 @@ class SharesTest(utils.TestCase):
'share_network_id': None,
'size': 1,
'availability_zone': availability_zone,
'consistency_group_id': None,
}
}
cs.shares.create('nfs', 1, is_public=is_public,

View File

@ -68,7 +68,6 @@ class ShellTest(test_utils.TestCase):
self.separators = [' ', '=']
self.create_share_body = {
"share": {
"consistency_group_id": None,
"share_type": None,
"name": None,
"snapshot_id": None,
@ -637,17 +636,16 @@ class ShellTest(test_utils.TestCase):
self.assert_called('DELETE', '/shares/1234')
@ddt.data(
'--cg 1234', '--consistency-group 1234', '--consistency_group 1234')
@mock.patch.object(shell_v2, '_find_consistency_group', mock.Mock())
def test_delete_with_cg(self, cg_cmd):
fcg = type(
'FakeConsistencyGroup', (object,), {'id': cg_cmd.split()[-1]})
shell_v2._find_consistency_group.return_value = fcg
'--group sg1313', '--share-group sg1313', '--share_group sg1313')
@mock.patch.object(shell_v2, '_find_share_group', mock.Mock())
def test_delete_with_share_group(self, sg_cmd):
fake_sg = type('FakeShareGroup', (object,), {'id': sg_cmd.split()[-1]})
shell_v2._find_share_group.return_value = fake_sg
self.run_command('delete 1234 %s' % cg_cmd)
self.run_command('delete 1234 %s' % sg_cmd)
self.assert_called('DELETE', '/shares/1234?consistency_group_id=1234')
self.assertTrue(shell_v2._find_consistency_group.called)
self.assert_called('DELETE', '/shares/1234?share_group_id=sg1313')
self.assertTrue(shell_v2._find_share_group.called)
def test_delete_not_found(self):
self.assertRaises(
@ -1763,152 +1761,448 @@ class ShellTest(test_utils.TestCase):
field_labels=['ID', 'Status', 'Version', 'Minimum Version'])
@mock.patch.object(cliutils, 'print_list', mock.Mock())
def test_cg_list(self):
self.run_command('cg-list')
self.assert_called('GET', '/consistency-groups/detail')
def test_share_group_list(self):
self.run_command('share-group-list')
self.assert_called('GET', '/share-groups/detail')
cliutils.print_list.assert_called_once_with(
mock.ANY, fields=['id', 'name', 'description', 'status'])
mock.ANY, fields=('ID', 'Name', 'Status', 'Description'))
@mock.patch.object(cliutils, 'print_list', mock.Mock())
def test_cg_list_select_column(self):
self.run_command('cg-list --columns id,name,description')
self.assert_called('GET', '/consistency-groups/detail')
def test_share_group_list_select_column(self):
self.run_command('share-group-list --columns id,name,description')
self.assert_called('GET', '/share-groups/detail')
cliutils.print_list.assert_called_once_with(
mock.ANY, fields=['Id', 'Name', 'Description'])
def test_share_group_show(self):
self.run_command('share-group-show 1234')
self.assert_called('GET', '/share-groups/1234')
def test_share_group_create(self):
fake_share_type_1 = type('FakeShareType1', (object,), {'id': '1234'})
fake_share_type_2 = type('FakeShareType2', (object,), {'id': '5678'})
self.mock_object(
shell_v2, '_find_share_type',
mock.Mock(side_effect=[fake_share_type_1, fake_share_type_2]))
fake_share_group_type = type(
'FakeShareGroupType', (object,), {'id': '2345'})
self.mock_object(
shell_v2, '_find_share_group_type',
mock.Mock(return_value=fake_share_group_type))
fake_share_network = type(
'FakeShareNetwork', (object,), {'id': '3456'})
self.mock_object(
shell_v2, '_find_share_network',
mock.Mock(return_value=fake_share_network))
self.run_command(
'share-group-create --name fake_sg '
'--description my_group --share-types 1234,5678 '
'--share-group-type fake_sg_type '
'--share-network fake_share_network '
'--availability-zone fake_az')
expected = {
'share_group': {
'name': 'fake_sg',
'description': 'my_sg',
'availability_zone': 'fake_az',
'share_group_type_id': '2345',
'share_network_id': '3456',
'share_types': ['1234', '5678'],
},
}
self.assert_called('POST', '/share-groups', body=expected)
@ddt.data(
'--source-cgsnapshot-id fake-cg-id',
'--name fake_name --source-cgsnapshot-id fake-cg-id',
'--name fake_name --availability-zone fake_az',
'--description my_fake_description --name fake_name',
'--availability-zone fake_az',
)
def test_cg_create(self, data):
cmd = 'cg-create' + ' ' + data
def test_share_group_create_no_share_types(self, data):
cmd = 'share-group-create' + ' ' + data
self.run_command(cmd)
self.assert_called('POST', '/consistency-groups')
@mock.patch.object(shell_v2, '_find_consistency_group', mock.Mock())
def test_cg_delete(self):
fcg = type('FakeConsistencyGroup', (object,), {'id': '1234'})
shell_v2._find_consistency_group.return_value = fcg
self.assert_called('POST', '/share-groups')
self.run_command('cg-delete fake-cg')
self.assert_called('DELETE', '/consistency-groups/1234')
def test_share_group_create_invalid_args(self):
fake_share_type_1 = type('FakeShareType1', (object,), {'id': '1234'})
fake_share_type_2 = type('FakeShareType2', (object,), {'id': '5678'})
self.mock_object(
shell_v2, '_find_share_type',
mock.Mock(side_effect=[fake_share_type_1, fake_share_type_2]))
fake_share_group_type = type(
'FakeShareGroupType', (object,), {'id': '2345'})
self.mock_object(
shell_v2, '_find_share_group_type',
mock.Mock(return_value=fake_share_group_type))
fake_share_group_snapshot = type(
'FakeShareGroupSnapshot', (object,), {'id': '3456'})
self.mock_object(
shell_v2, '_find_share_group_snapshot',
mock.Mock(return_value=fake_share_group_snapshot))
@mock.patch.object(shell_v2, '_find_consistency_group', mock.Mock())
def test_cg_delete_force(self):
fcg = type('FakeConsistencyGroup', (object,), {'id': '1234'})
shell_v2._find_consistency_group.return_value = fcg
self.assertRaises(
ValueError,
self.run_command,
'share-group-create --name fake_sg '
'--description my_group --share-types 1234,5678 '
'--share-group-type fake_sg_type '
'--source-share-group-snapshot fake_share_group_snapshot '
'--availability-zone fake_az')
self.run_command('cg-delete --force fake-cg')
self.assert_called('POST', '/consistency-groups/1234/action',
{'force_delete': None})
@ddt.data(
('--name new-name', {'name': 'new-name'}),
('--description new-description', {'description': 'new-description'}),
('--name new-name --description new-description',
{'name': 'new-name', 'description': 'new-description'}),
)
@ddt.unpack
def test_share_group_update(self, cmd, expected_body):
self.run_command('share-group-update 1234 %s' % cmd)
@mock.patch.object(shell_v2, '_find_consistency_group', mock.Mock())
def test_cg_reset_state_with_flag(self):
fcg = type('FakeConsistencyGroup', (object,), {'id': '1234'})
shell_v2._find_consistency_group.return_value = fcg
expected = {'share_group': expected_body}
self.assert_called('PUT', '/share-groups/1234', body=expected)
self.run_command('cg-reset-state --state error 1234')
self.assert_called('POST', '/consistency-groups/1234/action',
{'reset_status': {'status': 'error'}})
def test_try_update_share_group_without_data(self):
self.assertRaises(
exceptions.CommandError,
self.run_command, 'share-group-update 1234')
@mock.patch.object(shell_v2, '_find_cg_snapshot', mock.Mock())
def test_cg_snapshot_reset_state(self):
fcg = type('FakeConsistencyGroup', (object,), {'id': '1234'})
shell_v2._find_cg_snapshot.return_value = fcg
@mock.patch.object(shell_v2, '_find_share_group', mock.Mock())
def test_share_group_delete(self):
fake_group = type('FakeShareGroup', (object,), {'id': '1234'})
shell_v2._find_share_group.return_value = fake_group
self.run_command('cg-snapshot-reset-state 1234')
self.assert_called('POST', '/cgsnapshots/1234/action',
{'reset_status': {'status': 'available'}})
self.run_command('share-group-delete fake-sg')
@mock.patch.object(shell_v2, '_find_cg_snapshot', mock.Mock())
def test_cg_snapshot_reset_state_with_flag(self):
fcg = type('FakeConsistencyGroup', (object,), {'id': '1234'})
shell_v2._find_cg_snapshot.return_value = fcg
self.assert_called('DELETE', '/share-groups/1234')
self.run_command('cg-snapshot-reset-state --state creating 1234')
self.assert_called('POST', '/cgsnapshots/1234/action',
{'reset_status': {'status': 'creating'}})
@mock.patch.object(shell_v2, '_find_share_group', mock.Mock())
def test_share_group_delete_force(self):
fake_group = type('FakeShareGroup', (object,), {'id': '1234'})
shell_v2._find_share_group.return_value = fake_group
self.run_command('share-group-delete --force fake-group')
self.assert_called(
'POST', '/share-groups/1234/action', {'force_delete': None})
@mock.patch.object(shell_v2, '_find_share_group', mock.Mock())
def test_share_group_delete_all_fail(self):
shell_v2._find_share_group.side_effect = Exception
self.assertRaises(
exceptions.CommandError,
self.run_command, 'share-group-delete fake-group')
@mock.patch.object(shell_v2, '_find_share_group', mock.Mock())
def test_share_group_reset_state_with_flag(self):
fake_group = type('FakeShareGroup', (object,), {'id': '1234'})
shell_v2._find_share_group.return_value = fake_group
self.run_command('share-group-reset-state --state error 1234')
self.assert_called(
'POST', '/share-groups/1234/action',
{'reset_status': {'status': 'error'}})
@ddt.data(
'fake-sg-id',
'--name fake_name fake-sg-id',
'--description my_fake_description --name fake_name fake-sg-id',
)
@mock.patch.object(shell_v2, '_find_share_group', mock.Mock())
def test_share_group_snapshot_create(self, data):
fake_sg = type('FakeShareGroup', (object,), {'id': '1234'})
shell_v2._find_share_group.return_value = fake_sg
self.run_command('share-group-snapshot-create ' + data)
shell_v2._find_share_group.assert_called_with(mock.ANY, 'fake-sg-id')
self.assert_called('POST', '/share-group-snapshots')
@mock.patch.object(cliutils, 'print_list', mock.Mock())
def test_cg_snapshot_list(self):
self.run_command('cg-snapshot-list')
self.assert_called('GET', '/cgsnapshots/detail')
def test_share_group_snapshot_list(self):
self.run_command('share-group-snapshot-list')
self.assert_called('GET', '/share-group-snapshots/detail')
cliutils.print_list.assert_called_once_with(
mock.ANY, fields=['id', 'name', 'description', 'status'])
mock.ANY, fields=('id', 'name', 'status', 'description'))
@mock.patch.object(cliutils, 'print_list', mock.Mock())
def test_cg_snapshot_list_select_column(self):
self.run_command('cg-snapshot-list --columns id,name')
self.assert_called('GET', '/cgsnapshots/detail')
def test_share_group_snapshot_list_select_column(self):
self.run_command('share-group-snapshot-list --columns id,name')
self.assert_called('GET', '/share-group-snapshots/detail')
cliutils.print_list.assert_called_once_with(
mock.ANY, fields=['Id', 'Name'])
@mock.patch.object(cliutils, 'print_list', mock.Mock())
@mock.patch.object(shell_v2, '_find_cg_snapshot', mock.Mock())
def test_cg_snapshot_members(self):
fcg = type('FakeConsistencyGroup', (object,), {'id': 'fake-cg-id'})
shell_v2._find_cg_snapshot.return_value = fcg
def test_share_group_snapshot_list_all_tenants_only_key(self):
self.run_command('share-group-snapshot-list --all-tenants')
self.run_command('cg-snapshot-members fake-cg-id')
self.assert_called('GET', '/cgsnapshots/fake-cg-id/members')
shell_v2._find_cg_snapshot.assert_called_with(mock.ANY, fcg.id)
self.assert_called(
'GET', '/share-group-snapshots/detail?all_tenants=1')
cliutils.print_list.assert_called_once_with(
mock.ANY, fields=['Id', 'Size', 'Created_at',
'Share_protocol', 'Share_id', 'Share_type_id'])
def test_cg_snapshot_list_all_tenants_only_key(self):
self.run_command('cg-snapshot-list --all-tenants')
self.assert_called('GET', '/cgsnapshots/detail?all_tenants=1')
def test_cg_snapshot_list_all_tenants_key_and_value_1(self):
def test_share_group_snapshot_list_all_tenants_key_and_value_1(self):
for separator in self.separators:
self.run_command(
'cg-snapshot-list --all-tenants' + separator + '1')
self.assert_called('GET', '/cgsnapshots/detail?all_tenants=1')
'share-group-snapshot-list --all-tenants' + separator + '1')
def test_cg_snapshot_list_with_filters(self):
self.run_command('cg-snapshot-list --limit 10 --offset 0')
self.assert_called('GET', '/cgsnapshots/detail?limit=10&offset=0')
self.assert_called(
'GET', '/share-group-snapshots/detail?all_tenants=1')
def test_share_group_snapshot_list_with_filters(self):
self.run_command('share-group-snapshot-list --limit 10 --offset 0')
self.assert_called(
'GET', '/share-group-snapshots/detail?limit=10&offset=0')
def test_share_group_snapshot_show(self):
self.run_command('share-group-snapshot-show 1234')
self.assert_called('GET', '/share-group-snapshots/1234')
def test_share_group_snapshot_list_members(self):
self.run_command('share-group-snapshot-list-members 1234')
self.assert_called('GET', '/share-group-snapshots/1234')
def test_share_group_snapshot_list_members_select_column(self):
self.mock_object(cliutils, 'print_list')
self.run_command(
'share-group-snapshot-list-members 1234 --columns id,size')
self.assert_called('GET', '/share-group-snapshots/1234')
cliutils.print_list.assert_called_once_with(
mock.ANY, fields=['Id', 'Size'])
@mock.patch.object(shell_v2, '_find_share_group_snapshot', mock.Mock())
def test_share_group_snapshot_reset_state(self):
fake_sg_snapshot = type(
'FakeShareGroupSnapshot', (object,), {'id': '1234'})
shell_v2._find_share_group_snapshot.return_value = fake_sg_snapshot
self.run_command('share-group-snapshot-reset-state 1234')
self.assert_called(
'POST', '/share-group-snapshots/1234/action',
{'reset_status': {'status': 'available'}})
@mock.patch.object(shell_v2, '_find_share_group_snapshot', mock.Mock())
def test_share_group_snapshot_reset_state_with_flag(self):
fake_sg_snapshot = type('FakeSGSnapshot', (object,), {'id': '1234'})
shell_v2._find_share_group_snapshot.return_value = fake_sg_snapshot
self.run_command(
'share-group-snapshot-reset-state --state creating 1234')
self.assert_called(
'POST', '/share-group-snapshots/1234/action',
{'reset_status': {'status': 'creating'}})
@ddt.data(
'fake-cg-id',
'--name fake_name fake-cg-id',
"--description my_fake_description --name fake_name fake-cg-id",
('--name new-name', {'name': 'new-name'}),
('--description new-description', {'description': 'new-description'}),
('--name new-name --description new-description',
{'name': 'new-name', 'description': 'new-description'}),
)
@mock.patch.object(shell_v2, '_find_consistency_group', mock.Mock())
def test_cg_snapshot_create(self, data):
fcg = type('FakeConsistencyGroup', (object,), {'id': 'fake-cg-id'})
shell_v2._find_consistency_group.return_value = fcg
@ddt.unpack
def test_share_group_snapshot_update(self, cmd, expected_body):
self.run_command('share-group-snapshot-update 1234 %s' % cmd)
cmd = 'cg-snapshot-create' + ' ' + data
expected = {'share_group_snapshot': expected_body}
self.assert_called('PUT', '/share-group-snapshots/1234', body=expected)
self.run_command(cmd)
def test_try_update_share_group_snapshot_without_data(self):
self.assertRaises(
exceptions.CommandError,
self.run_command, 'share-group-snapshot-update 1234')
shell_v2._find_consistency_group.assert_called_with(mock.ANY, fcg.id)
self.assert_called('POST', '/cgsnapshots')
@mock.patch.object(shell_v2, '_find_share_group_snapshot', mock.Mock())
def test_share_group_snapshot_delete(self):
fake_sg_snapshot = type('FakeSGSnapshot', (object,), {'id': '1234'})
shell_v2._find_share_group_snapshot.return_value = fake_sg_snapshot
@mock.patch.object(shell_v2, '_find_cg_snapshot', mock.Mock())
def test_cg_snapshot_delete(self):
fcg = type('FakeConsistencyGroup', (object,), {'id': '1234'})
shell_v2._find_cg_snapshot.return_value = fcg
self.run_command('share-group-snapshot-delete fake-group-snapshot')
self.run_command('cg-snapshot-delete fake-cg')
self.assert_called('DELETE', '/cgsnapshots/1234')
self.assert_called('DELETE', '/share-group-snapshots/1234')
@mock.patch.object(shell_v2, '_find_cg_snapshot', mock.Mock())
def test_cg_snapshot_delete_force(self):
fcg = type('FakeConsistencyGroup', (object,), {'id': '1234'})
shell_v2._find_cg_snapshot.return_value = fcg
@mock.patch.object(shell_v2, '_find_share_group_snapshot', mock.Mock())
def test_share_group_snapshot_delete_force(self):
fake_sg_snapshot = type('FakeSGSnapshot', (object,), {'id': '1234'})
shell_v2._find_share_group_snapshot.return_value = fake_sg_snapshot
self.run_command('cg-snapshot-delete --force fake-cg')
self.assert_called('POST', '/cgsnapshots/1234/action',
{'force_delete': None})
self.run_command(
'share-group-snapshot-delete --force fake-sg-snapshot')
self.assert_called(
'POST', '/share-group-snapshots/1234/action',
{'force_delete': None})
def test_share_group_snapshot_delete_all_fail(self):
self.mock_object(
shell_v2, '_find_share_group_snapshot',
mock.Mock(side_effect=Exception))
self.assertRaises(
exceptions.CommandError,
self.run_command, 'share-group-snapshot-delete fake-sg-snapshot')
def test_share_group_type_list(self):
self.mock_object(shell_v2, '_print_share_group_type_list')
self.run_command('share-group-type-list')
self.assert_called('GET', '/share-group-types')
shell_v2._print_share_group_type_list.assert_called_once_with(
mock.ANY, default_share_group_type=mock.ANY, columns=mock.ANY)
def test_share_group_type_list_select_column(self):
self.mock_object(shell_v2, '_print_share_group_type_list')
self.run_command('share-group-type-list --columns id,name')
self.assert_called('GET', '/share-group-types')
shell_v2._print_share_group_type_list.assert_called_once_with(
mock.ANY, default_share_group_type=mock.ANY, columns='id,name')
def test_share_group_type_list_default_share_type(self):
self.run_command('share-group-type-list')
self.assert_called_anytime('GET', '/share-group-types/default')
def test_share_group_type_list_all(self):
self.run_command('share-group-type-list --all')
self.assert_called_anytime('GET', '/share-group-types?is_public=all')
@ddt.data(('', mock.ANY), (' --columns id,name', 'id,name'))
@ddt.unpack
def test_share_group_specs_list(self, args_cmd, expected_columns):
self.mock_object(shell_v2, '_print_type_and_extra_specs_list')
self.run_command('share-group-type-specs-list')
self.assert_called('GET', '/share-group-types?is_public=all')
shell_v2._print_type_and_extra_specs_list.assert_called_once_with(
mock.ANY, columns=mock.ANY)
@ddt.data(True, False)
def test_share_group_type_create_with_access(self, public):
fake_share_type_1 = type('FakeShareType', (object,), {'id': '1234'})
fake_share_type_2 = type('FakeShareType', (object,), {'id': '5678'})
self.mock_object(
shell_v2, '_find_share_type',
mock.Mock(side_effect=[fake_share_type_1, fake_share_type_2]))
expected = {
'share_group_type': {
'name': 'test-group-type-1',
'share_types': ['1234', '5678'],
'group_specs': {},
'is_public': public,
}
}
self.run_command(
'share-group-type-create test-group-type-1 type1,type2 '
'--is-public %s' % six.text_type(public))
self.assert_called('POST', '/share-group-types', body=expected)
def test_share_group_type_delete(self):
fake_share_group_type = type(
'FakeShareGroupType', (object,), {'id': '1234'})
self.mock_object(
shell_v2, '_find_share_group_type',
mock.Mock(return_value=fake_share_group_type))
self.run_command('share-group-type-delete test-group-type-1')
self.assert_called('DELETE', '/share-group-types/1234')
def test_share_group_type_key_set(self):
fake_share_group_type = type(
'FakeShareGroupType', (object,),
{'id': '1234', 'is_public': False, 'set_keys': mock.Mock(),
'unset_keys': mock.Mock()})
self.mock_object(
shell_v2, '_find_share_group_type',
mock.Mock(return_value=fake_share_group_type))
self.run_command('share-group-type-key fake_sg_type set key1=value1')
fake_share_group_type.set_keys.assert_called_with({'key1': 'value1'})
def test_share_group_type_key_unset(self):
fake_share_group_type = type(
'FakeShareGroupType', (object,),
{'id': '1234', 'is_public': False, 'set_keys': mock.Mock(),
'unset_keys': mock.Mock()})
self.mock_object(
shell_v2, '_find_share_group_type',
mock.Mock(return_value=fake_share_group_type))
self.run_command('share-group-type-key fake_group_type unset key1')
fake_share_group_type.unset_keys.assert_called_with(['key1'])
def test_share_group_type_access_list(self):
fake_share_group_type = type(
'FakeShareGroupType', (object,),
{'id': '1234', 'is_public': False})
self.mock_object(
shell_v2, '_find_share_group_type',
mock.Mock(return_value=fake_share_group_type))
self.run_command('share-group-type-access-list 1234')
self.assert_called('GET', '/share-group-types/1234/access')
def test_share_group_type_access_list_public(self):
fake_share_group_type = type(
'FakeShareGroupType', (object,),
{'id': '1234', 'is_public': True})
self.mock_object(
shell_v2, '_find_share_group_type',
mock.Mock(return_value=fake_share_group_type))
self.assertRaises(
exceptions.CommandError,
self.run_command, 'share-group-type-access-list 1234')
def test_share_group_type_access_add_project(self):
fake_share_group_type = type(
'FakeShareGroupType', (object,),
{'id': '1234', 'is_public': False})
self.mock_object(
shell_v2, '_find_share_group_type',
mock.Mock(return_value=fake_share_group_type))
expected = {'addProjectAccess': {'project': '101'}}
self.run_command('share-group-type-access-add 1234 101')
self.assert_called(
'POST', '/share-group-types/1234/action', body=expected)
def test_share_group_type_access_remove_project(self):
fake_share_group_type = type(
'FakeShareGroupType', (object,),
{'id': '1234', 'is_public': False})
self.mock_object(
shell_v2, '_find_share_group_type',
mock.Mock(return_value=fake_share_group_type))
expected = {'removeProjectAccess': {'project': '101'}}
self.run_command('share-group-type-access-remove 1234 101')
self.assert_called(
'POST', '/share-group-types/1234/action', body=expected)
@ddt.data(
{'--shares': 5},

View File

@ -23,8 +23,6 @@ from manilaclient.common import constants
from manilaclient.common import httpclient
from manilaclient import exceptions
from manilaclient.v2 import availability_zones
from manilaclient.v2 import consistency_group_snapshots as cg_snapshots
from manilaclient.v2 import consistency_groups
from manilaclient.v2 import limits
from manilaclient.v2 import quota_classes
from manilaclient.v2 import quotas
@ -32,6 +30,10 @@ from manilaclient.v2 import scheduler_stats
from manilaclient.v2 import security_services
from manilaclient.v2 import services
from manilaclient.v2 import share_export_locations
from manilaclient.v2 import share_group_snapshots
from manilaclient.v2 import share_group_type_access
from manilaclient.v2 import share_group_types
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
@ -219,6 +221,12 @@ class Client(object):
self.shares = shares.ShareManager(self)
self.share_export_locations = (
share_export_locations.ShareExportLocationManager(self))
self.share_groups = share_groups.ShareGroupManager(self)
self.share_group_snapshots = (
share_group_snapshots.ShareGroupSnapshotManager(self))
self.share_group_type_access = (
share_group_type_access.ShareGroupTypeAccessManager(self))
self.share_group_types = share_group_types.ShareGroupTypeManager(self)
self.share_instances = share_instances.ShareInstanceManager(self)
self.share_instance_export_locations = (
share_instance_export_locations.ShareInstanceExportLocationManager(
@ -232,10 +240,6 @@ class Client(object):
self.share_servers = share_servers.ShareServerManager(self)
self.share_replicas = share_replicas.ShareReplicaManager(self)
self.pools = scheduler_stats.PoolManager(self)
self.consistency_groups = (
consistency_groups.ConsistencyGroupManager(self))
self.cg_snapshots = (
cg_snapshots.ConsistencyGroupSnapshotManager(self))
self._load_extensions(extensions)

View File

@ -1,202 +0,0 @@
# Copyright 2015 Chuck Fouts
# 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.
"""Interface for consistency group snapshots extension."""
from six.moves.urllib import parse
from manilaclient import api_versions
from manilaclient import base
from manilaclient.common.apiclient import base as common_base
RESOURCES_PATH = '/cgsnapshots'
RESOURCE_PATH = '/cgsnapshots/%s'
RESOURCE_PATH_ACTION = '/cgsnapshots/%s/action'
RESOURCES_NAME = 'cgsnapshots'
RESOURCE_NAME = 'cgsnapshot'
MEMBERS_RESOURCE_NAME = 'cgsnapshot_members'
class ConsistencyGroupSnapshot(common_base.Resource):
"""A group of snapshots taken of multiple shares."""
def __repr__(self):
return "<Consistency Group Snapshot: %s>" % self.id
def update(self, **kwargs):
"""Update this consistency group snapshot."""
self.manager.update(self, **kwargs)
def delete(self):
"""Delete this consistency group snapshot."""
self.manager.delete(self)
def reset_state(self, state):
"""Update the consistency group snapshot with the provided state."""
self.manager.reset_state(self, state)
class ConsistencyGroupSnapshotManager(base.ManagerWithFind):
resource_class = ConsistencyGroupSnapshot
@api_versions.wraps("2.4")
@api_versions.experimental_api
def create(self, consistency_group_id, name=None, description=None):
"""Create a consistency group snapshot.
:param name: text - name of the new cg snapshot
:param description: text - description of the cg snapshot
:rtype: :class:`ConsistencyGroup`
"""
body = {
'consistency_group_id': consistency_group_id,
'name': name,
'description': description,
}
return self._create(RESOURCES_PATH,
{RESOURCE_NAME: body},
RESOURCE_NAME)
@api_versions.wraps("2.4")
@api_versions.experimental_api
def get(self, cg_snapshot):
"""Get a consistency group snapshot.
:param cg_snapshot: either cg snapshot object or text with
its ID.
:rtype: :class:`ConsistencyGroup`
"""
consistency_group_id = common_base.getid(cg_snapshot)
return self._get(RESOURCE_PATH % consistency_group_id,
RESOURCE_NAME)
@api_versions.wraps("2.4")
@api_versions.experimental_api
def update(self, cg_snapshot, **kwargs):
"""Updates a consistency group snapshot.
:param cg_snapshot: either consistency group snapshot object or text
with its ID.
:rtype: :class:`ConsistencyGroup`
"""
if not kwargs:
return
body = {RESOURCE_NAME: kwargs}
cg_snapshot_id = common_base.getid(cg_snapshot)
return self._update(RESOURCE_PATH % cg_snapshot_id,
body,
RESOURCE_NAME)
@api_versions.wraps("2.4")
@api_versions.experimental_api
def list(self, detailed=True, search_opts=None):
"""Get a list of all consistency group snapshots.
:param detailed: Whether to return detailed snapshot info or not.
:param search_opts: dict with search options to filter out snapshots.
available keys are below (('name1', 'name2', ...), 'type'):
- ('all_tenants', int)
- ('offset', int)
- ('limit', int)
Note, that member context will have restricted set of
available search options.
:rtype: list of :class:`ConsistencyGroupSnapshot`
"""
if search_opts is None:
search_opts = {}
query_string = self._query_string_helper(search_opts)
if detailed:
path = RESOURCES_PATH + '/detail%s' % (query_string,)
else:
path = RESOURCES_PATH + '%s' % (query_string,)
return self._list(path, RESOURCES_NAME)
def _do_delete(self, cg_snapshot, force=False, action_name='force_delete'):
"""Delete a consistency group snapshot.
:param cg_snapshot: either a cg snapshot object or text wit its ID.
"""
cg_id = common_base.getid(cg_snapshot)
body = None
if force:
body = {action_name: None}
if body:
self.api.client.post(RESOURCE_PATH_ACTION % cg_id, body=body)
else:
self._delete(RESOURCE_PATH % cg_id)
@api_versions.wraps("2.4", "2.6")
@api_versions.experimental_api
def delete(self, cg_snapshot, force=False):
return self._do_delete(cg_snapshot, force, 'os-force_delete')
@api_versions.wraps("2.7") # noqa
@api_versions.experimental_api
def delete(self, cg_snapshot, force=False):
return self._do_delete(cg_snapshot, force, 'force_delete')
@api_versions.wraps("2.4")
@api_versions.experimental_api
def members(self, cg_snapshot, search_opts=None):
"""Get a list of consistency group snapshot members.
:param search_opts: dict with search options to filter out members.
- ('offset', int)
- ('limit', int)
:rtype: list of :class:`ConsistencyGroupSnapshot`
"""
consistency_group_id = common_base.getid(cg_snapshot)
if search_opts is None:
search_opts = {}
query_string = self._query_string_helper(search_opts)
path = RESOURCES_PATH + '/%s/members%s' % (consistency_group_id,
query_string,)
return self._list(path, MEMBERS_RESOURCE_NAME)
def _do_reset_state(self, cg_snapshot, state, action_name):
"""Update the specified consistency group with the provided state."""
body = {action_name: {'status': state}}
cg_id = common_base.getid(cg_snapshot)
url = RESOURCE_PATH_ACTION % cg_id
return self.api.client.post(url, body=body)
@api_versions.wraps("2.4", "2.6")
@api_versions.experimental_api
def reset_state(self, cg_snapshot, state):
return self._do_reset_state(cg_snapshot, state, 'os-reset_status')
@api_versions.wraps("2.7") # noqa
@api_versions.experimental_api
def reset_state(self, cg_snapshot, state):
return self._do_reset_state(cg_snapshot, state, 'reset_status')
def _query_string_helper(self, search_opts):
q_string = parse.urlencode(
sorted([(k, v) for (k, v) in list(search_opts.items()) if v]))
if q_string:
q_string = "?%s" % (q_string,)
else:
q_string = ''
return q_string

View File

@ -1,199 +0,0 @@
# Copyright 2015 Andrew Kerr
# Copyright 2015 Chuck Fouts
# 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.
"""Interface for consistency groups extension."""
from six.moves.urllib import parse
from manilaclient import api_versions
from manilaclient import base
from manilaclient.common.apiclient import base as common_base
RESOURCES_PATH = '/consistency-groups'
RESOURCE_PATH = '/consistency-groups/%s'
RESOURCE_PATH_ACTION = '/consistency-groups/%s/action'
RESOURCES_NAME = 'consistency_groups'
RESOURCE_NAME = 'consistency_group'
class ConsistencyGroup(common_base.Resource):
"""A consistency group is a logical grouping of shares."""
def __repr__(self):
return "<Consistency Group: %s>" % self.id
def update(self, **kwargs):
"""Update this consistency group."""
self.manager.update(self, **kwargs)
def delete(self):
"""Delete this consistency group."""
self.manager.delete(self)
def reset_state(self, state):
"""Update the consistency group with the provided state."""
self.manager.reset_state(self, state)
class ConsistencyGroupManager(base.ManagerWithFind):
"""Manage :class:`ConsistencyGroup` resources."""
resource_class = ConsistencyGroup
@api_versions.wraps("2.4")
@api_versions.experimental_api
def create(self, share_network=None, name=None, description=None,
source_cgsnapshot_id=None, share_types=None):
"""Create a Consistency Group.
:param share_network: either the share network object or text of the
uuid - represents the share network to use when creating a
consistency group with multi-svm capabilities.
:param name: text - name of the new consistency group
:param description: text - description of the consistency group
:param source_cgsnapshot_id: text - The uuid of the cgsnapshot from
which this CG was created. Cannot be supplied when 'share_types'
is provided.
:param share_types: List of the share types that shares in the CG are
allowed to be a part of. Cannot be supplied when
'source_cgsnapshot_id' is provided.
:rtype: :class:`ConsistencyGroup`
"""
if share_types:
share_types = [common_base.getid(share_type)
for share_type in share_types]
body = {'name': name, 'description': description}
share_network_id = None
if share_network:
share_network_id = common_base.getid(share_network)
if share_network_id:
body['share_network_id'] = share_network_id
if source_cgsnapshot_id:
body['source_cgsnapshot_id'] = source_cgsnapshot_id
if share_types:
body['share_types'] = share_types
return self._create(RESOURCES_PATH,
{RESOURCE_NAME: body}, RESOURCE_NAME)
@api_versions.wraps("2.4")
@api_versions.experimental_api
def get(self, consistency_group):
"""Get a consistency group.
:param consistency_group: either consistency group object or text with
its ID.
:rtype: :class:`ConsistencyGroup`
"""
consistency_group_id = common_base.getid(consistency_group)
return self._get(RESOURCE_PATH % consistency_group_id,
RESOURCE_NAME)
@api_versions.wraps("2.4")
@api_versions.experimental_api
def update(self, consistency_group, **kwargs):
"""Updates a consistency group.
:param consistency_group: either consistency group object or text
with its ID.
:rtype: :class:`ConsistencyGroup`
"""
if not kwargs:
return
body = {RESOURCE_NAME: kwargs}
consistency_group_id = common_base.getid(consistency_group)
return self._update(RESOURCE_PATH % consistency_group_id,
body,
RESOURCE_NAME)
@api_versions.wraps("2.4")
@api_versions.experimental_api
def list(self, detailed=True, search_opts=None,
sort_key=None, sort_dir=None):
"""Get a list of all shares.
:param detailed: Whether to return detailed share info or not.
:param search_opts: dict with search options to filter out shares.
available keys are below (('name1', 'name2', ...), 'type'):
- ('offset', int)
- ('limit', int)
:rtype: list of :class:`ConsistencyGroup`
"""
if search_opts is None:
search_opts = {}
query_string = self._query_string_helper(search_opts)
if detailed:
path = RESOURCES_PATH + '/detail%s' % (query_string,)
else:
path = RESOURCES_PATH + '%s' % (query_string,)
return self._list(path, RESOURCES_NAME)
def _do_delete(self, consistency_group, force=False,
action_name='force_delete'):
"""Delete a consistency group.
:param consistency_group: either consistency group object or text with
its ID.
"""
cg_id = common_base.getid(consistency_group)
url = RESOURCE_PATH % cg_id
body = None
if force:
body = {action_name: None}
if body:
self.api.client.post(url + '/action', body=body)
else:
self._delete(url)
@api_versions.wraps("2.4", "2.6")
@api_versions.experimental_api
def delete(self, consistency_group, force=False):
return self._do_delete(consistency_group, force, 'os-force_delete')
@api_versions.wraps("2.7") # noqa
@api_versions.experimental_api
def delete(self, consistency_group, force=False):
return self._do_delete(consistency_group, force, 'force_delete')
def _do_reset_state(self, consistency_group, state, action_name):
"""Update the specified consistency group with the provided state."""
body = {action_name: {'status': state}}
url = RESOURCE_PATH_ACTION % common_base.getid(consistency_group)
return self.api.client.post(url, body=body)
@api_versions.wraps("2.4", "2.6")
@api_versions.experimental_api
def reset_state(self, cg, state):
return self._do_reset_state(
consistency_group, state, 'os-reset_status')
@api_versions.wraps("2.7") # noqa
@api_versions.experimental_api
def reset_state(self, consistency_group, state):
return self._do_reset_state(consistency_group, state, 'reset_status')
def _query_string_helper(self, search_opts):
q_string = parse.urlencode(
sorted([(k, v) for (k, v) in list(search_opts.items()) if v]))
if q_string:
q_string = "?%s" % (q_string,)
else:
q_string = ''
return q_string

View File

@ -0,0 +1,180 @@
# Copyright 2015 Chuck Fouts
# Copyright 2016 Clinton Knight
# 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
from manilaclient.common import constants
RESOURCES_PATH = '/share-group-snapshots'
RESOURCE_PATH = '/share-group-snapshots/%s'
RESOURCE_PATH_ACTION = '/share-group-snapshots/%s/action'
RESOURCES_NAME = 'share_group_snapshots'
RESOURCE_NAME = 'share_group_snapshot'
class ShareGroupSnapshot(common_base.Resource):
"""A snapshot of a share group."""
def __repr__(self):
return "<Share Group Snapshot: %s>" % self.id
def update(self, **kwargs):
"""Update this share group snapshot."""
self.manager.update(self, **kwargs)
def delete(self):
"""Delete this share group snapshot."""
self.manager.delete(self)
def reset_state(self, state):
"""Update this share group snapshot with the provided state."""
self.manager.reset_state(self, state)
class ShareGroupSnapshotManager(base.ManagerWithFind):
"""Manage :class:`ShareGroupSnapshot` resources."""
resource_class = ShareGroupSnapshot
@api_versions.wraps("2.31")
@api_versions.experimental_api
def create(self, share_group, name=None, description=None):
"""Create a share group snapshot.
:param share_group: either ShareGroup object or text with its UUID
:param name: text - name of the new group snapshot
:param description: text - description of the group snapshot
:rtype: :class:`ShareGroupSnapshot`
"""
share_group_id = common_base.getid(share_group)
body = {'share_group_id': share_group_id}
if name:
body['name'] = name
if description:
body['description'] = description
return self._create(
RESOURCES_PATH, {RESOURCE_NAME: body}, RESOURCE_NAME)
@api_versions.wraps("2.31")
@api_versions.experimental_api
def get(self, share_group_snapshot):
"""Get a share group snapshot.
:param share_group_snapshot: either share group snapshot object or text
with its UUID
:rtype: :class:`ShareGroupSnapshot`
"""
share_group_snapshot_id = common_base.getid(share_group_snapshot)
url = RESOURCE_PATH % share_group_snapshot_id
return self._get(url, RESOURCE_NAME)
@api_versions.wraps("2.31")
@api_versions.experimental_api
def list(self, detailed=True, search_opts=None,
sort_key=None, sort_dir=None):
"""Get a list of all share group snapshots.
:param detailed: Whether to return detailed snapshot info or not.
:param search_opts: dict with search options to filter out snapshots.
available keys are below (('name1', 'name2', ...), 'type'):
- ('all_tenants', int)
- ('offset', int)
- ('limit', int)
- ('name', text)
- ('status', text)
- ('share_group_id', text)
:param sort_key: Key to be sorted (i.e. 'created_at' or 'status').
:param sort_dir: Sort direction, should be 'desc' or 'asc'.
:rtype: list of :class:`ShareGroupSnapshot`
"""
if not search_opts:
search_opts = {}
if sort_key is not None:
if sort_key in constants.SHARE_GROUP_SNAPSHOT_SORT_KEY_VALUES:
search_opts['sort_key'] = sort_key
else:
msg = 'sort_key must be one of the following: %s.'
msg_args = ', '.join(
constants.SHARE_GROUP_SNAPSHOT_SORT_KEY_VALUES)
raise ValueError(msg % msg_args)
if sort_dir is not None:
if sort_dir in constants.SORT_DIR_VALUES:
search_opts['sort_dir'] = sort_dir
else:
raise ValueError('sort_dir must be one of the following: %s.'
% ', '.join(constants.SORT_DIR_VALUES))
query_string = self._build_query_string(search_opts)
if detailed:
url = RESOURCES_PATH + '/detail' + query_string
else:
url = RESOURCES_PATH + query_string
return self._list(url, RESOURCES_NAME)
@api_versions.wraps("2.31")
@api_versions.experimental_api
def update(self, share_group_snapshot, **kwargs):
"""Updates a share group snapshot.
:param share_group_snapshot: either ShareGroupSnapshot object or text
with its UUID
:rtype: :class:`ShareGroupSnapshot`
"""
share_group_snapshot_id = common_base.getid(share_group_snapshot)
url = RESOURCE_PATH % share_group_snapshot_id
if not kwargs:
return self._get(url, RESOURCE_NAME)
else:
body = {RESOURCE_NAME: kwargs}
return self._update(url, body, RESOURCE_NAME)
@api_versions.wraps("2.31")
@api_versions.experimental_api
def delete(self, share_group_snapshot, force=False):
"""Delete a share group snapshot.
:param share_group_snapshot: either ShareGroupSnapshot object or text
with its UUID
:param force: True to force the deletion
"""
share_group_snapshot_id = common_base.getid(share_group_snapshot)
if force:
url = RESOURCE_PATH_ACTION % share_group_snapshot_id
body = {'force_delete': None}
self.api.client.post(url, body=body)
else:
url = RESOURCE_PATH % share_group_snapshot_id
self._delete(url)
@api_versions.wraps("2.31")
@api_versions.experimental_api
def reset_state(self, share_group_snapshot, state):
"""Update the specified share group snapshot.
:param share_group_snapshot: either ShareGroupSnapshot object or text
with its UUID
:param state: The new state for the share group snapshot
"""
share_group_snapshot_id = common_base.getid(share_group_snapshot)
url = RESOURCE_PATH_ACTION % share_group_snapshot_id
body = {'reset_status': {'status': state}}
self.api.client.post(url, body=body)

View File

@ -0,0 +1,64 @@
# Copyright 2016 Clinton Knight
#
# 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.
"""Share group type access interface."""
from manilaclient import api_versions
from manilaclient import base
from manilaclient.common.apiclient import base as common_base
RESOURCES_PATH = '/share-group-types'
RESOURCE_PATH = '/share-group-types/%s/access'
RESOURCE_PATH_ACTION = '/share-group-types/%s/action'
RESOURCE_NAME = 'share_group_type_access'
class ShareGroupTypeAccess(common_base.Resource):
def __repr__(self):
return "<Share Group Type Access: %s>" % self.id
class ShareGroupTypeAccessManager(base.ManagerWithFind):
"""Manage :class:`ShareGroupTypeAccess` resources."""
resource_class = ShareGroupTypeAccess
@api_versions.wraps("2.31")
@api_versions.experimental_api
def list(self, share_group_type):
if share_group_type.is_public:
return None
share_group_type_id = common_base.getid(share_group_type)
url = RESOURCE_PATH % share_group_type_id
return self._list(url, RESOURCE_NAME)
@api_versions.wraps("2.31")
@api_versions.experimental_api
def add_project_access(self, share_group_type, project):
"""Add a project to the given share group type access list."""
info = {'project': project}
self._action('addProjectAccess', share_group_type, info)
@api_versions.wraps("2.31")
@api_versions.experimental_api
def remove_project_access(self, share_group_type, project):
"""Remove a project from the given share group type access list."""
info = {'project': project}
self._action('removeProjectAccess', share_group_type, info)
def _action(self, action, share_group_type, info, **kwargs):
"""Perform a share group type action."""
body = {action: info}
self.run_hooks('modify_body_for_action', body, **kwargs)
share_group_type_id = common_base.getid(share_group_type)
url = RESOURCE_PATH_ACTION % share_group_type_id
return self.api.client.post(url, body=body)

View File

@ -0,0 +1,153 @@
# Copyright 2016 Clinton Knight
#
# 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.
"""Interface for share group types extension."""
from manilaclient import api_versions
from manilaclient import base
from manilaclient.common.apiclient import base as common_base
RESOURCES_PATH = '/share-group-types'
RESOURCE_PATH = '/share-group-types/%s'
RESOURCE_PATH_ACTION = '/share-group-types/%s/action'
RESOURCES_NAME = 'share_group_types'
RESOURCE_NAME = 'share_group_type'
GROUP_SPECS_RESOURCES_PATH = '/share-group-types/%s/group-specs'
GROUP_SPECS_RESOURCE_PATH = '/share-group-types/%s/group-specs/%s'
GROUP_SPECS_RESOURCES_NAME = 'group_specs'
class ShareGroupType(common_base.Resource):
"""A Share Group Type is the type of share group to be created."""
def __init__(self, manager, info, loaded=False):
super(ShareGroupType, self).__init__(manager, info, loaded)
self._group_specs = info.get(GROUP_SPECS_RESOURCES_NAME, {})
def __repr__(self):
return "<Share Group Type: %s>" % self.name
@property
def is_public(self):
"""Provide a user-friendly accessor to share-type-access."""
return self._info.get('is_public', 'N/A')
def get_keys(self, prefer_resource_data=True):
"""Get group specs from a share group type.
:param prefer_resource_data: By default group_specs are retrieved from
resource data, but user can force this method to make an API call
and update the group specs in this object.
:return: dict with group specs
"""
if prefer_resource_data:
return self._group_specs
else:
share_group_type_id = common_base.getid(self)
url = GROUP_SPECS_RESOURCES_PATH % share_group_type_id
_resp, body = self.manager.api.client.get(url)
self._group_specs = body.get(GROUP_SPECS_RESOURCES_NAME, {})
return self._group_specs
def set_keys(self, group_specs):
"""Set group specs on a share group type.
:param extra_specs: A dict of key/value pairs to be set on this object
:return: dict with group specs
"""
share_group_type_id = common_base.getid(self)
url = GROUP_SPECS_RESOURCES_PATH % share_group_type_id
body = {GROUP_SPECS_RESOURCES_NAME: group_specs}
return self.manager._create(
url, body, GROUP_SPECS_RESOURCES_NAME, return_raw=True)
def unset_keys(self, keys):
"""Unset group specs on a share group type.
:param keys: A list of keys on this object to be unset
:return: None if successful, else API response on failure
"""
share_group_type_id = common_base.getid(self)
for k in keys:
url = GROUP_SPECS_RESOURCE_PATH % (share_group_type_id, k)
resp = self.manager._delete(url)
if resp is not None:
return resp
class ShareGroupTypeManager(base.ManagerWithFind):
"""Manage :class:`ShareGroupType` resources."""
resource_class = ShareGroupType
@api_versions.wraps("2.31")
@api_versions.experimental_api
def create(self, name, share_types, is_public=False, group_specs=None):
"""Create a share group type.
:param name: Descriptive name of the share group type
:param share_types: list of either instances of ShareType or text
with share type UUIDs
:param is_public: True to create a public share group type
:param group_specs: dict containing group spec key-value pairs
:rtype: :class:`ShareGroupType`
"""
if not share_types:
raise ValueError('At least one share type must be specified when '
'creating a share group type.')
body = {
'name': name,
'is_public': is_public,
'group_specs': group_specs or {},
'share_types': [common_base.getid(share_type)
for share_type in share_types],
}
return self._create(
RESOURCES_PATH, {RESOURCE_NAME: body}, RESOURCE_NAME)
@api_versions.wraps("2.31")
@api_versions.experimental_api
def get(self, share_group_type="default"):
"""Get a specific share group type.
:param share_group_type: either instance of ShareGroupType, or text
with UUID, or 'default'
:rtype: :class:`ShareGroupType`
"""
share_group_type_id = common_base.getid(share_group_type)
url = RESOURCE_PATH % share_group_type_id
return self._get(url, RESOURCE_NAME)
@api_versions.wraps("2.31")
@api_versions.experimental_api
def list(self, show_all=True):
"""Get a list of all share group types.
:rtype: list of :class:`ShareGroupType`.
"""
query_string = '?is_public=all' if show_all else ''
url = RESOURCES_PATH + query_string
return self._list(url, RESOURCES_NAME)
@api_versions.wraps("2.31")
@api_versions.experimental_api
def delete(self, share_group_type):
"""Delete a specific share group type.
:param share_group_type: either instance of ShareGroupType, or text
with UUID
"""
share_group_type_id = common_base.getid(share_group_type)
url = RESOURCE_PATH % share_group_type_id
self._delete(url)

View File

@ -0,0 +1,219 @@
# Copyright 2015 Andrew Kerr
# Copyright 2015 Chuck Fouts
# Copyright 2016 Clinton Knight
# 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
from manilaclient.common import constants
RESOURCES_PATH = '/share-groups'
RESOURCE_PATH = '/share-groups/%s'
RESOURCE_PATH_ACTION = '/share-groups/%s/action'
RESOURCES_NAME = 'share_groups'
RESOURCE_NAME = 'share_group'
class ShareGroup(common_base.Resource):
"""A share group is a logical grouping of shares on a single backend."""
def __repr__(self):
return "<Share Group: %s>" % self.id
def update(self, **kwargs):
"""Update this share group."""
self.manager.update(self, **kwargs)
def delete(self, force=False):
"""Delete this share group."""
self.manager.delete(self, force=force)
def reset_state(self, state):
"""Update this share group with the provided state."""
self.manager.reset_state(self, state)
class ShareGroupManager(base.ManagerWithFind):
"""Manage :class:`ShareGroup` resources."""
resource_class = ShareGroup
@api_versions.wraps("2.31")
@api_versions.experimental_api
def create(self, share_group_type=None, share_types=None,
share_network=None, name=None, description=None,
source_share_group_snapshot=None, availability_zone=None):
"""Create a Share Group.
:param share_group_type: either instance of ShareGroupType or text
with UUID
:param share_types: list of the share types allowed in the group. May
not be supplied when 'source_group_snapshot_id' is provided. These
may be ShareType objects or UUIDs.
:param share_network: either the share network object or text of the
UUID - represents the share network to use when creating a
share group when driver_handles_share_servers = True.
:param name: text - name of the new share group
:param description: text - description of the share group
:param source_share_group_snapshot: text - either instance of
ShareGroupSnapshot or text with UUID from which this shar_group is
to be created. May not be supplied when 'share_types' is provided.
:param availability_zone: name of the availability zone where the
group is to be created
:rtype: :class:`ShareGroup`
"""
if share_types and source_share_group_snapshot:
raise ValueError('Cannot specify a share group with both'
'share_types and source_share_group_snapshot.')
body = {}
if name:
body['name'] = name
if description:
body['description'] = description
if availability_zone:
body['availability_zone'] = availability_zone
if share_group_type:
body['share_group_type_id'] = common_base.getid(share_group_type)
if share_network:
body['share_network_id'] = common_base.getid(share_network)
if source_share_group_snapshot:
body['source_share_group_snapshot_id'] = common_base.getid(
source_share_group_snapshot)
elif share_types:
body['share_types'] = [common_base.getid(share_type)
for share_type in share_types]
return self._create(
RESOURCES_PATH, {RESOURCE_NAME: body}, RESOURCE_NAME)
@api_versions.wraps("2.31")
@api_versions.experimental_api
def get(self, share_group):
"""Get a share group.
:param share_group: either ShareGroup object or text with its UUID
:rtype: :class:`ShareGroup`
"""
share_group_id = common_base.getid(share_group)
url = RESOURCE_PATH % share_group_id
return self._get(url, RESOURCE_NAME)
@api_versions.wraps("2.31")
@api_versions.experimental_api
def list(self, detailed=True, search_opts=None,
sort_key=None, sort_dir=None):
"""Get a list of all share groups.
:param detailed: Whether to return detailed share group info or not.
:param search_opts: dict with search options to filter out groups.
available keys include (('name1', 'name2', ...), 'type'):
- ('offset', int)
- ('limit', int)
- ('all_tenants', int)
- ('name', text)
- ('status', text)
- ('share_server_id', text)
- ('share_group_type_id', text)
- ('source_share_group_snapshot_id', text)
- ('host', text)
- ('share_network_id', text)
- ('project_id', text)
:param sort_key: Key to be sorted (i.e. 'created_at' or 'status').
:param sort_dir: Sort direction, should be 'desc' or 'asc'.
:rtype: list of :class:`ShareGroup`
"""
if not search_opts:
search_opts = {}
if sort_key is not None:
if sort_key in constants.SHARE_GROUP_SORT_KEY_VALUES:
search_opts['sort_key'] = sort_key
# NOTE(cknight): Replace aliases with appropriate keys
if sort_key == 'share_group_type':
search_opts['sort_key'] = 'share_group_type_id'
elif sort_key == 'share_network':
search_opts['sort_key'] = 'share_network_id'
else:
msg = 'sort_key must be one of the following: %s.'
msg_args = ', '.join(constants.SHARE_GROUP_SORT_KEY_VALUES)
raise ValueError(msg % msg_args)
if sort_dir is not None:
if sort_dir in constants.SORT_DIR_VALUES:
search_opts['sort_dir'] = sort_dir
else:
raise ValueError('sort_dir must be one of the following: %s.'
% ', '.join(constants.SORT_DIR_VALUES))
query_string = self._build_query_string(search_opts)
if detailed:
url = RESOURCES_PATH + '/detail' + query_string
else:
url = RESOURCES_PATH + query_string
return self._list(url, RESOURCES_NAME)
@api_versions.wraps("2.31")
@api_versions.experimental_api
def update(self, share_group, **kwargs):
"""Updates a share group.
:param share_group: either ShareGroup object or text with its UUID
:rtype: :class:`ShareGroup`
"""
share_group_id = common_base.getid(share_group)
url = RESOURCE_PATH % share_group_id
if not kwargs:
return self._get(url, RESOURCE_NAME)
else:
body = {RESOURCE_NAME: kwargs}
return self._update(url, body, RESOURCE_NAME)
@api_versions.wraps("2.31")
@api_versions.experimental_api
def delete(self, share_group, force=False):
"""Delete a share group.
:param share_group: either ShareGroup object or text with its UUID
:param force: True to force the deletion
"""
share_group_id = common_base.getid(share_group)
if force:
url = RESOURCE_PATH_ACTION % share_group_id
body = {'force_delete': None}
self.api.client.post(url, body=body)
else:
url = RESOURCE_PATH % share_group_id
self._delete(url)
@api_versions.wraps("2.31")
@api_versions.experimental_api
def reset_state(self, share_group, state):
"""Update the specified share group with the provided state.
:param share_group: either ShareGroup object or text with its UUID
:param state: The new state for the share group
"""
share_group_id = common_base.getid(share_group)
url = RESOURCE_PATH_ACTION % share_group_id
body = {'reset_status': {'status': state}}
self.api.client.post(url, body=body)

View File

@ -69,9 +69,9 @@ class Share(common_base.Resource):
"""Reset the task state of a given share."""
self.manager.reset_task_state(self, task_state)
def delete(self, consistency_group_id=None):
def delete(self, share_group_id=None):
"""Delete this share."""
self.manager.delete(self, consistency_group_id=consistency_group_id)
self.manager.delete(self, share_group_id=share_group_id)
def force_delete(self):
"""Delete the specified share ignoring its current state."""
@ -121,7 +121,7 @@ class ShareManager(base.ManagerWithFind):
def create(self, share_proto, size, snapshot_id=None, name=None,
description=None, metadata=None, share_network=None,
share_type=None, is_public=False, availability_zone=None,
consistency_group_id=None):
share_group_id=None):
"""Create a share.
:param share_proto: text - share protocol for new share
@ -134,8 +134,8 @@ class ShareManager(base.ManagerWithFind):
:param share_network: either instance of ShareNetwork or text with ID
:param share_type: either instance of ShareType or text with ID
:param is_public: bool, whether to set share as public or not.
:param consistency_group_id: text - ID of the consistency group to
which the share should belong
:param share_group_id: text - ID of the share group to which the share
should belong
:rtype: :class:`Share`
"""
share_metadata = metadata if metadata is not None else dict()
@ -150,8 +150,10 @@ class ShareManager(base.ManagerWithFind):
'share_type': common_base.getid(share_type),
'is_public': is_public,
'availability_zone': availability_zone,
'consistency_group_id': consistency_group_id,
}
if share_group_id:
body['share_group_id'] = share_group_id
return self._create('/shares', {'share': body}, 'share')
@api_versions.wraps("2.29")
@ -386,16 +388,16 @@ class ShareManager(base.ManagerWithFind):
return self._list(path, 'shares')
def delete(self, share, consistency_group_id=None):
def delete(self, share, share_group_id=None):
"""Delete a share.
:param share: either share object or text with its ID.
:param consistency_group_id: text - ID of the consistency group to
which the share belongs to.
:param share_group_id: text - ID of the share group to which the share
belongs
"""
url = "/shares/%s" % common_base.getid(share)
if consistency_group_id:
url += "?consistency_group_id=%s" % consistency_group_id
if share_group_id:
url += "?share_group_id=%s" % share_group_id
self._delete(url)
def _do_force_delete(self, share, action_name):

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,6 @@
---
features:
- Added share group support (replacing CG support).
upgrade:
- By adding share group support, the earlier support for CGs was removed.
Preexisting CGs should be managed as share groups instead.