Cinder Client for Consistency Groups

This patch implements CLI commands for the Consistency Groups
feature. Only snapshots for CGs will be implemented in phase 1.

Change-Id: I447555fd8a92bceecf6f40be59030d65461e4cbb
Implements: blueprint consistency-groups
This commit is contained in:
Xing Yang
2014-07-03 22:37:40 -04:00
parent 4162c9ff5c
commit 9fc64a52b1
9 changed files with 641 additions and 3 deletions

View File

@@ -69,6 +69,32 @@ def _stub_snapshot(**kwargs):
return snapshot
def _stub_consistencygroup(**kwargs):
consistencygroup = {
"created_at": "2012-08-28T16:30:31.000000",
"description": None,
"name": "cg",
"id": "11111111-1111-1111-1111-111111111111",
"availability_zone": "myzone",
"status": "available",
}
consistencygroup.update(kwargs)
return consistencygroup
def _stub_cgsnapshot(**kwargs):
cgsnapshot = {
"created_at": "2012-08-28T16:30:31.000000",
"description": None,
"name": None,
"id": "11111111-1111-1111-1111-111111111111",
"status": "available",
"consistencygroup_id": "00000000-0000-0000-0000-000000000000",
}
cgsnapshot.update(kwargs)
return cgsnapshot
def _self_href(base_uri, tenant_id, backup_id):
return '%s/v2/%s/backups/%s' % (base_uri, tenant_id, backup_id)
@@ -394,6 +420,43 @@ class FakeHTTPClient(base_client.HTTPClient):
def delete_volumes_5678(self, **kw):
return (202, {}, None)
#
# Consistencygroups
#
def get_consistencygroups_detail(self, **kw):
return (200, {}, {"consistencygroups": [
_stub_consistencygroup(id='1234'),
_stub_consistencygroup(id='4567')]})
def get_consistencygroups_1234(self, **kw):
return (200, {}, {'consistencygroup':
_stub_consistencygroup(id='1234')})
def post_consistencygroups(self, **kw):
return (202, {}, {'consistencygroup': {}})
def post_consistencygroups_1234_delete(self, **kw):
return (202, {}, {})
#
# Cgsnapshots
#
def get_cgsnapshots_detail(self, **kw):
return (200, {}, {"cgsnapshots": [
_stub_cgsnapshot(id='1234'),
_stub_cgsnapshot(id='4567')]})
def get_cgsnapshots_1234(self, **kw):
return (200, {}, {'cgsnapshot': _stub_cgsnapshot(id='1234')})
def post_cgsnapshots(self, **kw):
return (202, {}, {'cgsnapshot': {}})
def delete_cgsnapshots_1234(self, **kw):
return (202, {}, {})
#
# Quotas
#

View File

@@ -0,0 +1,56 @@
# Copyright (C) 2012 - 2014 EMC Corporation.
#
# 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 cinderclient.tests import utils
from cinderclient.tests.v2 import fakes
cs = fakes.FakeClient()
class cgsnapshotsTest(utils.TestCase):
def test_delete_cgsnapshot(self):
v = cs.cgsnapshots.list()[0]
v.delete()
cs.assert_called('DELETE', '/cgsnapshots/1234')
cs.cgsnapshots.delete('1234')
cs.assert_called('DELETE', '/cgsnapshots/1234')
cs.cgsnapshots.delete(v)
cs.assert_called('DELETE', '/cgsnapshots/1234')
def test_create_cgsnapshot(self):
cs.cgsnapshots.create('cgsnap')
cs.assert_called('POST', '/cgsnapshots')
def test_create_cgsnapshot_with_cg_id(self):
cs.cgsnapshots.create('1234')
expected = {'cgsnapshot': {'status': 'creating',
'description': None,
'user_id': None,
'name': None,
'consistencygroup_id': '1234',
'project_id': None}}
cs.assert_called('POST', '/cgsnapshots', body=expected)
def test_list_cgsnapshot(self):
cs.cgsnapshots.list()
cs.assert_called('GET', '/cgsnapshots/detail')
def test_get_cgsnapshot(self):
cgsnapshot_id = '1234'
cs.cgsnapshots.get(cgsnapshot_id)
cs.assert_called('GET', '/cgsnapshots/%s' % cgsnapshot_id)

View File

@@ -0,0 +1,52 @@
# Copyright (C) 2012 - 2014 EMC Corporation.
#
# 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 cinderclient.tests import utils
from cinderclient.tests.v2 import fakes
cs = fakes.FakeClient()
class ConsistencygroupsTest(utils.TestCase):
def test_delete_consistencygroup(self):
v = cs.consistencygroups.list()[0]
v.delete(force='True')
cs.assert_called('POST', '/consistencygroups/1234/delete')
cs.consistencygroups.delete('1234', force=True)
cs.assert_called('POST', '/consistencygroups/1234/delete')
cs.consistencygroups.delete(v, force=True)
cs.assert_called('POST', '/consistencygroups/1234/delete')
def test_create_consistencygroup(self):
cs.consistencygroups.create('cg')
cs.assert_called('POST', '/consistencygroups')
def test_create_consistencygroup_with_volume_types(self):
cs.consistencygroups.create('cg', volume_types='type1,type2')
expected = {'consistencygroup': {'status': 'creating',
'description': None,
'availability_zone': None,
'user_id': None,
'name': 'cg',
'volume_types': 'type1,type2',
'project_id': None}}
cs.assert_called('POST', '/consistencygroups', body=expected)
def test_list_consistencygroup(self):
cs.consistencygroups.list()
cs.assert_called('GET', '/consistencygroups/detail')

View File

@@ -67,7 +67,8 @@ class VolumesTest(utils.TestCase):
'volume_type': None,
'project_id': None,
'metadata': {},
'source_replica': None},
'source_replica': None,
'consistencygroup_id': None},
'OS-SCH-HNT:scheduler_hints': 'uuid'}
cs.assert_called('POST', '/volumes', body=expected)

View File

@@ -0,0 +1,124 @@
# Copyright (C) 2012 - 2014 EMC Corporation.
# 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.
"""cgsnapshot interface (v2 extension)."""
import six
try:
from urllib import urlencode
except ImportError:
from urllib.parse import urlencode
from cinderclient import base
class Cgsnapshot(base.Resource):
"""A cgsnapshot is snapshot of a consistency group."""
def __repr__(self):
return "<cgsnapshot: %s>" % self.id
def delete(self):
"""Delete this cgsnapshot."""
self.manager.delete(self)
def update(self, **kwargs):
"""Update the name or description for this cgsnapshot."""
self.manager.update(self, **kwargs)
class CgsnapshotManager(base.ManagerWithFind):
"""Manage :class:`Cgsnapshot` resources."""
resource_class = Cgsnapshot
def create(self, consistencygroup_id, name=None, description=None,
user_id=None,
project_id=None):
"""Creates a cgsnapshot.
:param consistencygroup: Name or uuid of a consistencygroup
:param name: Name of the cgsnapshot
:param description: Description of the cgsnapshot
:param user_id: User id derived from context
:param project_id: Project id derived from context
:rtype: :class:`Cgsnapshot`
"""
body = {'cgsnapshot': {'consistencygroup_id': consistencygroup_id,
'name': name,
'description': description,
'user_id': user_id,
'project_id': project_id,
'status': "creating",
}}
return self._create('/cgsnapshots', body, 'cgsnapshot')
def get(self, cgsnapshot_id):
"""Get a cgsnapshot.
:param cgsnapshot_id: The ID of the cgsnapshot to get.
:rtype: :class:`Cgsnapshot`
"""
return self._get("/cgsnapshots/%s" % cgsnapshot_id, "cgsnapshot")
def list(self, detailed=True, search_opts=None):
"""Lists all cgsnapshots.
:rtype: list of :class:`Cgsnapshot`
"""
if search_opts is None:
search_opts = {}
qparams = {}
for opt, val in six.iteritems(search_opts):
if val:
qparams[opt] = val
query_string = "?%s" % urlencode(qparams) if qparams else ""
detail = ""
if detailed:
detail = "/detail"
return self._list("/cgsnapshots%s%s" % (detail, query_string),
"cgsnapshots")
def delete(self, cgsnapshot):
"""Delete a cgsnapshot.
:param cgsnapshot: The :class:`Cgsnapshot` to delete.
"""
self._delete("/cgsnapshots/%s" % base.getid(cgsnapshot))
def update(self, cgsnapshot, **kwargs):
"""Update the name or description for a cgsnapshot.
:param cgsnapshot: The :class:`Cgsnapshot` to update.
"""
if not kwargs:
return
body = {"cgsnapshot": kwargs}
self._update("/cgsnapshots/%s" % base.getid(cgsnapshot), body)
def _action(self, action, cgsnapshot, info=None, **kwargs):
"""Perform a cgsnapshot "action."
"""
body = {action: info}
self.run_hooks('modify_body_for_action', body, **kwargs)
url = '/cgsnapshots/%s/action' % base.getid(cgsnapshot)
return self.api.client.post(url, body=body)

View File

@@ -15,6 +15,8 @@
from cinderclient import client
from cinderclient.v2 import availability_zones
from cinderclient.v2 import cgsnapshots
from cinderclient.v2 import consistencygroups
from cinderclient.v2 import limits
from cinderclient.v2 import qos_specs
from cinderclient.v2 import quota_classes
@@ -69,6 +71,9 @@ class Client(object):
self.restores = volume_backups_restore.VolumeBackupRestoreManager(self)
self.transfers = volume_transfers.VolumeTransferManager(self)
self.services = services.ServiceManager(self)
self.consistencygroups = consistencygroups.\
ConsistencygroupManager(self)
self.cgsnapshots = cgsnapshots.CgsnapshotManager(self)
self.availability_zones = \
availability_zones.AvailabilityZoneManager(self)

View File

@@ -0,0 +1,131 @@
# Copyright (C) 2012 - 2014 EMC Corporation.
# 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.
"""Consistencygroup interface (v2 extension)."""
import six
try:
from urllib import urlencode
except ImportError:
from urllib.parse import urlencode
from cinderclient import base
class Consistencygroup(base.Resource):
"""A Consistencygroup of volumes."""
def __repr__(self):
return "<Consistencygroup: %s>" % self.id
def delete(self, force='False'):
"""Delete this consistencygroup."""
self.manager.delete(self, force)
def update(self, **kwargs):
"""Update the name or description for this consistencygroup."""
self.manager.update(self, **kwargs)
class ConsistencygroupManager(base.ManagerWithFind):
"""Manage :class:`Consistencygroup` resources."""
resource_class = Consistencygroup
def create(self, name=None, description=None,
volume_types=None, user_id=None,
project_id=None, availability_zone=None):
"""Creates a consistencygroup.
:param name: Name of the ConsistencyGroup
:param description: Description of the ConsistencyGroup
:param volume_types: Types of volume
:param user_id: User id derived from context
:param project_id: Project id derived from context
:param availability_zone: Availability Zone to use
:rtype: :class:`Consistencygroup`
"""
body = {'consistencygroup': {'name': name,
'description': description,
'volume_types': volume_types,
'user_id': user_id,
'project_id': project_id,
'availability_zone': availability_zone,
'status': "creating",
}}
return self._create('/consistencygroups', body, 'consistencygroup')
def get(self, group_id):
"""Get a consistencygroup.
:param group_id: The ID of the consistencygroup to get.
:rtype: :class:`Consistencygroup`
"""
return self._get("/consistencygroups/%s" % group_id,
"consistencygroup")
def list(self, detailed=True, search_opts=None):
"""Lists all consistencygroups.
:rtype: list of :class:`Consistencygroup`
"""
if search_opts is None:
search_opts = {}
qparams = {}
for opt, val in six.iteritems(search_opts):
if val:
qparams[opt] = val
query_string = "?%s" % urlencode(qparams) if qparams else ""
detail = ""
if detailed:
detail = "/detail"
return self._list("/consistencygroups%s%s" % (detail, query_string),
"consistencygroups")
def delete(self, consistencygroup, force=False):
"""Delete a consistencygroup.
:param Consistencygroup: The :class:`Consistencygroup` to delete.
"""
body = {'consistencygroup': {'force': force}}
self.run_hooks('modify_body_for_action', body, 'consistencygroup')
url = '/consistencygroups/%s/delete' % base.getid(consistencygroup)
return self.api.client.post(url, body=body)
def update(self, consistencygroup, **kwargs):
"""Update the name or description for a consistencygroup.
:param Consistencygroup: The :class:`Consistencygroup` to update.
"""
if not kwargs:
return
body = {"consistencygroup": kwargs}
self._update("/consistencygroups/%s" % base.getid(consistencygroup),
body)
def _action(self, action, consistencygroup, info=None, **kwargs):
"""Perform a consistencygroup "action."
"""
body = {action: info}
self.run_hooks('modify_body_for_action', body, **kwargs)
url = '/consistencygroups/%s/action' % base.getid(consistencygroup)
return self.api.client.post(url, body=body)

View File

@@ -70,6 +70,16 @@ def _find_backup(cs, backup):
return utils.find_resource(cs.backups, backup)
def _find_consistencygroup(cs, consistencygroup):
"""Gets a consistencygroup by name or ID."""
return utils.find_resource(cs.consistencygroups, consistencygroup)
def _find_cgsnapshot(cs, cgsnapshot):
"""Gets a cgsnapshot by name or ID."""
return utils.find_resource(cs.cgsnapshots, cgsnapshot)
def _find_transfer(cs, transfer):
"""Gets a transfer by name or ID."""
return utils.find_resource(cs.transfers, transfer)
@@ -240,6 +250,11 @@ class CheckSizeArgForCreate(argparse.Action):
action=CheckSizeArgForCreate,
help='Size of volume, in GBs. (Required unless '
'snapshot-id/source-volid is specified).')
@utils.arg('--consisgroup-id',
metavar='<consistencygroup-id>',
default=None,
help='ID of a consistency group where the new volume belongs to. '
'Default=None.')
@utils.arg('--snapshot-id',
metavar='<snapshot-id>',
default=None,
@@ -332,6 +347,7 @@ def do_create(cs, args):
#NOTE(N.S.): end of taken piece
volume = cs.volumes.create(args.size,
args.consisgroup_id,
args.snapshot_id,
args.source_volid,
args.name,
@@ -1694,3 +1710,191 @@ def do_replication_promote(cs, args):
def do_replication_reenable(cs, args):
"""Sync the secondary volume with primary for a relationship."""
utils.find_volume(cs, args.volume).reenable(args.volume)
@utils.arg('--all-tenants',
dest='all_tenants',
metavar='<0|1>',
nargs='?',
type=int,
const=1,
default=0,
help='Shows details for all tenants. Admin only.')
@utils.service_type('volumev2')
def do_consisgroup_list(cs, args):
"""Lists all consistencygroups."""
consistencygroups = cs.consistencygroups.list()
columns = ['ID', 'Status', 'Name']
utils.print_list(consistencygroups, columns)
@utils.arg('consistencygroup',
metavar='<consistencygroup>',
help='Name or ID of a consistency group.')
@utils.service_type('volumev2')
def do_consisgroup_show(cs, args):
"""Shows details of a consistency group."""
info = dict()
consistencygroup = _find_consistencygroup(cs, args.consistencygroup)
info.update(consistencygroup._info)
info.pop('links', None)
utils.print_dict(info)
@utils.arg('--name',
metavar='<name>',
help='Name of a consistency group.')
@utils.arg('--description',
metavar='<description>',
default=None,
help='Description of a consistency group. Default=None.')
@utils.arg('--volume-types',
metavar='<volume-types>',
default=None,
help='Volume types. If not provided, default_volume_type '
'in cinder.conf must be specified. Default=None.')
@utils.arg('--availability-zone',
metavar='<availability-zone>',
default=None,
help='Availability zone for volume. Default=None.')
@utils.service_type('volumev2')
def do_consisgroup_create(cs, args):
"""Creates a consistency group."""
consistencygroup = cs.consistencygroups.create(
args.name,
args.description,
args.volume_types,
availability_zone=args.availability_zone)
info = dict()
consistencygroup = cs.consistencygroups.get(consistencygroup.id)
info.update(consistencygroup._info)
info.pop('links', None)
utils.print_dict(info)
@utils.arg('consistencygroup',
metavar='<consistencygroup>', nargs='+',
help='Name or ID of one or more consistency groups '
'to be deleted.')
@utils.arg('--force',
action='store_true',
help='Allows or disallows consistency groups '
'to be deleted. If the consistency group is empty, '
'it can be deleted without the force flag. '
'If the consistency group is not empty, the force '
'flag is required for it to be deleted.',
default=False)
@utils.service_type('volumev2')
def do_consisgroup_delete(cs, args):
"""Removes one or more consistency groups."""
failure_count = 0
for consistencygroup in args.consistencygroup:
try:
_find_consistencygroup(cs, consistencygroup).delete(args.force)
except Exception as e:
failure_count += 1
print("Delete for consistency group %s failed: %s" %
(consistencygroup, e))
if failure_count == len(args.consistencygroup):
raise exceptions.CommandError("Unable to delete any of specified "
"consistency groups.")
@utils.arg('--all-tenants',
dest='all_tenants',
metavar='<0|1>',
nargs='?',
type=int,
const=1,
default=0,
help='Shows details for all tenants. Admin only.')
@utils.arg('--status',
metavar='<status>',
default=None,
help='Filters results by a status. Default=None.')
@utils.arg('--consistencygroup-id',
metavar='<consistencygroup_id>',
default=None,
help='Filters results by a consistency group ID. Default=None.')
@utils.service_type('volumev2')
def do_cgsnapshot_list(cs, args):
"""Lists all cgsnapshots."""
cgsnapshots = cs.cgsnapshots.list()
all_tenants = int(os.environ.get("ALL_TENANTS", args.all_tenants))
search_opts = {
'all_tenants': all_tenants,
'status': args.status,
'consistencygroup_id': args.consistencygroup_id,
}
cgsnapshots = cs.cgsnapshots.list(search_opts=search_opts)
columns = ['ID', 'Status', 'Name']
utils.print_list(cgsnapshots, columns)
@utils.arg('cgsnapshot',
metavar='<cgsnapshot>',
help='Name or ID of cgsnapshot.')
@utils.service_type('volumev2')
def do_cgsnapshot_show(cs, args):
"""Shows cgsnapshot details."""
info = dict()
cgsnapshot = _find_cgsnapshot(cs, args.cgsnapshot)
info.update(cgsnapshot._info)
info.pop('links', None)
utils.print_dict(info)
@utils.arg('consistencygroup',
metavar='<consistencygroup>',
help='Name or ID of a consistency group.')
@utils.arg('--name',
metavar='<name>',
default=None,
help='Cgsnapshot name. Default=None.')
@utils.arg('--description',
metavar='<description>',
default=None,
help='Cgsnapshot description. Default=None.')
@utils.service_type('volumev2')
def do_cgsnapshot_create(cs, args):
"""Creates a cgsnapshot."""
consistencygroup = _find_consistencygroup(cs, args.consistencygroup)
cgsnapshot = cs.cgsnapshots.create(
consistencygroup.id,
args.name,
args.description)
info = dict()
cgsnapshot = cs.cgsnapshots.get(cgsnapshot.id)
info.update(cgsnapshot._info)
info.pop('links', None)
utils.print_dict(info)
@utils.arg('cgsnapshot',
metavar='<cgsnapshot>', nargs='+',
help='Name or ID of one or more cgsnapshots to be deleted.')
@utils.service_type('volumev2')
def do_cgsnapshot_delete(cs, args):
"""Removes one or more cgsnapshots."""
failure_count = 0
for cgsnapshot in args.cgsnapshot:
try:
_find_cgsnapshot(cs, cgsnapshot).delete()
except Exception as e:
failure_count += 1
print("Delete for cgsnapshot %s failed: %s" % (cgsnapshot, e))
if failure_count == len(args.cgsnapshot):
raise exceptions.CommandError("Unable to delete any of specified "
"cgsnapshots.")

View File

@@ -165,8 +165,8 @@ class VolumeManager(base.ManagerWithFind):
"""Manage :class:`Volume` resources."""
resource_class = Volume
def create(self, size, snapshot_id=None, source_volid=None,
name=None, description=None,
def create(self, size, consistencygroup_id=None, snapshot_id=None,
source_volid=None, name=None, description=None,
volume_type=None, user_id=None,
project_id=None, availability_zone=None,
metadata=None, imageRef=None, scheduler_hints=None,
@@ -174,6 +174,7 @@ class VolumeManager(base.ManagerWithFind):
"""Creates a volume.
:param size: Size of volume in GB
:param consistencygroup_id: ID of the consistencygroup
:param snapshot_id: ID of the snapshot
:param name: Name of the volume
:param description: Description of the volume
@@ -196,6 +197,7 @@ class VolumeManager(base.ManagerWithFind):
volume_metadata = metadata
body = {'volume': {'size': size,
'consistencygroup_id': consistencygroup_id,
'snapshot_id': snapshot_id,
'name': name,
'description': description,