diff --git a/cinderclient/client.py b/cinderclient/client.py index af8f6faad..622ac7153 100644 --- a/cinderclient/client.py +++ b/cinderclient/client.py @@ -66,11 +66,11 @@ if not hasattr(urlparse, 'parse_qsl'): import cgi urlparse.parse_qsl = cgi.parse_qsl -_VALID_VERSIONS = ['v1', 'v2'] +_VALID_VERSIONS = ['v1', 'v2', 'v3'] # tell keystoneclient that we can ignore the /v1|v2/{project_id} component of # the service catalog when doing discovery lookups -for svc in ('volume', 'volumev2'): +for svc in ('volume', 'volumev2', 'volumev3'): discover.add_catalog_discover_hack(svc, re.compile('/v[12]/\w+/?$'), '/') @@ -580,6 +580,7 @@ def get_client_class(version): version_map = { '1': 'cinderclient.v1.client.Client', '2': 'cinderclient.v2.client.Client', + '3': 'cinderclient.v3.client.Client', } try: client_path = version_map[str(version)] diff --git a/cinderclient/service_catalog.py b/cinderclient/service_catalog.py index ce78b47be..022775053 100644 --- a/cinderclient/service_catalog.py +++ b/cinderclient/service_catalog.py @@ -57,16 +57,18 @@ class ServiceCatalog(object): # enabled and the service_type is set to 'volume', go ahead and # accept that. skip_service_type_check = False - if service_type == 'volumev2' and service['type'] == 'volume': + if (service_type in ('volumev2', 'volumev3') and + service['type'] == 'volume'): version = service['endpoints'][0]['publicURL'].split('/')[3] - if version == 'v2': + if version in ('v2', 'v3'): skip_service_type_check = True if (not skip_service_type_check and service.get("type") != service_type): continue - if (volume_service_name and service_type in ('volume', 'volumev2') + if (volume_service_name and service_type in + ('volume', 'volumev2', 'volumev3') and service.get('name') != volume_service_name): continue diff --git a/cinderclient/shell.py b/cinderclient/shell.py index 9a7ef33b6..67f1fb840 100644 --- a/cinderclient/shell.py +++ b/cinderclient/shell.py @@ -32,8 +32,6 @@ from cinderclient import exceptions as exc from cinderclient import utils import cinderclient.auth_plugin from cinderclient._i18n import _ -from cinderclient.v1 import shell as shell_v1 -from cinderclient.v2 import shell as shell_v2 from keystoneclient import discover from keystoneclient import session @@ -54,6 +52,9 @@ _i18n.enable_lazy() DEFAULT_OS_VOLUME_API_VERSION = "2" DEFAULT_CINDER_ENDPOINT_TYPE = 'publicURL' DEFAULT_CINDER_SERVICE_TYPE = 'volumev2' +V1_SHELL = 'cinderclient.v1.shell' +V2_SHELL = 'cinderclient.v2.shell' +V3_SHELL = 'cinderclient.v3.shell' logging.basicConfig() logger = logging.getLogger(__name__) @@ -392,13 +393,12 @@ class OpenStackCinderShell(object): self.subcommands = {} subparsers = parser.add_subparsers(metavar='') - try: - actions_module = { - '1.1': shell_v1, - '2': shell_v2, - }[version] - except KeyError: - actions_module = shell_v1 + if version == '2': + actions_module = importutils.import_module(V2_SHELL) + elif version == '3': + actions_module = importutils.import_module(V3_SHELL) + else: + actions_module = importutils.import_module(V1_SHELL) self._find_actions(subparsers, actions_module) self._find_actions(subparsers, self) diff --git a/cinderclient/tests/unit/v3/__init__.py b/cinderclient/tests/unit/v3/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/cinderclient/tests/unit/v3/fakes.py b/cinderclient/tests/unit/v3/fakes.py new file mode 100644 index 000000000..679012a2a --- /dev/null +++ b/cinderclient/tests/unit/v3/fakes.py @@ -0,0 +1,36 @@ +# Copyright (c) 2013 OpenStack Foundation +# +# 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.unit import fakes +from cinderclient.v3 import client +from cinderclient.tests.unit.v2 import fakes as fake_v2 + + +class FakeClient(fakes.FakeClient, client.Client): + + def __init__(self, *args, **kwargs): + client.Client.__init__(self, 'username', 'password', + 'project_id', 'auth_url', + extensions=kwargs.get('extensions')) + self.client = FakeHTTPClient(**kwargs) + + def get_volume_api_version_from_endpoint(self): + return self.client.get_volume_api_version_from_endpoint() + + +class FakeHTTPClient(fake_v2.FakeHTTPClient): + + def __init__(self, **kwargs): + super(FakeHTTPClient, self).__init__() + self.management_url = 'http://10.0.2.15:8776/v3/fake' diff --git a/cinderclient/tests/unit/v3/test_shell.py b/cinderclient/tests/unit/v3/test_shell.py new file mode 100644 index 000000000..5925fecd9 --- /dev/null +++ b/cinderclient/tests/unit/v3/test_shell.py @@ -0,0 +1,73 @@ +# Copyright (c) 2013 OpenStack Foundation +# 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 fixtures +import mock +from requests_mock.contrib import fixture as requests_mock_fixture + +from cinderclient import client +from cinderclient import shell +from cinderclient.tests.unit import utils +from cinderclient.tests.unit.v3 import fakes +from cinderclient.tests.unit.fixture_data import keystone_client + + +class ShellTest(utils.TestCase): + + FAKE_ENV = { + 'CINDER_USERNAME': 'username', + 'CINDER_PASSWORD': 'password', + 'CINDER_PROJECT_ID': 'project_id', + 'OS_VOLUME_API_VERSION': '3', + 'CINDER_URL': keystone_client.BASE_URL, + } + + # Patch os.environ to avoid required auth info. + def setUp(self): + """Run before each test.""" + super(ShellTest, self).setUp() + for var in self.FAKE_ENV: + self.useFixture(fixtures.EnvironmentVariable(var, + self.FAKE_ENV[var])) + + self.shell = shell.OpenStackCinderShell() + + # HACK(bcwaldon): replace this when we start using stubs + self.old_get_client_class = client.get_client_class + client.get_client_class = lambda *_: fakes.FakeClient + + self.requests = self.useFixture(requests_mock_fixture.Fixture()) + self.requests.register_uri( + 'GET', keystone_client.BASE_URL, + text=keystone_client.keystone_request_callback) + + self.cs = mock.Mock() + + def run_command(self, cmd): + self.shell.main(cmd.split()) + + def assert_called(self, method, url, body=None, + partial_body=None, **kwargs): + return self.shell.cs.assert_called(method, url, body, + partial_body, **kwargs) + + def test_list(self): + self.run_command('list') + # NOTE(jdg): we default to detail currently + self.assert_called('GET', '/volumes/detail') + + def test_list_availability_zone(self): + self.run_command('availability-zone-list') + self.assert_called('GET', '/os-availability-zone') diff --git a/cinderclient/utils.py b/cinderclient/utils.py index ff339d031..0b28272c4 100644 --- a/cinderclient/utils.py +++ b/cinderclient/utils.py @@ -18,6 +18,7 @@ from __future__ import print_function import os import pkg_resources import sys +import types import uuid import six @@ -268,3 +269,10 @@ def _load_entry_point(ep_name, name=None): return ep.load() except (ImportError, pkg_resources.UnknownExtra, AttributeError): continue + + +def retype_method(old_type, new_type, namespace): + for attr in namespace.values(): + if (isinstance(attr, types.FunctionType) and + getattr(attr, 'service_type', None) == old_type): + setattr(attr, 'service_type', new_type) diff --git a/cinderclient/v2/availability_zones.py b/cinderclient/v2/availability_zones.py index aec2279ab..e70d38299 100644 --- a/cinderclient/v2/availability_zones.py +++ b/cinderclient/v2/availability_zones.py @@ -13,30 +13,7 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. - """Availability Zone interface (v2 extension)""" -from cinderclient import base +from cinderclient.v3.availability_zones import * # flake8: noqa - -class AvailabilityZone(base.Resource): - NAME_ATTR = 'display_name' - - def __repr__(self): - return "" % self.zoneName - - -class AvailabilityZoneManager(base.ManagerWithFind): - """Manage :class:`AvailabilityZone` resources.""" - resource_class = AvailabilityZone - - def list(self, detailed=False): - """Lists all availability zones. - - :rtype: list of :class:`AvailabilityZone` - """ - if detailed is True: - return self._list("/os-availability-zone/detail", - "availabilityZoneInfo") - else: - return self._list("/os-availability-zone", "availabilityZoneInfo") diff --git a/cinderclient/v2/capabilities.py b/cinderclient/v2/capabilities.py index a30c15530..61d7717e2 100644 --- a/cinderclient/v2/capabilities.py +++ b/cinderclient/v2/capabilities.py @@ -15,25 +15,5 @@ """Capabilities interface (v2 extension)""" +from cinderclient.v3.capabilities import * # flake8: noqa -from cinderclient import base - - -class Capabilities(base.Resource): - NAME_ATTR = 'name' - - def __repr__(self): - return "" % self.name - - -class CapabilitiesManager(base.Manager): - """Manage :class:`Capabilities` resources.""" - resource_class = Capabilities - - def get(self, host): - """Show backend volume stats and properties. - - :param host: Specified backend to obtain volume stats and properties. - :rtype: :class:`Capabilities` - """ - return self._get('/capabilities/%s' % host, None) diff --git a/cinderclient/v2/cgsnapshots.py b/cinderclient/v2/cgsnapshots.py index f41434a8b..d9118362a 100644 --- a/cinderclient/v2/cgsnapshots.py +++ b/cinderclient/v2/cgsnapshots.py @@ -15,112 +15,5 @@ """cgsnapshot interface (v2 extension).""" -import six -try: - from urllib import urlencode -except ImportError: - from urllib.parse import urlencode +from cinderclient.v3.cgsnapshots import * # flake8: noqa -from cinderclient import base -from cinderclient.openstack.common.apiclient import base as common_base - - -class Cgsnapshot(base.Resource): - """A cgsnapshot is snapshot of a consistency group.""" - def __repr__(self): - return "" % self.id - - def delete(self): - """Delete this cgsnapshot.""" - return self.manager.delete(self) - - def update(self, **kwargs): - """Update the name or description for this cgsnapshot.""" - return 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. - """ - return 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} - - return 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) - resp, body = self.api.client.post(url, body=body) - return common_base.TupleWithMeta((resp, body), resp) diff --git a/cinderclient/v2/consistencygroups.py b/cinderclient/v2/consistencygroups.py index 80d903bc9..fcee881ae 100644 --- a/cinderclient/v2/consistencygroups.py +++ b/cinderclient/v2/consistencygroups.py @@ -15,148 +15,5 @@ """Consistencygroup interface (v2 extension).""" -import six -try: - from urllib import urlencode -except ImportError: - from urllib.parse import urlencode +from cinderclient.v3.consistencygroups import * # flake8: noqa -from cinderclient import base -from cinderclient.openstack.common.apiclient import base as common_base - - -class Consistencygroup(base.Resource): - """A Consistencygroup of volumes.""" - def __repr__(self): - return "" % self.id - - def delete(self, force='False'): - """Delete this consistencygroup.""" - return self.manager.delete(self, force) - - def update(self, **kwargs): - """Update the name or description for this consistencygroup.""" - return self.manager.update(self, **kwargs) - - -class ConsistencygroupManager(base.ManagerWithFind): - """Manage :class:`Consistencygroup` resources.""" - resource_class = Consistencygroup - - def create(self, volume_types, name=None, - description=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 create_from_src(self, cgsnapshot_id, source_cgid, name=None, - description=None, user_id=None, - project_id=None): - """Creates a consistencygroup from a cgsnapshot or a source CG. - - :param cgsnapshot_id: UUID of a CGSnapshot - :param source_cgid: UUID of a source CG - :param name: Name of the ConsistencyGroup - :param description: Description of the ConsistencyGroup - :param user_id: User id derived from context - :param project_id: Project id derived from context - :rtype: A dictionary containing Consistencygroup metadata - """ - body = {'consistencygroup-from-src': {'name': name, - 'description': description, - 'cgsnapshot_id': cgsnapshot_id, - 'source_cgid': source_cgid, - 'user_id': user_id, - 'project_id': project_id, - 'status': "creating", - }} - - self.run_hooks('modify_body_for_update', body, - 'consistencygroup-from-src') - resp, body = self.api.client.post( - "/consistencygroups/create_from_src", body=body) - return common_base.DictWithMeta(body['consistencygroup'], resp) - - 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) - resp, body = self.api.client.post(url, body=body) - return common_base.TupleWithMeta((resp, body), resp) - - 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} - - return 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) - resp, body = self.api.client.post(url, body=body) - return common_base.TupleWithMeta((resp, body), resp) diff --git a/cinderclient/v2/limits.py b/cinderclient/v2/limits.py index 512a58dec..2a20684ca 100644 --- a/cinderclient/v2/limits.py +++ b/cinderclient/v2/limits.py @@ -12,80 +12,7 @@ # implied. # See the License for the specific language governing permissions and # limitations under the License. +"""Limits interface (v2 extension)""" -from cinderclient import base +from cinderclient.v3.limits import * # flake8: noqa - -class Limits(base.Resource): - """A collection of RateLimit and AbsoluteLimit objects.""" - - def __repr__(self): - return "" - - @property - def absolute(self): - for (name, value) in list(self._info['absolute'].items()): - yield AbsoluteLimit(name, value) - - @property - def rate(self): - for group in self._info['rate']: - uri = group['uri'] - regex = group['regex'] - for rate in group['limit']: - yield RateLimit(rate['verb'], uri, regex, rate['value'], - rate['remaining'], rate['unit'], - rate['next-available']) - - -class RateLimit(object): - """Data model that represents a flattened view of a single rate limit.""" - - def __init__(self, verb, uri, regex, value, remain, - unit, next_available): - self.verb = verb - self.uri = uri - self.regex = regex - self.value = value - self.remain = remain - self.unit = unit - self.next_available = next_available - - def __eq__(self, other): - return self.uri == other.uri \ - and self.regex == other.regex \ - and self.value == other.value \ - and self.verb == other.verb \ - and self.remain == other.remain \ - and self.unit == other.unit \ - and self.next_available == other.next_available - - def __repr__(self): - return "" % (self.verb, self.uri) - - -class AbsoluteLimit(object): - """Data model that represents a single absolute limit.""" - - def __init__(self, name, value): - self.name = name - self.value = value - - def __eq__(self, other): - return self.value == other.value and self.name == other.name - - def __repr__(self): - return "" % (self.name) - - -class LimitsManager(base.Manager): - """Manager object used to interact with limits resource.""" - - resource_class = Limits - - def get(self): - """Get a specific extension. - - :rtype: :class:`Limits` - """ - return self._get("/limits", "limits") diff --git a/cinderclient/v2/pools.py b/cinderclient/v2/pools.py index 608e057d0..5f3a72c47 100644 --- a/cinderclient/v2/pools.py +++ b/cinderclient/v2/pools.py @@ -15,48 +15,5 @@ """Pools interface (v2 extension)""" -import six +from cinderclient.v3.pools import * # flake8: noqa -from cinderclient import base - - -class Pool(base.Resource): - NAME_ATTR = 'name' - - def __repr__(self): - return "" % self.name - - -class PoolManager(base.Manager): - """Manage :class:`Pool` resources.""" - resource_class = Pool - - def list(self, detailed=False): - """Lists all - - :rtype: list of :class:`Pool` - """ - if detailed is True: - pools = self._list("/scheduler-stats/get_pools?detail=True", - "pools") - # Other than the name, all of the pool data is buried below in - # a 'capabilities' dictionary. In order to be consistent with the - # get-pools command line, these elements are moved up a level to - # be attributes of the pool itself. - for pool in pools: - if hasattr(pool, 'capabilities'): - for k, v in six.iteritems(pool.capabilities): - setattr(pool, k, v) - - # Remove the capabilities dictionary since all of its - # elements have been copied up to the containing pool - del pool.capabilities - return pools - else: - pools = self._list("/scheduler-stats/get_pools", "pools") - - # avoid cluttering the basic pool list with capabilities dict - for pool in pools: - if hasattr(pool, 'capabilities'): - del pool.capabilities - return pools diff --git a/cinderclient/v2/qos_specs.py b/cinderclient/v2/qos_specs.py index 84b8e0ac5..8b419b478 100644 --- a/cinderclient/v2/qos_specs.py +++ b/cinderclient/v2/qos_specs.py @@ -14,143 +14,9 @@ # See the License for the specific language governing permissions and # limitations under the License. - """ QoS Specs interface. """ -from cinderclient import base -from cinderclient.openstack.common.apiclient import base as common_base +from cinderclient.v3.qos_specs import * # flake8: noqa - -class QoSSpecs(base.Resource): - """QoS specs entity represents quality-of-service parameters/requirements. - - A QoS specs is a set of parameters or requirements for quality-of-service - purpose, which can be associated with volume types (for now). In future, - QoS specs may be extended to be associated other entities, such as single - volume. - """ - def __repr__(self): - return "" % self.name - - def delete(self): - return self.manager.delete(self) - - -class QoSSpecsManager(base.ManagerWithFind): - """ - Manage :class:`QoSSpecs` resources. - """ - resource_class = QoSSpecs - - def list(self, search_opts=None): - """Get a list of all qos specs. - - :rtype: list of :class:`QoSSpecs`. - """ - return self._list("/qos-specs", "qos_specs") - - def get(self, qos_specs): - """Get a specific qos specs. - - :param qos_specs: The ID of the :class:`QoSSpecs` to get. - :rtype: :class:`QoSSpecs` - """ - return self._get("/qos-specs/%s" % base.getid(qos_specs), "qos_specs") - - def delete(self, qos_specs, force=False): - """Delete a specific qos specs. - - :param qos_specs: The ID of the :class:`QoSSpecs` to be removed. - :param force: Flag that indicates whether to delete target qos specs - if it was in-use. - """ - return self._delete("/qos-specs/%s?force=%s" % - (base.getid(qos_specs), force)) - - def create(self, name, specs): - """Create a qos specs. - - :param name: Descriptive name of the qos specs, must be unique - :param specs: A dict of key/value pairs to be set - :rtype: :class:`QoSSpecs` - """ - - body = { - "qos_specs": { - "name": name, - } - } - - body["qos_specs"].update(specs) - return self._create("/qos-specs", body, "qos_specs") - - def set_keys(self, qos_specs, specs): - """Add/Update keys in qos specs. - - :param qos_specs: The ID of qos specs - :param specs: A dict of key/value pairs to be set - :rtype: :class:`QoSSpecs` - """ - - body = { - "qos_specs": {} - } - - body["qos_specs"].update(specs) - return self._update("/qos-specs/%s" % qos_specs, body) - - def unset_keys(self, qos_specs, specs): - """Remove keys from a qos specs. - - :param qos_specs: The ID of qos specs - :param specs: A list of key to be unset - :rtype: :class:`QoSSpecs` - """ - - body = {'keys': specs} - - return self._update("/qos-specs/%s/delete_keys" % qos_specs, - body) - - def get_associations(self, qos_specs): - """Get associated entities of a qos specs. - - :param qos_specs: The id of the :class: `QoSSpecs` - :return: a list of entities that associated with specific qos specs. - """ - return self._list("/qos-specs/%s/associations" % base.getid(qos_specs), - "qos_associations") - - def associate(self, qos_specs, vol_type_id): - """Associate a volume type with specific qos specs. - - :param qos_specs: The qos specs to be associated with - :param vol_type_id: The volume type id to be associated with - """ - resp, body = self.api.client.get( - "/qos-specs/%s/associate?vol_type_id=%s" % - (base.getid(qos_specs), vol_type_id)) - return common_base.TupleWithMeta((resp, body), resp) - - def disassociate(self, qos_specs, vol_type_id): - """Disassociate qos specs from volume type. - - :param qos_specs: The qos specs to be associated with - :param vol_type_id: The volume type id to be associated with - """ - resp, body = self.api.client.get( - "/qos-specs/%s/disassociate?vol_type_id=%s" % - (base.getid(qos_specs), vol_type_id)) - return common_base.TupleWithMeta((resp, body), resp) - - def disassociate_all(self, qos_specs): - """Disassociate all entities from specific qos specs. - - :param qos_specs: The qos specs to be associated with - """ - resp, body = self.api.client.get( - "/qos-specs/%s/disassociate_all" % - base.getid(qos_specs)) - return common_base.TupleWithMeta((resp, body), resp) diff --git a/cinderclient/v2/quota_classes.py b/cinderclient/v2/quota_classes.py index 0e5fb5b83..28065b80e 100644 --- a/cinderclient/v2/quota_classes.py +++ b/cinderclient/v2/quota_classes.py @@ -13,34 +13,5 @@ # License for the specific language governing permissions and limitations # under the License. -from cinderclient import base +from cinderclient.v3.quota_classes import * # flake8: noqa - -class QuotaClassSet(base.Resource): - - @property - def id(self): - """Needed by base.Resource to self-refresh and be indexed.""" - return self.class_name - - def update(self, *args, **kwargs): - return self.manager.update(self.class_name, *args, **kwargs) - - -class QuotaClassSetManager(base.Manager): - resource_class = QuotaClassSet - - def get(self, class_name): - return self._get("/os-quota-class-sets/%s" % (class_name), - "quota_class_set") - - def update(self, class_name, **updates): - body = {'quota_class_set': {'class_name': class_name}} - - for update in updates: - body['quota_class_set'][update] = updates[update] - - result = self._update('/os-quota-class-sets/%s' % (class_name), body) - return self.resource_class(self, - result['quota_class_set'], loaded=True, - resp=result.request_ids) diff --git a/cinderclient/v2/quotas.py b/cinderclient/v2/quotas.py index bebf32a39..83fb27134 100644 --- a/cinderclient/v2/quotas.py +++ b/cinderclient/v2/quotas.py @@ -13,44 +13,5 @@ # License for the specific language governing permissions and limitations # under the License. -from cinderclient import base +from cinderclient.v3.quotas import * # flake8: noqa - -class QuotaSet(base.Resource): - - @property - def id(self): - """Needed by base.Resource to self-refresh and be indexed.""" - return self.tenant_id - - def update(self, *args, **kwargs): - return self.manager.update(self.tenant_id, *args, **kwargs) - - -class QuotaSetManager(base.Manager): - resource_class = QuotaSet - - def get(self, tenant_id, usage=False): - if hasattr(tenant_id, 'tenant_id'): - tenant_id = tenant_id.tenant_id - return self._get("/os-quota-sets/%s?usage=%s" % (tenant_id, usage), - "quota_set") - - def update(self, tenant_id, **updates): - body = {'quota_set': {'tenant_id': tenant_id}} - - for update in updates: - body['quota_set'][update] = updates[update] - - result = self._update('/os-quota-sets/%s' % (tenant_id), body) - return self.resource_class(self, result['quota_set'], loaded=True, - resp=result.request_ids) - - def defaults(self, tenant_id): - return self._get('/os-quota-sets/%s/defaults' % tenant_id, - 'quota_set') - - def delete(self, tenant_id): - if hasattr(tenant_id, 'tenant_id'): - tenant_id = tenant_id.tenant_id - return self._delete("/os-quota-sets/%s" % tenant_id) diff --git a/cinderclient/v2/services.py b/cinderclient/v2/services.py index 8cefc342b..b49faca54 100644 --- a/cinderclient/v2/services.py +++ b/cinderclient/v2/services.py @@ -16,64 +16,6 @@ """ service interface """ -from cinderclient import base +from cinderclient.v3.services import * # flake8: noqa -class Service(base.Resource): - - def __repr__(self): - return "" % self.service - - -class ServiceManager(base.ManagerWithFind): - resource_class = Service - - def list(self, host=None, binary=None): - """ - Describes service list for host. - - :param host: destination host name. - :param binary: service binary. - """ - url = "/os-services" - filters = [] - if host: - filters.append("host=%s" % host) - if binary: - filters.append("binary=%s" % binary) - if filters: - url = "%s?%s" % (url, "&".join(filters)) - return self._list(url, "services") - - def enable(self, host, binary): - """Enable the service specified by hostname and binary.""" - body = {"host": host, "binary": binary} - result = self._update("/os-services/enable", body) - return self.resource_class(self, result, resp=result.request_ids) - - def disable(self, host, binary): - """Disable the service specified by hostname and binary.""" - body = {"host": host, "binary": binary} - result = self._update("/os-services/disable", body) - return self.resource_class(self, result, resp=result.request_ids) - - def disable_log_reason(self, host, binary, reason): - """Disable the service with reason.""" - body = {"host": host, "binary": binary, "disabled_reason": reason} - result = self._update("/os-services/disable-log-reason", body) - return self.resource_class(self, result, resp=result.request_ids) - - def freeze_host(self, host): - """Freeze the service specified by hostname.""" - body = {"host": host} - return self._update("/os-services/freeze", body) - - def thaw_host(self, host): - """Thaw the service specified by hostname.""" - body = {"host": host} - return self._update("/os-services/thaw", body) - - def failover_host(self, host, backend_id): - """Failover a replicated backend by hostname.""" - body = {"host": host, "backend_id": backend_id} - return self._update("/os-services/failover_host", body) diff --git a/cinderclient/v2/shell.py b/cinderclient/v2/shell.py index 19398c0b4..1e0ddb352 100644 --- a/cinderclient/v2/shell.py +++ b/cinderclient/v2/shell.py @@ -14,1698 +14,13 @@ # See the License for the specific language governing permissions and # limitations under the License. -from __future__ import print_function - -import argparse -import copy -import os -import sys -import time - -import six - -from cinderclient import base -from cinderclient import exceptions +from cinderclient.v3.shell import * # flake8: noqa from cinderclient import utils -from cinderclient.v2 import availability_zones -from oslo_utils import strutils +utils.retype_method('volumev3', 'volumev2', globals()) -def _poll_for_status(poll_fn, obj_id, action, final_ok_states, - poll_period=5, show_progress=True): - """Blocks while an action occurs. Periodically shows progress.""" - def print_progress(progress): - if show_progress: - msg = ('\rInstance %(action)s... %(progress)s%% complete' - % dict(action=action, progress=progress)) - else: - msg = '\rInstance %(action)s...' % dict(action=action) - - sys.stdout.write(msg) - sys.stdout.flush() - - print() - while True: - obj = poll_fn(obj_id) - status = obj.status.lower() - progress = getattr(obj, 'progress', None) or 0 - if status in final_ok_states: - print_progress(100) - print("\nFinished") - break - elif status == "error": - print("\nError %(action)s instance" % {'action': action}) - break - else: - print_progress(progress) - time.sleep(poll_period) - - -def _find_volume_snapshot(cs, snapshot): - """Gets a volume snapshot by name or ID.""" - return utils.find_resource(cs.volume_snapshots, snapshot) - - -def _find_vtype(cs, vtype): - """Gets a volume type by name or ID.""" - return utils.find_resource(cs.volume_types, vtype) - - -def _find_backup(cs, backup): - """Gets a backup by name or ID.""" - 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) - - -def _find_qos_specs(cs, qos_specs): - """Gets a qos specs by ID.""" - return utils.find_resource(cs.qos_specs, qos_specs) - - -def _print_volume_snapshot(snapshot): - utils.print_dict(snapshot._info) - - -def _print_volume_image(image): - utils.print_dict(image[1]['os-volume_upload_image']) - - -def _translate_keys(collection, convert): - for item in collection: - keys = item.__dict__ - for from_key, to_key in convert: - if from_key in keys and to_key not in keys: - setattr(item, to_key, item._info[from_key]) - - -def _translate_volume_keys(collection): - convert = [('volumeType', 'volume_type'), - ('os-vol-tenant-attr:tenant_id', 'tenant_id')] - _translate_keys(collection, convert) - - -def _translate_volume_snapshot_keys(collection): - convert = [('volumeId', 'volume_id')] - _translate_keys(collection, convert) - - -def _translate_availability_zone_keys(collection): - convert = [('zoneName', 'name'), ('zoneState', 'status')] - _translate_keys(collection, convert) - - -def _extract_metadata(args): - metadata = {} - for metadatum in args.metadata: - # unset doesn't require a val, so we have the if/else - if '=' in metadatum: - (key, value) = metadatum.split('=', 1) - else: - key = metadatum - value = None - - metadata[key] = value - return metadata - - -@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('--all_tenants', - nargs='?', - type=int, - const=1, - help=argparse.SUPPRESS) -@utils.arg('--name', - metavar='', - default=None, - help='Filters results by a name. Default=None.') -@utils.arg('--display-name', - help=argparse.SUPPRESS) -@utils.arg('--status', - metavar='', - default=None, - help='Filters results by a status. Default=None.') -@utils.arg('--bootable', - metavar='', - const=True, - nargs='?', - choices=['True', 'true', 'False', 'false'], - help='Filters results by bootable status. Default=None.') -@utils.arg('--migration_status', - metavar='', - default=None, - help='Filters results by a migration status. Default=None. ' - 'Admin only.') -@utils.arg('--metadata', - type=str, - nargs='*', - metavar='', - default=None, - help='Filters results by a metadata key and value pair. ' - 'Default=None.') -@utils.arg('--marker', - metavar='', - default=None, - help='Begin returning volumes that appear later in the volume ' - 'list than that represented by this volume id. ' - 'Default=None.') -@utils.arg('--limit', - metavar='', - default=None, - help='Maximum number of volumes to return. Default=None.') -@utils.arg('--fields', - default=None, - metavar='', - help='Comma-separated list of fields to display. ' - 'Use the show command to see which fields are available. ' - 'Unavailable/non-existent fields will be ignored. ' - 'Default=None.') -@utils.arg('--sort_key', - metavar='', - default=None, - help=argparse.SUPPRESS) -@utils.arg('--sort_dir', - metavar='', - default=None, - help=argparse.SUPPRESS) -@utils.arg('--sort', - metavar='[:]', - default=None, - help=(('Comma-separated list of sort keys and directions in the ' - 'form of [:]. ' - 'Valid keys: %s. ' - 'Default=None.') % ', '.join(base.SORT_KEY_VALUES))) -@utils.arg('--tenant', - type=str, - dest='tenant', - nargs='?', - metavar='', - help='Display information from single tenant (Admin only).') -@utils.service_type('volumev2') -def do_list(cs, args): - """Lists all volumes.""" - # NOTE(thingee): Backwards-compatibility with v1 args - if args.display_name is not None: - args.name = args.display_name - - all_tenants = 1 if args.tenant else \ - int(os.environ.get("ALL_TENANTS", args.all_tenants)) - search_opts = { - 'all_tenants': all_tenants, - 'project_id': args.tenant, - 'name': args.name, - 'status': args.status, - 'bootable': args.bootable, - 'migration_status': args.migration_status, - 'metadata': _extract_metadata(args) if args.metadata else None, - } - - # If unavailable/non-existent fields are specified, these fields will - # be removed from key_list at the print_list() during key validation. - field_titles = [] - if args.fields: - for field_title in args.fields.split(','): - field_titles.append(field_title) - - # --sort_key and --sort_dir deprecated in kilo and is not supported - # with --sort - if args.sort and (args.sort_key or args.sort_dir): - raise exceptions.CommandError( - 'The --sort_key and --sort_dir arguments are deprecated and are ' - 'not supported with --sort.') - - volumes = cs.volumes.list(search_opts=search_opts, marker=args.marker, - limit=args.limit, sort_key=args.sort_key, - sort_dir=args.sort_dir, sort=args.sort) - _translate_volume_keys(volumes) - - # Create a list of servers to which the volume is attached - for vol in volumes: - servers = [s.get('server_id') for s in vol.attachments] - setattr(vol, 'attached_to', ','.join(map(str, servers))) - - if field_titles: - key_list = ['ID'] + field_titles - else: - key_list = ['ID', 'Status', 'Name', 'Size', 'Volume Type', - 'Bootable', 'Attached to'] - # If all_tenants is specified, print - # Tenant ID as well. - if search_opts['all_tenants']: - key_list.insert(1, 'Tenant ID') - - if args.sort_key or args.sort_dir or args.sort: - sortby_index = None - else: - sortby_index = 0 - utils.print_list(volumes, key_list, exclude_unavailable=True, - sortby_index=sortby_index) - - -@utils.arg('volume', - metavar='', - help='Name or ID of volume.') -@utils.service_type('volumev2') -def do_show(cs, args): - """Shows volume details.""" - info = dict() - volume = utils.find_volume(cs, args.volume) - info.update(volume._info) - - info.pop('links', None) - utils.print_dict(info, - formatters=['metadata', 'volume_image_metadata']) - - -class CheckSizeArgForCreate(argparse.Action): - def __call__(self, parser, args, values, option_string=None): - if ((args.snapshot_id or args.source_volid or args.source_replica) - is None and values is None): - parser.error('Size is a required parameter if snapshot ' - 'or source volume is not specified.') - setattr(args, self.dest, values) - - -@utils.arg('size', - metavar='', - nargs='?', - type=int, - action=CheckSizeArgForCreate, - help='Size of volume, in GiBs. (Required unless ' - 'snapshot-id/source-volid is specified).') -@utils.arg('--consisgroup-id', - metavar='', - default=None, - help='ID of a consistency group where the new volume belongs to. ' - 'Default=None.') -@utils.arg('--snapshot-id', - metavar='', - default=None, - help='Creates volume from snapshot ID. Default=None.') -@utils.arg('--snapshot_id', - help=argparse.SUPPRESS) -@utils.arg('--source-volid', - metavar='', - default=None, - help='Creates volume from volume ID. Default=None.') -@utils.arg('--source_volid', - help=argparse.SUPPRESS) -@utils.arg('--source-replica', - metavar='', - default=None, - help='Creates volume from replicated volume ID. Default=None.') -@utils.arg('--image-id', - metavar='', - default=None, - help='Creates volume from image ID. Default=None.') -@utils.arg('--image_id', - help=argparse.SUPPRESS) -@utils.arg('--image', - metavar='', - default=None, - help='Creates a volume from image (ID or name). Default=None.') -@utils.arg('--image_ref', - help=argparse.SUPPRESS) -@utils.arg('--name', - metavar='', - default=None, - help='Volume name. Default=None.') -@utils.arg('--display-name', - help=argparse.SUPPRESS) -@utils.arg('--display_name', - help=argparse.SUPPRESS) -@utils.arg('--description', - metavar='', - default=None, - help='Volume description. Default=None.') -@utils.arg('--display-description', - help=argparse.SUPPRESS) -@utils.arg('--display_description', - help=argparse.SUPPRESS) -@utils.arg('--volume-type', - metavar='', - default=None, - help='Volume type. Default=None.') -@utils.arg('--volume_type', - help=argparse.SUPPRESS) -@utils.arg('--availability-zone', - metavar='', - default=None, - help='Availability zone for volume. Default=None.') -@utils.arg('--availability_zone', - help=argparse.SUPPRESS) -@utils.arg('--metadata', - type=str, - nargs='*', - metavar='', - default=None, - help='Metadata key and value pairs. Default=None.') -@utils.arg('--hint', - metavar='', - dest='scheduler_hints', - action='append', - default=[], - help='Scheduler hint, like in nova.') -@utils.arg('--allow-multiattach', - dest='multiattach', - action="store_true", - help=('Allow volume to be attached more than once.' - ' Default=False'), - default=False) -@utils.service_type('volumev2') -def do_create(cs, args): - """Creates a volume.""" - # NOTE(thingee): Backwards-compatibility with v1 args - if args.display_name is not None: - args.name = args.display_name - - if args.display_description is not None: - args.description = args.display_description - - volume_metadata = None - if args.metadata is not None: - volume_metadata = _extract_metadata(args) - - # NOTE(N.S.): take this piece from novaclient - hints = {} - if args.scheduler_hints: - for hint in args.scheduler_hints: - key, _sep, value = hint.partition('=') - # NOTE(vish): multiple copies of same hint will - # result in a list of values - if key in hints: - if isinstance(hints[key], six.string_types): - hints[key] = [hints[key]] - hints[key] += [value] - else: - hints[key] = value - # NOTE(N.S.): end of taken piece - - # Keep backward compatibility with image_id, favoring explicit ID - image_ref = args.image_id or args.image or args.image_ref - - volume = cs.volumes.create(args.size, - args.consisgroup_id, - args.snapshot_id, - args.source_volid, - args.name, - args.description, - args.volume_type, - availability_zone=args.availability_zone, - imageRef=image_ref, - metadata=volume_metadata, - scheduler_hints=hints, - source_replica=args.source_replica, - multiattach=args.multiattach) - - info = dict() - volume = cs.volumes.get(volume.id) - info.update(volume._info) - - info.pop('links', None) - utils.print_dict(info) - - -@utils.arg('--cascade', - metavar='', - default=False, - const=True, - nargs='?', - help='Remove any snapshots along with volume. Default=False.') -@utils.arg('volume', - metavar='', nargs='+', - help='Name or ID of volume or volumes to delete.') -@utils.service_type('volumev2') -def do_delete(cs, args): - """Removes one or more volumes.""" - failure_count = 0 - for volume in args.volume: - try: - utils.find_volume(cs, volume).delete(cascade=args.cascade) - print("Request to delete volume %s has been accepted." % (volume)) - except Exception as e: - failure_count += 1 - print("Delete for volume %s failed: %s" % (volume, e)) - if failure_count == len(args.volume): - raise exceptions.CommandError("Unable to delete any of the specified " - "volumes.") - - -@utils.arg('volume', - metavar='', nargs='+', - help='Name or ID of volume or volumes to delete.') -@utils.service_type('volumev2') -def do_force_delete(cs, args): - """Attempts force-delete of volume, regardless of state.""" - failure_count = 0 - for volume in args.volume: - try: - utils.find_volume(cs, volume).force_delete() - except Exception as e: - failure_count += 1 - print("Delete for volume %s failed: %s" % (volume, e)) - if failure_count == len(args.volume): - raise exceptions.CommandError("Unable to force delete any of the " - "specified volumes.") - - -@utils.arg('volume', metavar='', nargs='+', - help='Name or ID of volume to modify.') -@utils.arg('--state', metavar='', default='available', - help=('The state to assign to the volume. Valid values are ' - '"available", "error", "creating", "deleting", "in-use", ' - '"attaching", "detaching", "error_deleting" and ' - '"maintenance". ' - 'NOTE: This command simply changes the state of the ' - 'Volume in the DataBase with no regard to actual status, ' - 'exercise caution when using. Default=available.')) -@utils.arg('--attach-status', metavar='', default=None, - help=('The attach status to assign to the volume in the DataBase, ' - 'with no regard to the actual status. Valid values are ' - '"attached" and "detached". Default=None, that means the ' - 'status is unchanged.')) -@utils.arg('--reset-migration-status', - action='store_true', - help=('Clears the migration status of the volume in the DataBase ' - 'that indicates the volume is source or destination of ' - 'volume migration, with no regard to the actual status.')) -@utils.service_type('volumev2') -def do_reset_state(cs, args): - """Explicitly updates the volume state in the Cinder database. - - Note that this does not affect whether the volume is actually attached to - the Nova compute host or instance and can result in an unusable volume. - Being a database change only, this has no impact on the true state of the - volume and may not match the actual state. This can render a volume - unusable in the case of change to the 'available' state. - """ - failure_flag = False - migration_status = 'none' if args.reset_migration_status else None - - for volume in args.volume: - try: - utils.find_volume(cs, volume).reset_state(args.state, - args.attach_status, - migration_status) - except Exception as e: - failure_flag = True - msg = "Reset state for volume %s failed: %s" % (volume, e) - print(msg) - - if failure_flag: - msg = "Unable to reset the state for the specified volume(s)." - raise exceptions.CommandError(msg) - - -@utils.arg('volume', - metavar='', - help='Name or ID of volume to rename.') -@utils.arg('name', - nargs='?', - metavar='', - help='New name for volume.') -@utils.arg('--description', metavar='', - help='Volume description. Default=None.', - default=None) -@utils.arg('--display-description', - help=argparse.SUPPRESS) -@utils.arg('--display_description', - help=argparse.SUPPRESS) -@utils.service_type('volumev2') -def do_rename(cs, args): - """Renames a volume.""" - kwargs = {} - - if args.name is not None: - kwargs['name'] = args.name - if args.display_description is not None: - kwargs['description'] = args.display_description - elif args.description is not None: - kwargs['description'] = args.description - - if not any(kwargs): - msg = 'Must supply either name or description.' - raise exceptions.ClientException(code=1, message=msg) - - utils.find_volume(cs, args.volume).update(**kwargs) - - -@utils.arg('volume', - metavar='', - help='Name or ID of volume for which to update metadata.') -@utils.arg('action', - metavar='', - choices=['set', 'unset'], - help='The action. Valid values are "set" or "unset."') -@utils.arg('metadata', - metavar='', - nargs='+', - default=[], - help='Metadata key and value pair to set or unset. ' - 'For unset, specify only the key.') -@utils.service_type('volumev2') -def do_metadata(cs, args): - """Sets or deletes volume metadata.""" - volume = utils.find_volume(cs, args.volume) - metadata = _extract_metadata(args) - - if args.action == 'set': - cs.volumes.set_metadata(volume, metadata) - elif args.action == 'unset': - # NOTE(zul): Make sure py2/py3 sorting is the same - cs.volumes.delete_metadata(volume, sorted(metadata.keys(), - reverse=True)) - - -@utils.arg('volume', - metavar='', - help='Name or ID of volume for which to update metadata.') -@utils.arg('action', - metavar='', - choices=['set', 'unset'], - help="The action. Valid values are 'set' or 'unset.'") -@utils.arg('metadata', - metavar='', - nargs='+', - default=[], - help='Metadata key and value pair to set or unset. ' - 'For unset, specify only the key.') -@utils.service_type('volumev2') -def do_image_metadata(cs, args): - """Sets or deletes volume image metadata.""" - volume = utils.find_volume(cs, args.volume) - metadata = _extract_metadata(args) - - if args.action == 'set': - cs.volumes.set_image_metadata(volume, metadata) - elif args.action == 'unset': - cs.volumes.delete_image_metadata(volume, sorted(metadata.keys(), - reverse=True)) - - -@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('--all_tenants', - nargs='?', - type=int, - const=1, - help=argparse.SUPPRESS) -@utils.arg('--name', - metavar='', - default=None, - help='Filters results by a name. Default=None.') -@utils.arg('--display-name', - help=argparse.SUPPRESS) -@utils.arg('--display_name', - help=argparse.SUPPRESS) -@utils.arg('--status', - metavar='', - default=None, - help='Filters results by a status. Default=None.') -@utils.arg('--volume-id', - metavar='', - default=None, - help='Filters results by a volume ID. Default=None.') -@utils.arg('--volume_id', - help=argparse.SUPPRESS) -@utils.arg('--marker', - metavar='', - default=None, - help='Begin returning snapshots that appear later in the snapshot ' - 'list than that represented by this id. ' - 'Default=None.') -@utils.arg('--limit', - metavar='', - default=None, - help='Maximum number of snapshots to return. Default=None.') -@utils.arg('--sort', - metavar='[:]', - default=None, - help=(('Comma-separated list of sort keys and directions in the ' - 'form of [:]. ' - 'Valid keys: %s. ' - 'Default=None.') % ', '.join(base.SORT_KEY_VALUES))) -@utils.arg('--tenant', - type=str, - dest='tenant', - nargs='?', - metavar='', - help='Display information from single tenant (Admin only).') -@utils.service_type('volumev2') -def do_snapshot_list(cs, args): - """Lists all snapshots.""" - all_tenants = (1 if args.tenant else - int(os.environ.get("ALL_TENANTS", args.all_tenants))) - - if args.display_name is not None: - args.name = args.display_name - - search_opts = { - 'all_tenants': all_tenants, - 'display_name': args.name, - 'status': args.status, - 'volume_id': args.volume_id, - 'project_id': args.tenant, - } - - snapshots = cs.volume_snapshots.list(search_opts=search_opts, - marker=args.marker, - limit=args.limit, - sort=args.sort) - _translate_volume_snapshot_keys(snapshots) - if args.sort: - sortby_index = None - else: - sortby_index = 0 - - utils.print_list(snapshots, - ['ID', 'Volume ID', 'Status', 'Name', 'Size'], - sortby_index=sortby_index) - - -@utils.arg('snapshot', - metavar='', - help='Name or ID of snapshot.') -@utils.service_type('volumev2') -def do_snapshot_show(cs, args): - """Shows snapshot details.""" - snapshot = _find_volume_snapshot(cs, args.snapshot) - _print_volume_snapshot(snapshot) - - -@utils.arg('volume', - metavar='', - help='Name or ID of volume to snapshot.') -@utils.arg('--force', - metavar='', - const=True, - nargs='?', - default=False, - help='Allows or disallows snapshot of ' - 'a volume when the volume is attached to an instance. ' - 'If set to True, ignores the current status of the ' - 'volume when attempting to snapshot it rather ' - 'than forcing it to be available. ' - 'Default=False.') -@utils.arg('--name', - metavar='', - default=None, - help='Snapshot name. Default=None.') -@utils.arg('--display-name', - help=argparse.SUPPRESS) -@utils.arg('--display_name', - help=argparse.SUPPRESS) -@utils.arg('--description', - metavar='', - default=None, - help='Snapshot description. Default=None.') -@utils.arg('--display-description', - help=argparse.SUPPRESS) -@utils.arg('--display_description', - help=argparse.SUPPRESS) -@utils.arg('--metadata', - type=str, - nargs='*', - metavar='', - default=None, - help='Snapshot metadata key and value pairs. Default=None.') -@utils.service_type('volumev2') -def do_snapshot_create(cs, args): - """Creates a snapshot.""" - if args.display_name is not None: - args.name = args.display_name - - if args.display_description is not None: - args.description = args.display_description - - snapshot_metadata = None - if args.metadata is not None: - snapshot_metadata = _extract_metadata(args) - - volume = utils.find_volume(cs, args.volume) - snapshot = cs.volume_snapshots.create(volume.id, - args.force, - args.name, - args.description, - metadata=snapshot_metadata) - _print_volume_snapshot(snapshot) - - -@utils.arg('snapshot', - metavar='', nargs='+', - help='Name or ID of the snapshot(s) to delete.') -@utils.service_type('volumev2') -def do_snapshot_delete(cs, args): - """Removes one or more snapshots.""" - failure_count = 0 - for snapshot in args.snapshot: - try: - _find_volume_snapshot(cs, snapshot).delete() - except Exception as e: - failure_count += 1 - print("Delete for snapshot %s failed: %s" % (snapshot, e)) - if failure_count == len(args.snapshot): - raise exceptions.CommandError("Unable to delete any of the specified " - "snapshots.") - - -@utils.arg('snapshot', metavar='', - help='Name or ID of snapshot.') -@utils.arg('name', nargs='?', metavar='', - help='New name for snapshot.') -@utils.arg('--description', metavar='', - default=None, - help='Snapshot description. Default=None.') -@utils.arg('--display-description', - help=argparse.SUPPRESS) -@utils.arg('--display_description', - help=argparse.SUPPRESS) -@utils.service_type('volumev2') -def do_snapshot_rename(cs, args): - """Renames a snapshot.""" - kwargs = {} - - if args.name is not None: - kwargs['name'] = args.name - - if args.description is not None: - kwargs['description'] = args.description - elif args.display_description is not None: - kwargs['description'] = args.display_description - - if not any(kwargs): - msg = 'Must supply either name or description.' - raise exceptions.ClientException(code=1, message=msg) - - _find_volume_snapshot(cs, args.snapshot).update(**kwargs) - - -@utils.arg('snapshot', metavar='', nargs='+', - help='Name or ID of snapshot to modify.') -@utils.arg('--state', metavar='', - default='available', - help=('The state to assign to the snapshot. Valid values are ' - '"available", "error", "creating", "deleting", and ' - '"error_deleting". NOTE: This command simply changes ' - 'the state of the Snapshot in the DataBase with no regard ' - 'to actual status, exercise caution when using. ' - 'Default=available.')) -@utils.service_type('volumev2') -def do_snapshot_reset_state(cs, args): - """Explicitly updates the snapshot state.""" - failure_count = 0 - - single = (len(args.snapshot) == 1) - - for snapshot in args.snapshot: - try: - _find_volume_snapshot(cs, snapshot).reset_state(args.state) - except Exception as e: - failure_count += 1 - msg = "Reset state for snapshot %s failed: %s" % (snapshot, e) - if not single: - print(msg) - - if failure_count == len(args.snapshot): - if not single: - msg = ("Unable to reset the state for any of the specified " - "snapshots.") - raise exceptions.CommandError(msg) - - -def _print_volume_type_list(vtypes): - utils.print_list(vtypes, ['ID', 'Name', 'Description', 'Is_Public']) - - -@utils.service_type('volumev2') -def do_type_list(cs, args): - """Lists available 'volume types'. (Admin only will see private types)""" - vtypes = cs.volume_types.list() - _print_volume_type_list(vtypes) - - -@utils.service_type('volumev2') -def do_type_default(cs, args): - """List the default volume type.""" - vtype = cs.volume_types.default() - _print_volume_type_list([vtype]) - - -@utils.arg('volume_type', - metavar='', - help='Name or ID of the volume type.') -@utils.service_type('volumev2') -def do_type_show(cs, args): - """Show volume type details.""" - vtype = _find_vtype(cs, args.volume_type) - info = dict() - info.update(vtype._info) - - info.pop('links', None) - utils.print_dict(info) - - -@utils.arg('id', - metavar='', - help='ID of the volume type.') -@utils.arg('--name', - metavar='', - help='Name of the volume type.') -@utils.arg('--description', - metavar='', - help='Description of the volume type.') -@utils.arg('--is-public', - metavar='', - help='Make type accessible to the public or not.') -@utils.service_type('volumev2') -def do_type_update(cs, args): - """Updates volume type name, description, and/or is_public.""" - is_public = strutils.bool_from_string(args.is_public) - vtype = cs.volume_types.update(args.id, args.name, args.description, - is_public) - _print_volume_type_list([vtype]) - - -@utils.service_type('volumev2') -def do_extra_specs_list(cs, args): - """Lists current volume types and extra specs.""" - vtypes = cs.volume_types.list() - utils.print_list(vtypes, ['ID', 'Name', 'extra_specs']) - - -@utils.arg('name', - metavar='', - help='Name of new volume type.') -@utils.arg('--description', - metavar='', - help='Description of new volume type.') -@utils.arg('--is-public', - metavar='', - default=True, - help='Make type accessible to the public (default true).') -@utils.service_type('volumev2') -def do_type_create(cs, args): - """Creates a volume type.""" - is_public = strutils.bool_from_string(args.is_public) - vtype = cs.volume_types.create(args.name, args.description, is_public) - _print_volume_type_list([vtype]) - - -@utils.arg('vol_type', - metavar='', nargs='+', - help='Name or ID of volume type or types to delete.') -@utils.service_type('volumev2') -def do_type_delete(cs, args): - """Deletes volume type or types.""" - failure_count = 0 - for vol_type in args.vol_type: - try: - vtype = _find_volume_type(cs, vol_type) - cs.volume_types.delete(vtype) - print("Request to delete volume type %s has been accepted." - % (vol_type)) - except Exception as e: - failure_count += 1 - print("Delete for volume type %s failed: %s" % (vol_type, e)) - if failure_count == len(args.vol_type): - raise exceptions.CommandError("Unable to delete any of the " - "specified types.") - - -@utils.arg('vtype', - metavar='', - help='Name or ID of volume type.') -@utils.arg('action', - metavar='', - choices=['set', 'unset'], - help='The action. Valid values are "set" or "unset."') -@utils.arg('metadata', - metavar='', - nargs='+', - default=[], - help='The extra specs key and value pair to set or unset. ' - 'For unset, specify only the key.') -@utils.service_type('volumev2') -def do_type_key(cs, args): - """Sets or unsets extra_spec for a volume type.""" - vtype = _find_volume_type(cs, args.vtype) - keypair = _extract_metadata(args) - - if args.action == 'set': - vtype.set_keys(keypair) - elif args.action == 'unset': - vtype.unset_keys(list(keypair)) - - -@utils.arg('--volume-type', metavar='', required=True, - help='Filter results by volume type name or ID.') -@utils.service_type('volumev2') -def do_type_access_list(cs, args): - """Print access information about the given volume type.""" - volume_type = _find_volume_type(cs, args.volume_type) - if volume_type.is_public: - raise exceptions.CommandError("Failed to get access list " - "for public volume type.") - access_list = cs.volume_type_access.list(volume_type) - - columns = ['Volume_type_ID', 'Project_ID'] - utils.print_list(access_list, columns) - - -@utils.arg('--volume-type', metavar='', required=True, - help='Volume type name or ID to add access for the given project.') -@utils.arg('--project-id', metavar='', required=True, - help='Project ID to add volume type access for.') -@utils.service_type('volumev2') -def do_type_access_add(cs, args): - """Adds volume type access for the given project.""" - vtype = _find_volume_type(cs, args.volume_type) - cs.volume_type_access.add_project_access(vtype, args.project_id) - - -@utils.arg('--volume-type', metavar='', required=True, - help=('Volume type name or ID to remove access ' - 'for the given project.')) -@utils.arg('--project-id', metavar='', required=True, - help='Project ID to remove volume type access for.') -@utils.service_type('volumev2') -def do_type_access_remove(cs, args): - """Removes volume type access for the given project.""" - vtype = _find_volume_type(cs, args.volume_type) - cs.volume_type_access.remove_project_access( - vtype, args.project_id) - - -@utils.service_type('volumev2') -def do_endpoints(cs, args): - """Discovers endpoints registered by authentication service.""" - catalog = cs.client.service_catalog.catalog - for e in catalog['serviceCatalog']: - utils.print_dict(e['endpoints'][0], e['name']) - - -@utils.service_type('volumev2') -def do_credentials(cs, args): - """Shows user credentials returned from auth.""" - catalog = cs.client.service_catalog.catalog - - # formatters defines field to be converted from unicode to string - utils.print_dict(catalog['user'], "User Credentials", - formatters=['domain', 'roles']) - utils.print_dict(catalog['token'], "Token", - formatters=['audit_ids', 'tenant']) - -_quota_resources = ['volumes', 'snapshots', 'gigabytes', - 'backups', 'backup_gigabytes', - 'consistencygroups', 'per_volume_gigabytes'] -_quota_infos = ['Type', 'In_use', 'Reserved', 'Limit'] - - -def _quota_show(quotas): - quota_dict = {} - for resource in quotas._info: - good_name = False - for name in _quota_resources: - if resource.startswith(name): - good_name = True - if not good_name: - continue - quota_dict[resource] = getattr(quotas, resource, None) - utils.print_dict(quota_dict) - - -def _quota_usage_show(quotas): - quota_list = [] - for resource in quotas._info.keys(): - good_name = False - for name in _quota_resources: - if resource.startswith(name): - good_name = True - if not good_name: - continue - quota_info = getattr(quotas, resource, None) - quota_info['Type'] = resource - quota_info = dict((k.capitalize(), v) for k, v in quota_info.items()) - quota_list.append(quota_info) - utils.print_list(quota_list, _quota_infos) - - -def _quota_update(manager, identifier, args): - updates = {} - for resource in _quota_resources: - val = getattr(args, resource, None) - if val is not None: - if args.volume_type: - resource = resource + '_%s' % args.volume_type - updates[resource] = val - - if updates: - _quota_show(manager.update(identifier, **updates)) - - -@utils.arg('tenant', - metavar='', - help='ID of tenant for which to list quotas.') -@utils.service_type('volumev2') -def do_quota_show(cs, args): - """Lists quotas for a tenant.""" - - _quota_show(cs.quotas.get(args.tenant)) - - -@utils.arg('tenant', metavar='', - help='ID of tenant for which to list quota usage.') -@utils.service_type('volumev2') -def do_quota_usage(cs, args): - """Lists quota usage for a tenant.""" - - _quota_usage_show(cs.quotas.get(args.tenant, usage=True)) - - -@utils.arg('tenant', - metavar='', - help='ID of tenant for which to list quota defaults.') -@utils.service_type('volumev2') -def do_quota_defaults(cs, args): - """Lists default quotas for a tenant.""" - - _quota_show(cs.quotas.defaults(args.tenant)) - - -@utils.arg('tenant', - metavar='', - help='ID of tenant for which to set quotas.') -@utils.arg('--volumes', - metavar='', - type=int, default=None, - help='The new "volumes" quota value. Default=None.') -@utils.arg('--snapshots', - metavar='', - type=int, default=None, - help='The new "snapshots" quota value. Default=None.') -@utils.arg('--gigabytes', - metavar='', - type=int, default=None, - help='The new "gigabytes" quota value. Default=None.') -@utils.arg('--backups', - metavar='', - type=int, default=None, - help='The new "backups" quota value. Default=None.') -@utils.arg('--backup-gigabytes', - metavar='', - type=int, default=None, - help='The new "backup_gigabytes" quota value. Default=None.') -@utils.arg('--consistencygroups', - metavar='', - type=int, default=None, - help='The new "consistencygroups" quota value. Default=None.') -@utils.arg('--volume-type', - metavar='', - default=None, - help='Volume type. Default=None.') -@utils.arg('--per-volume-gigabytes', - metavar='', - type=int, default=None, - help='Set max volume size limit. Default=None.') -@utils.service_type('volumev2') -def do_quota_update(cs, args): - """Updates quotas for a tenant.""" - - _quota_update(cs.quotas, args.tenant, args) - - -@utils.arg('tenant', metavar='', - help='UUID of tenant to delete the quotas for.') -@utils.service_type('volumev2') -def do_quota_delete(cs, args): - """Delete the quotas for a tenant.""" - - cs.quotas.delete(args.tenant) - - -@utils.arg('class_name', - metavar='', - help='Name of quota class for which to list quotas.') -@utils.service_type('volumev2') -def do_quota_class_show(cs, args): - """Lists quotas for a quota class.""" - - _quota_show(cs.quota_classes.get(args.class_name)) - - -@utils.arg('class_name', - metavar='', - help='Name of quota class for which to set quotas.') -@utils.arg('--volumes', - metavar='', - type=int, default=None, - help='The new "volumes" quota value. Default=None.') -@utils.arg('--snapshots', - metavar='', - type=int, default=None, - help='The new "snapshots" quota value. Default=None.') -@utils.arg('--gigabytes', - metavar='', - type=int, default=None, - help='The new "gigabytes" quota value. Default=None.') -@utils.arg('--volume-type', - metavar='', - default=None, - help='Volume type. Default=None.') -@utils.service_type('volumev2') -def do_quota_class_update(cs, args): - """Updates quotas for a quota class.""" - - _quota_update(cs.quota_classes, args.class_name, args) - - -@utils.service_type('volumev2') -def do_absolute_limits(cs, args): - """Lists absolute limits for a user.""" - limits = cs.limits.get().absolute - columns = ['Name', 'Value'] - utils.print_list(limits, columns) - - -@utils.service_type('volumev2') -def do_rate_limits(cs, args): - """Lists rate limits for a user.""" - limits = cs.limits.get().rate - columns = ['Verb', 'URI', 'Value', 'Remain', 'Unit', 'Next_Available'] - utils.print_list(limits, columns) - - -def _find_volume_type(cs, vtype): - """Gets a volume type by name or ID.""" - return utils.find_resource(cs.volume_types, vtype) - - -@utils.arg('volume', - metavar='', - help='Name or ID of volume to snapshot.') -@utils.arg('--force', - metavar='', - const=True, - nargs='?', - default=False, - help='Enables or disables upload of ' - 'a volume that is attached to an instance. ' - 'Default=False.') -@utils.arg('--container-format', - metavar='', - default='bare', - help='Container format type. ' - 'Default is bare.') -@utils.arg('--container_format', - help=argparse.SUPPRESS) -@utils.arg('--disk-format', - metavar='', - default='raw', - help='Disk format type. ' - 'Default is raw.') -@utils.arg('--disk_format', - help=argparse.SUPPRESS) -@utils.arg('image_name', - metavar='', - help='The new image name.') -@utils.arg('--image_name', - help=argparse.SUPPRESS) -@utils.service_type('volumev2') -def do_upload_to_image(cs, args): - """Uploads volume to Image Service as an image.""" - volume = utils.find_volume(cs, args.volume) - _print_volume_image(volume.upload_to_image(args.force, - args.image_name, - args.container_format, - args.disk_format)) - - -@utils.arg('volume', metavar='', help='ID of volume to migrate.') -@utils.arg('host', metavar='', help='Destination host. Takes the form: ' - 'host@backend-name#pool') -@utils.arg('--force-host-copy', metavar='', - choices=['True', 'False'], - required=False, - const=True, - nargs='?', - default=False, - help='Enables or disables generic host-based ' - 'force-migration, which bypasses driver ' - 'optimizations. Default=False.') -@utils.arg('--lock-volume', metavar='', - choices=['True', 'False'], - required=False, - const=True, - nargs='?', - default=False, - help='Enables or disables the termination of volume migration ' - 'caused by other commands. This option applies to the ' - 'available volume. True means it locks the volume ' - 'state and does not allow the migration to be aborted. The ' - 'volume status will be in maintenance during the ' - 'migration. False means it allows the volume migration ' - 'to be aborted. The volume status is still in the original ' - 'status. Default=False.') -@utils.service_type('volumev2') -def do_migrate(cs, args): - """Migrates volume to a new host.""" - volume = utils.find_volume(cs, args.volume) - try: - volume.migrate_volume(args.host, args.force_host_copy, - args.lock_volume) - print("Request to migrate volume %s has been accepted." % (volume)) - except Exception as e: - print("Migration for volume %s failed: %s." % (volume, - six.text_type(e))) - - -@utils.arg('volume', metavar='', - help='Name or ID of volume for which to modify type.') -@utils.arg('new_type', metavar='', help='New volume type.') -@utils.arg('--migration-policy', metavar='', required=False, - choices=['never', 'on-demand'], default='never', - help='Migration policy during retype of volume.') -@utils.service_type('volumev2') -def do_retype(cs, args): - """Changes the volume type for a volume.""" - volume = utils.find_volume(cs, args.volume) - volume.retype(args.new_type, args.migration_policy) - - -@utils.arg('volume', metavar='', - help='Name or ID of volume to backup.') -@utils.arg('--container', metavar='', - default=None, - help='Backup container name. Default=None.') -@utils.arg('--display-name', - help=argparse.SUPPRESS) -@utils.arg('--name', metavar='', - default=None, - help='Backup name. Default=None.') -@utils.arg('--display-description', - help=argparse.SUPPRESS) -@utils.arg('--description', - metavar='', - default=None, - help='Backup description. Default=None.') -@utils.arg('--incremental', - action='store_true', - help='Incremental backup. Default=False.', - default=False) -@utils.arg('--force', - action='store_true', - help='Allows or disallows backup of a volume ' - 'when the volume is attached to an instance. ' - 'If set to True, backs up the volume whether ' - 'its status is "available" or "in-use". The backup ' - 'of an "in-use" volume means your data is crash ' - 'consistent. Default=False.', - default=False) -@utils.arg('--snapshot-id', - metavar='', - default=None, - help='ID of snapshot to backup. Default=None.') -@utils.service_type('volumev2') -def do_backup_create(cs, args): - """Creates a volume backup.""" - if args.display_name is not None: - args.name = args.display_name - - if args.display_description is not None: - args.description = args.display_description - - volume = utils.find_volume(cs, args.volume) - backup = cs.backups.create(volume.id, - args.container, - args.name, - args.description, - args.incremental, - args.force, - args.snapshot_id) - - info = {"volume_id": volume.id} - info.update(backup._info) - - if 'links' in info: - info.pop('links') - - utils.print_dict(info) - - -@utils.arg('backup', metavar='', help='Name or ID of backup.') -@utils.service_type('volumev2') -def do_backup_show(cs, args): - """Shows backup details.""" - backup = _find_backup(cs, args.backup) - info = dict() - info.update(backup._info) - - info.pop('links', None) - utils.print_dict(info) - - -@utils.arg('--all-tenants', - metavar='', - nargs='?', - type=int, - const=1, - default=0, - help='Shows details for all tenants. Admin only.') -@utils.arg('--all_tenants', - nargs='?', - type=int, - const=1, - help=argparse.SUPPRESS) -@utils.arg('--name', - metavar='', - default=None, - help='Filters results by a name. Default=None.') -@utils.arg('--status', - metavar='', - default=None, - help='Filters results by a status. Default=None.') -@utils.arg('--volume-id', - metavar='', - default=None, - help='Filters results by a volume ID. Default=None.') -@utils.arg('--volume_id', - help=argparse.SUPPRESS) -@utils.arg('--marker', - metavar='', - default=None, - help='Begin returning backups that appear later in the backup ' - 'list than that represented by this id. ' - 'Default=None.') -@utils.arg('--limit', - metavar='', - default=None, - help='Maximum number of backups to return. Default=None.') -@utils.arg('--sort', - metavar='[:]', - default=None, - help=(('Comma-separated list of sort keys and directions in the ' - 'form of [:]. ' - 'Valid keys: %s. ' - 'Default=None.') % ', '.join(base.SORT_KEY_VALUES))) -@utils.service_type('volumev2') -def do_backup_list(cs, args): - """Lists all backups.""" - - search_opts = { - 'all_tenants': args.all_tenants, - 'name': args.name, - 'status': args.status, - 'volume_id': args.volume_id, - } - - backups = cs.backups.list(search_opts=search_opts, - marker=args.marker, - limit=args.limit, - sort=args.sort) - _translate_volume_snapshot_keys(backups) - columns = ['ID', 'Volume ID', 'Status', 'Name', 'Size', 'Object Count', - 'Container'] - if args.sort: - sortby_index = None - else: - sortby_index = 0 - utils.print_list(backups, columns, sortby_index=sortby_index) - - -@utils.arg('backup', metavar='', nargs='+', - help='Name or ID of backup(s) to delete.') -@utils.service_type('volumev2') -def do_backup_delete(cs, args): - """Removes one or more backups.""" - failure_count = 0 - for backup in args.backup: - try: - _find_backup(cs, backup).delete() - print("Request to delete backup %s has been accepted." % (backup)) - except Exception as e: - failure_count += 1 - print("Delete for backup %s failed: %s" % (backup, e)) - if failure_count == len(args.backup): - raise exceptions.CommandError("Unable to delete any of the specified " - "backups.") - - -@utils.arg('backup', metavar='', - help='ID of backup to restore.') -@utils.arg('--volume-id', metavar='', - default=None, - help=argparse.SUPPRESS) -@utils.arg('--volume', metavar='', - default=None, - help='Name or ID of volume to which to restore. ' - 'Default=None.') -@utils.service_type('volumev2') -def do_backup_restore(cs, args): - """Restores a backup.""" - vol = args.volume or args.volume_id - if vol: - volume_id = utils.find_volume(cs, vol).id - else: - volume_id = None - - restore = cs.restores.restore(args.backup, volume_id) - - info = {"backup_id": args.backup} - info.update(restore._info) - - info.pop('links', None) - - utils.print_dict(info) - - -@utils.arg('backup', metavar='', - help='ID of the backup to export.') -@utils.service_type('volumev2') -def do_backup_export(cs, args): - """Export backup metadata record.""" - info = cs.backups.export_record(args.backup) - utils.print_dict(info) - - -@utils.arg('backup_service', metavar='', - help='Backup service to use for importing the backup.') -@utils.arg('backup_url', metavar='', - help='Backup URL for importing the backup metadata.') -@utils.service_type('volumev2') -def do_backup_import(cs, args): - """Import backup metadata record.""" - info = cs.backups.import_record(args.backup_service, args.backup_url) - info.pop('links', None) - - utils.print_dict(info) - - -@utils.arg('backup', metavar='', nargs='+', - help='Name or ID of the backup to modify.') -@utils.arg('--state', metavar='', - default='available', - help='The state to assign to the backup. Valid values are ' - '"available", "error". Default=available.') -@utils.service_type('volumev2') -def do_backup_reset_state(cs, args): - """Explicitly updates the backup state.""" - failure_count = 0 - - single = (len(args.backup) == 1) - - for backup in args.backup: - try: - _find_backup(cs, backup).reset_state(args.state) - except Exception as e: - failure_count += 1 - msg = "Reset state for backup %s failed: %s" % (backup, e) - if not single: - print(msg) - - if failure_count == len(args.backup): - if not single: - msg = ("Unable to reset the state for any of the specified " - "backups.") - raise exceptions.CommandError(msg) - - -@utils.arg('volume', metavar='', - help='Name or ID of volume to transfer.') -@utils.arg('--name', - metavar='', - default=None, - help='Transfer name. Default=None.') -@utils.arg('--display-name', - help=argparse.SUPPRESS) -@utils.service_type('volumev2') -def do_transfer_create(cs, args): - """Creates a volume transfer.""" - if args.display_name is not None: - args.name = args.display_name - - volume = utils.find_volume(cs, args.volume) - transfer = cs.transfers.create(volume.id, - args.name) - info = dict() - info.update(transfer._info) - - info.pop('links', None) - utils.print_dict(info) - - -@utils.arg('transfer', metavar='', - help='Name or ID of transfer to delete.') -@utils.service_type('volumev2') -def do_transfer_delete(cs, args): - """Undoes a transfer.""" - transfer = _find_transfer(cs, args.transfer) - transfer.delete() - - -@utils.arg('transfer', metavar='', - help='ID of transfer to accept.') -@utils.arg('auth_key', metavar='', - help='Authentication key of transfer to accept.') -@utils.service_type('volumev2') -def do_transfer_accept(cs, args): - """Accepts a volume transfer.""" - transfer = cs.transfers.accept(args.transfer, args.auth_key) - info = dict() - info.update(transfer._info) - - info.pop('links', None) - utils.print_dict(info) - - -@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('--all_tenants', - nargs='?', - type=int, - const=1, - help=argparse.SUPPRESS) -@utils.service_type('volumev2') -def do_transfer_list(cs, args): - """Lists all transfers.""" - all_tenants = int(os.environ.get("ALL_TENANTS", args.all_tenants)) - search_opts = { - 'all_tenants': all_tenants, - } - transfers = cs.transfers.list(search_opts=search_opts) - columns = ['ID', 'Volume ID', 'Name'] - utils.print_list(transfers, columns) - - -@utils.arg('transfer', metavar='', - help='Name or ID of transfer to accept.') -@utils.service_type('volumev2') -def do_transfer_show(cs, args): - """Shows transfer details.""" - transfer = _find_transfer(cs, args.transfer) - info = dict() - info.update(transfer._info) - - info.pop('links', None) - utils.print_dict(info) - - -@utils.arg('volume', metavar='', - help='Name or ID of volume to extend.') -@utils.arg('new_size', - metavar='', - type=int, - help='New size of volume, in GiBs.') -@utils.service_type('volumev2') -def do_extend(cs, args): - """Attempts to extend size of an existing volume.""" - volume = utils.find_volume(cs, args.volume) - cs.volumes.extend(volume, args.new_size) - - -@utils.arg('--host', metavar='', default=None, - help='Host name. Default=None.') -@utils.arg('--binary', metavar='', default=None, - help='Service binary. Default=None.') -@utils.arg('--withreplication', - metavar='', - const=True, - nargs='?', - default=False, - help='Enables or disables display of ' - 'Replication info for c-vol services. Default=False.') -@utils.service_type('volumev2') -def do_service_list(cs, args): - """Lists all services. Filter by host and service binary.""" - replication = strutils.bool_from_string(args.withreplication) - result = cs.services.list(host=args.host, binary=args.binary) - columns = ["Binary", "Host", "Zone", "Status", "State", "Updated_at"] - if replication: - columns.extend(["Replication Status", "Active Backend ID", "Frozen"]) - # NOTE(jay-lau-513): we check if the response has disabled_reason - # so as not to add the column when the extended ext is not enabled. - if result and hasattr(result[0], 'disabled_reason'): - columns.append("Disabled Reason") - utils.print_list(result, columns) - - -@utils.arg('host', metavar='', help='Host name.') -@utils.arg('binary', metavar='', help='Service binary.') -@utils.service_type('volumev2') -def do_service_enable(cs, args): - """Enables the service.""" - result = cs.services.enable(args.host, args.binary) - columns = ["Host", "Binary", "Status"] - utils.print_list([result], columns) - - -@utils.arg('host', metavar='', help='Host name.') -@utils.arg('binary', metavar='', help='Service binary.') -@utils.arg('--reason', metavar='', - help='Reason for disabling service.') -@utils.service_type('volumev2') -def do_service_disable(cs, args): - """Disables the service.""" - columns = ["Host", "Binary", "Status"] - if args.reason: - columns.append('Disabled Reason') - result = cs.services.disable_log_reason(args.host, args.binary, - args.reason) - else: - result = cs.services.disable(args.host, args.binary) - utils.print_list([result], columns) - +# Below is shameless hack for unit tests +# TODO remove after deciding if unit tests are moved to /v3/ dir def _treeizeAvailabilityZone(zone): """Builds a tree view for availability zones.""" @@ -1749,906 +64,3 @@ def _treeizeAvailabilityZone(zone): return result -@utils.service_type('volumev2') -def do_availability_zone_list(cs, _args): - """Lists all availability zones.""" - try: - availability_zones = cs.availability_zones.list() - except exceptions.Forbidden as e: # policy doesn't allow probably - try: - availability_zones = cs.availability_zones.list(detailed=False) - except Exception: - raise e - - result = [] - for zone in availability_zones: - result += _treeizeAvailabilityZone(zone) - _translate_availability_zone_keys(result) - utils.print_list(result, ['Name', 'Status']) - - -def _print_volume_encryption_type_list(encryption_types): - """ - Lists volume encryption types. - - :param encryption_types: a list of :class: VolumeEncryptionType instances - """ - utils.print_list(encryption_types, ['Volume Type ID', 'Provider', - 'Cipher', 'Key Size', - 'Control Location']) - - -@utils.service_type('volumev2') -def do_encryption_type_list(cs, args): - """Shows encryption type details for volume types. Admin only.""" - result = cs.volume_encryption_types.list() - utils.print_list(result, ['Volume Type ID', 'Provider', 'Cipher', - 'Key Size', 'Control Location']) - - -@utils.arg('volume_type', - metavar='', - type=str, - help='Name or ID of volume type.') -@utils.service_type('volumev2') -def do_encryption_type_show(cs, args): - """Shows encryption type details for a volume type. Admin only.""" - volume_type = _find_volume_type(cs, args.volume_type) - - result = cs.volume_encryption_types.get(volume_type) - - # Display result or an empty table if no result - if hasattr(result, 'volume_type_id'): - _print_volume_encryption_type_list([result]) - else: - _print_volume_encryption_type_list([]) - - -@utils.arg('volume_type', - metavar='', - type=str, - help='Name or ID of volume type.') -@utils.arg('provider', - metavar='', - type=str, - help='The class that provides encryption support. ' - 'For example, LuksEncryptor.') -@utils.arg('--cipher', - metavar='', - type=str, - required=False, - default=None, - help='The encryption algorithm or mode. ' - 'For example, aes-xts-plain64. Default=None.') -@utils.arg('--key_size', - metavar='', - type=int, - required=False, - default=None, - help='Size of encryption key, in bits. ' - 'For example, 128 or 256. Default=None.') -@utils.arg('--control_location', - metavar='', - choices=['front-end', 'back-end'], - type=str, - required=False, - default='front-end', - help='Notional service where encryption is performed. ' - 'Valid values are "front-end" or "back-end." ' - 'For example, front-end=Nova. Default is "front-end."') -@utils.service_type('volumev2') -def do_encryption_type_create(cs, args): - """Creates encryption type for a volume type. Admin only.""" - volume_type = _find_volume_type(cs, args.volume_type) - - body = { - 'provider': args.provider, - 'cipher': args.cipher, - 'key_size': args.key_size, - 'control_location': args.control_location - } - - result = cs.volume_encryption_types.create(volume_type, body) - _print_volume_encryption_type_list([result]) - - -@utils.arg('volume_type', - metavar='', - type=str, - help="Name or ID of the volume type") -@utils.arg('--provider', - metavar='', - type=str, - required=False, - default=argparse.SUPPRESS, - help="Class providing encryption support (e.g. LuksEncryptor) " - "(Optional)") -@utils.arg('--cipher', - metavar='', - type=str, - nargs='?', - required=False, - default=argparse.SUPPRESS, - const=None, - help="Encryption algorithm/mode to use (e.g., aes-xts-plain64). " - "Provide parameter without value to set to provider default. " - "(Optional)") -@utils.arg('--key-size', - dest='key_size', - metavar='', - type=int, - nargs='?', - required=False, - default=argparse.SUPPRESS, - const=None, - help="Size of the encryption key, in bits (e.g., 128, 256). " - "Provide parameter without value to set to provider default. " - "(Optional)") -@utils.arg('--control-location', - dest='control_location', - metavar='', - choices=['front-end', 'back-end'], - type=str, - required=False, - default=argparse.SUPPRESS, - help="Notional service where encryption is performed (e.g., " - "front-end=Nova). Values: 'front-end', 'back-end' (Optional)") -@utils.service_type('volumev2') -def do_encryption_type_update(cs, args): - """Update encryption type information for a volume type (Admin Only).""" - volume_type = _find_volume_type(cs, args.volume_type) - - # An argument should only be pulled if the user specified the parameter. - body = {} - for attr in ['provider', 'cipher', 'key_size', 'control_location']: - if hasattr(args, attr): - body[attr] = getattr(args, attr) - - cs.volume_encryption_types.update(volume_type, body) - result = cs.volume_encryption_types.get(volume_type) - _print_volume_encryption_type_list([result]) - - -@utils.arg('volume_type', - metavar='', - type=str, - help='Name or ID of volume type.') -@utils.service_type('volumev2') -def do_encryption_type_delete(cs, args): - """Deletes encryption type for a volume type. Admin only.""" - volume_type = _find_volume_type(cs, args.volume_type) - cs.volume_encryption_types.delete(volume_type) - - -def _print_qos_specs(qos_specs): - - # formatters defines field to be converted from unicode to string - utils.print_dict(qos_specs._info, formatters=['specs']) - - -def _print_qos_specs_list(q_specs): - utils.print_list(q_specs, ['ID', 'Name', 'Consumer', 'specs']) - - -def _print_qos_specs_and_associations_list(q_specs): - utils.print_list(q_specs, ['ID', 'Name', 'Consumer', 'specs']) - - -def _print_associations_list(associations): - utils.print_list(associations, ['Association_Type', 'Name', 'ID']) - - -@utils.arg('name', - metavar='', - help='Name of new QoS specifications.') -@utils.arg('metadata', - metavar='', - nargs='+', - default=[], - help='QoS specifications.') -@utils.service_type('volumev2') -def do_qos_create(cs, args): - """Creates a qos specs.""" - keypair = None - if args.metadata is not None: - keypair = _extract_metadata(args) - qos_specs = cs.qos_specs.create(args.name, keypair) - _print_qos_specs(qos_specs) - - -@utils.service_type('volumev2') -def do_qos_list(cs, args): - """Lists qos specs.""" - qos_specs = cs.qos_specs.list() - _print_qos_specs_list(qos_specs) - - -@utils.arg('qos_specs', metavar='', - help='ID of QoS specifications to show.') -@utils.service_type('volumev2') -def do_qos_show(cs, args): - """Shows qos specs details.""" - qos_specs = _find_qos_specs(cs, args.qos_specs) - _print_qos_specs(qos_specs) - - -@utils.arg('qos_specs', metavar='', - help='ID of QoS specifications to delete.') -@utils.arg('--force', - metavar='', - const=True, - nargs='?', - default=False, - help='Enables or disables deletion of in-use ' - 'QoS specifications. Default=False.') -@utils.service_type('volumev2') -def do_qos_delete(cs, args): - """Deletes a specified qos specs.""" - force = strutils.bool_from_string(args.force) - qos_specs = _find_qos_specs(cs, args.qos_specs) - cs.qos_specs.delete(qos_specs, force) - - -@utils.arg('qos_specs', metavar='', - help='ID of QoS specifications.') -@utils.arg('vol_type_id', metavar='', - help='ID of volume type with which to associate ' - 'QoS specifications.') -@utils.service_type('volumev2') -def do_qos_associate(cs, args): - """Associates qos specs with specified volume type.""" - cs.qos_specs.associate(args.qos_specs, args.vol_type_id) - - -@utils.arg('qos_specs', metavar='', - help='ID of QoS specifications.') -@utils.arg('vol_type_id', metavar='', - help='ID of volume type with which to associate ' - 'QoS specifications.') -@utils.service_type('volumev2') -def do_qos_disassociate(cs, args): - """Disassociates qos specs from specified volume type.""" - cs.qos_specs.disassociate(args.qos_specs, args.vol_type_id) - - -@utils.arg('qos_specs', metavar='', - help='ID of QoS specifications on which to operate.') -@utils.service_type('volumev2') -def do_qos_disassociate_all(cs, args): - """Disassociates qos specs from all its associations.""" - cs.qos_specs.disassociate_all(args.qos_specs) - - -@utils.arg('qos_specs', metavar='', - help='ID of QoS specifications.') -@utils.arg('action', - metavar='', - choices=['set', 'unset'], - help='The action. Valid values are "set" or "unset."') -@utils.arg('metadata', metavar='key=value', - nargs='+', - default=[], - help='Metadata key and value pair to set or unset. ' - 'For unset, specify only the key.') -@utils.service_type('volumev2') -def do_qos_key(cs, args): - """Sets or unsets specifications for a qos spec.""" - keypair = _extract_metadata(args) - - if args.action == 'set': - cs.qos_specs.set_keys(args.qos_specs, keypair) - elif args.action == 'unset': - cs.qos_specs.unset_keys(args.qos_specs, list(keypair)) - - -@utils.arg('qos_specs', metavar='', - help='ID of QoS specifications.') -@utils.service_type('volumev2') -def do_qos_get_association(cs, args): - """Lists all associations for specified qos specs.""" - associations = cs.qos_specs.get_associations(args.qos_specs) - _print_associations_list(associations) - - -@utils.arg('snapshot', - metavar='', - help='ID of snapshot for which to update metadata.') -@utils.arg('action', - metavar='', - choices=['set', 'unset'], - help='The action. Valid values are "set" or "unset."') -@utils.arg('metadata', - metavar='', - nargs='+', - default=[], - help='Metadata key and value pair to set or unset. ' - 'For unset, specify only the key.') -@utils.service_type('volumev2') -def do_snapshot_metadata(cs, args): - """Sets or deletes snapshot metadata.""" - snapshot = _find_volume_snapshot(cs, args.snapshot) - metadata = _extract_metadata(args) - - if args.action == 'set': - metadata = snapshot.set_metadata(metadata) - utils.print_dict(metadata._info) - elif args.action == 'unset': - snapshot.delete_metadata(list(metadata.keys())) - - -@utils.arg('snapshot', metavar='', - help='ID of snapshot.') -@utils.service_type('volumev2') -def do_snapshot_metadata_show(cs, args): - """Shows snapshot metadata.""" - snapshot = _find_volume_snapshot(cs, args.snapshot) - utils.print_dict(snapshot._info['metadata'], 'Metadata-property') - - -@utils.arg('volume', metavar='', - help='ID of volume.') -@utils.service_type('volumev2') -def do_metadata_show(cs, args): - """Shows volume metadata.""" - volume = utils.find_volume(cs, args.volume) - utils.print_dict(volume._info['metadata'], 'Metadata-property') - - -@utils.arg('volume', metavar='', - help='ID of volume.') -@utils.service_type('volumev2') -def do_image_metadata_show(cs, args): - """Shows volume image metadata.""" - volume = utils.find_volume(cs, args.volume) - resp, body = volume.show_image_metadata(volume) - utils.print_dict(body['metadata'], 'Metadata-property') - - -@utils.arg('volume', - metavar='', - help='ID of volume for which to update metadata.') -@utils.arg('metadata', - metavar='', - nargs='+', - default=[], - help='Metadata key and value pair or pairs to update.') -@utils.service_type('volumev2') -def do_metadata_update_all(cs, args): - """Updates volume metadata.""" - volume = utils.find_volume(cs, args.volume) - metadata = _extract_metadata(args) - metadata = volume.update_all_metadata(metadata) - utils.print_dict(metadata['metadata'], 'Metadata-property') - - -@utils.arg('snapshot', - metavar='', - help='ID of snapshot for which to update metadata.') -@utils.arg('metadata', - metavar='', - nargs='+', - default=[], - help='Metadata key and value pair to update.') -@utils.service_type('volumev2') -def do_snapshot_metadata_update_all(cs, args): - """Updates snapshot metadata.""" - snapshot = _find_volume_snapshot(cs, args.snapshot) - metadata = _extract_metadata(args) - metadata = snapshot.update_all_metadata(metadata) - utils.print_dict(metadata) - - -@utils.arg('volume', metavar='', help='ID of volume to update.') -@utils.arg('read_only', - metavar='', - choices=['True', 'true', 'False', 'false'], - help='Enables or disables update of volume to ' - 'read-only access mode.') -@utils.service_type('volumev2') -def do_readonly_mode_update(cs, args): - """Updates volume read-only access-mode flag.""" - volume = utils.find_volume(cs, args.volume) - cs.volumes.update_readonly_flag(volume, - strutils.bool_from_string(args.read_only)) - - -@utils.arg('volume', metavar='', help='ID of the volume to update.') -@utils.arg('bootable', - metavar='', - choices=['True', 'true', 'False', 'false'], - help='Flag to indicate whether volume is bootable.') -@utils.service_type('volumev2') -def do_set_bootable(cs, args): - """Update bootable status of a volume.""" - volume = utils.find_volume(cs, args.volume) - cs.volumes.set_bootable(volume, - strutils.bool_from_string(args.bootable)) - - -@utils.arg('host', - metavar='', - help='Cinder host on which the existing volume resides; ' - 'takes the form: host@backend-name#pool') -@utils.arg('identifier', - metavar='', - help='Name or other Identifier for existing volume') -@utils.arg('--id-type', - metavar='', - default='source-name', - help='Type of backend device identifier provided, ' - 'typically source-name or source-id (Default=source-name)') -@utils.arg('--name', - metavar='', - help='Volume name (Default=None)') -@utils.arg('--description', - metavar='', - help='Volume description (Default=None)') -@utils.arg('--volume-type', - metavar='', - help='Volume type (Default=None)') -@utils.arg('--availability-zone', - metavar='', - help='Availability zone for volume (Default=None)') -@utils.arg('--metadata', - type=str, - nargs='*', - metavar='', - help='Metadata key=value pairs (Default=None)') -@utils.arg('--bootable', - action='store_true', - help='Specifies that the newly created volume should be' - ' marked as bootable') -@utils.service_type('volumev2') -def do_manage(cs, args): - """Manage an existing volume.""" - volume_metadata = None - if args.metadata is not None: - volume_metadata = _extract_metadata(args) - - # Build a dictionary of key/value pairs to pass to the API. - ref_dict = {args.id_type: args.identifier} - - # The recommended way to specify an existing volume is by ID or name, and - # have the Cinder driver look for 'source-name' or 'source-id' elements in - # the ref structure. To make things easier for the user, we have special - # --source-name and --source-id CLI options that add the appropriate - # element to the ref structure. - # - # Note how argparse converts hyphens to underscores. We use hyphens in the - # dictionary so that it is consistent with what the user specified on the - # CLI. - - if hasattr(args, 'source_name') and args.source_name is not None: - ref_dict['source-name'] = args.source_name - if hasattr(args, 'source_id') and args.source_id is not None: - ref_dict['source-id'] = args.source_id - - volume = cs.volumes.manage(host=args.host, - ref=ref_dict, - name=args.name, - description=args.description, - volume_type=args.volume_type, - availability_zone=args.availability_zone, - metadata=volume_metadata, - bootable=args.bootable) - - info = {} - volume = cs.volumes.get(volume.id) - info.update(volume._info) - info.pop('links', None) - utils.print_dict(info) - - -@utils.arg('volume', metavar='', - help='Name or ID of the volume to unmanage.') -@utils.service_type('volumev2') -def do_unmanage(cs, args): - """Stop managing a volume.""" - volume = utils.find_volume(cs, args.volume) - cs.volumes.unmanage(volume.id) - - -@utils.arg('volume', metavar='', - help='Name or ID of the volume to promote. ' - 'The volume should have the replica volume created with ' - 'source-replica argument.') -@utils.service_type('volumev2') -def do_replication_promote(cs, args): - """Promote a secondary volume to primary for a relationship.""" - volume = utils.find_volume(cs, args.volume) - cs.volumes.promote(volume.id) - - -@utils.arg('volume', metavar='', - help='Name or ID of the volume to reenable replication. ' - 'The replication-status of the volume should be inactive.') -@utils.service_type('volumev2') -def do_replication_reenable(cs, args): - """Sync the secondary volume with primary for a relationship.""" - volume = utils.find_volume(cs, args.volume) - cs.volumes.reenable(volume.id) - - -@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='', - 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('volumetypes', - metavar='', - help='Volume types.') -@utils.arg('--name', - metavar='', - help='Name of a consistency group.') -@utils.arg('--description', - metavar='', - default=None, - help='Description of a consistency group. Default=None.') -@utils.arg('--availability-zone', - metavar='', - 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.volumetypes, - args.name, - args.description, - 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('--cgsnapshot', - metavar='', - help='Name or ID of a cgsnapshot. Default=None.') -@utils.arg('--source-cg', - metavar='', - help='Name or ID of a source CG. Default=None.') -@utils.arg('--name', - metavar='', - help='Name of a consistency group. Default=None.') -@utils.arg('--description', - metavar='', - help='Description of a consistency group. Default=None.') -@utils.service_type('volumev2') -def do_consisgroup_create_from_src(cs, args): - """Creates a consistency group from a cgsnapshot or a source CG.""" - if not args.cgsnapshot and not args.source_cg: - msg = ('Cannot create consistency group because neither ' - 'cgsnapshot nor source CG is provided.') - raise exceptions.ClientException(code=1, message=msg) - if args.cgsnapshot and args.source_cg: - msg = ('Cannot create consistency group because both ' - 'cgsnapshot and source CG are provided.') - raise exceptions.ClientException(code=1, message=msg) - cgsnapshot = None - if args.cgsnapshot: - cgsnapshot = _find_cgsnapshot(cs, args.cgsnapshot) - source_cg = None - if args.source_cg: - source_cg = _find_consistencygroup(cs, args.source_cg) - info = cs.consistencygroups.create_from_src( - cgsnapshot.id if cgsnapshot else None, - source_cg.id if source_cg else None, - args.name, - args.description) - - info.pop('links', None) - utils.print_dict(info) - - -@utils.arg('consistencygroup', - metavar='', nargs='+', - help='Name or ID of one or more consistency groups ' - 'to be deleted.') -@utils.arg('--force', - action='store_true', - default=False, - 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.') -@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 the specified " - "consistency groups.") - - -@utils.arg('consistencygroup', - metavar='', - help='Name or ID of a consistency group.') -@utils.arg('--name', metavar='', - help='New name for consistency group. Default=None.') -@utils.arg('--description', metavar='', - help='New description for consistency group. Default=None.') -@utils.arg('--add-volumes', - metavar='', - help='UUID of one or more volumes ' - 'to be added to the consistency group, ' - 'separated by commas. Default=None.') -@utils.arg('--remove-volumes', - metavar='', - help='UUID of one or more volumes ' - 'to be removed from the consistency group, ' - 'separated by commas. Default=None.') -@utils.service_type('volumev2') -def do_consisgroup_update(cs, args): - """Updates a consistencygroup.""" - kwargs = {} - - if args.name is not None: - kwargs['name'] = args.name - - if args.description is not None: - kwargs['description'] = args.description - - if args.add_volumes is not None: - kwargs['add_volumes'] = args.add_volumes - - if args.remove_volumes is not None: - kwargs['remove_volumes'] = args.remove_volumes - - if not kwargs: - msg = ('At least one of the following args must be supplied: ' - 'name, description, add-volumes, remove-volumes.') - raise exceptions.ClientException(code=1, message=msg) - - _find_consistencygroup(cs, args.consistencygroup).update(**kwargs) - - -@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='', - default=None, - help='Filters results by a status. Default=None.') -@utils.arg('--consistencygroup-id', - metavar='', - 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.""" - - 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='', - 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='', - help='Name or ID of a consistency group.') -@utils.arg('--name', - metavar='', - default=None, - help='Cgsnapshot name. Default=None.') -@utils.arg('--description', - metavar='', - 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='', 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 the specified " - "cgsnapshots.") - - -@utils.arg('--detail', - action='store_true', - help='Show detailed information about pools.') -@utils.service_type('volumev2') -def do_get_pools(cs, args): - """Show pool information for backends. Admin only.""" - pools = cs.volumes.get_pools(args.detail) - infos = dict() - infos.update(pools._info) - - for info in infos['pools']: - backend = dict() - backend['name'] = info['name'] - if args.detail: - backend.update(info['capabilities']) - utils.print_dict(backend) - - -@utils.arg('host', - metavar='', - help='Cinder host to show backend volume stats and properties; ' - 'takes the form: host@backend-name') -@utils.service_type('volumev2') -def do_get_capabilities(cs, args): - """Show backend volume stats and properties. Admin only.""" - - capabilities = cs.capabilities.get(args.host) - infos = dict() - infos.update(capabilities._info) - - prop = infos.pop('properties', None) - utils.print_dict(infos, "Volume stats") - utils.print_dict(prop, "Backend properties") - - -@utils.arg('volume', - metavar='', - help='Cinder volume already exists in volume backend') -@utils.arg('identifier', - metavar='', - help='Name or other Identifier for existing snapshot') -@utils.arg('--id-type', - metavar='', - default='source-name', - help='Type of backend device identifier provided, ' - 'typically source-name or source-id (Default=source-name)') -@utils.arg('--name', - metavar='', - help='Snapshot name (Default=None)') -@utils.arg('--description', - metavar='', - help='Snapshot description (Default=None)') -@utils.arg('--metadata', - type=str, - nargs='*', - metavar='', - help='Metadata key=value pairs (Default=None)') -@utils.service_type('volumev2') -def do_snapshot_manage(cs, args): - """Manage an existing snapshot.""" - snapshot_metadata = None - if args.metadata is not None: - snapshot_metadata = _extract_metadata(args) - - # Build a dictionary of key/value pairs to pass to the API. - ref_dict = {args.id_type: args.identifier} - - if hasattr(args, 'source_name') and args.source_name is not None: - ref_dict['source-name'] = args.source_name - if hasattr(args, 'source_id') and args.source_id is not None: - ref_dict['source-id'] = args.source_id - - volume = utils.find_volume(cs, args.volume) - snapshot = cs.volume_snapshots.manage(volume_id=volume.id, - ref=ref_dict, - name=args.name, - description=args.description, - metadata=snapshot_metadata) - - info = {} - snapshot = cs.volume_snapshots.get(snapshot.id) - info.update(snapshot._info) - info.pop('links', None) - utils.print_dict(info) - - -@utils.arg('snapshot', metavar='', - help='Name or ID of the snapshot to unmanage.') -@utils.service_type('volumev2') -def do_snapshot_unmanage(cs, args): - """Stop managing a snapshot.""" - snapshot = _find_volume_snapshot(cs, args.snapshot) - cs.volume_snapshots.unmanage(snapshot.id) - - -@utils.arg('host', metavar='', help='Host name.') -@utils.service_type('volumev2') -def do_freeze_host(cs, args): - """Freeze and disable the specified cinder-volume host.""" - cs.services.freeze_host(args.host) - - -@utils.arg('host', metavar='', help='Host name.') -@utils.service_type('volumev2') -def do_thaw_host(cs, args): - """Thaw and enable the specified cinder-volume host.""" - cs.services.thaw_host(args.host) - - -@utils.arg('host', metavar='', help='Host name.') -@utils.arg('--backend_id', - metavar='', - help='ID of backend to failover to (Default=None)') -@utils.service_type('volumev2') -def do_failover_host(cs, args): - """Failover a replicating cinder-volume host.""" - cs.services.failover_host(args.host, args.backend_id) diff --git a/cinderclient/v2/volume_backups.py b/cinderclient/v2/volume_backups.py index 7039e608c..bf716d97b 100644 --- a/cinderclient/v2/volume_backups.py +++ b/cinderclient/v2/volume_backups.py @@ -14,111 +14,8 @@ # under the License. """ -Volume Backups interface (1.1 extension). +Volume Backups interface (v2 extension). """ -from cinderclient import base -from cinderclient.openstack.common.apiclient import base as common_base +from cinderclient.v3.volume_backups import * # flake8: noqa -class VolumeBackup(base.Resource): - """A volume backup is a block level backup of a volume.""" - - def __repr__(self): - return "" % self.id - - def delete(self): - """Delete this volume backup.""" - return self.manager.delete(self) - - def reset_state(self, state): - return self.manager.reset_state(self, state) - - -class VolumeBackupManager(base.ManagerWithFind): - """Manage :class:`VolumeBackup` resources.""" - resource_class = VolumeBackup - - def create(self, volume_id, container=None, - name=None, description=None, - incremental=False, force=False, - snapshot_id=None): - """Creates a volume backup. - - :param volume_id: The ID of the volume to backup. - :param container: The name of the backup service container. - :param name: The name of the backup. - :param description: The description of the backup. - :param incremental: Incremental backup. - :param force: If True, allows an in-use volume to be backed up. - :rtype: :class:`VolumeBackup` - """ - body = {'backup': {'volume_id': volume_id, - 'container': container, - 'name': name, - 'description': description, - 'incremental': incremental, - 'force': force, - 'snapshot_id': snapshot_id, }} - return self._create('/backups', body, 'backup') - - def get(self, backup_id): - """Show volume backup details. - - :param backup_id: The ID of the backup to display. - :rtype: :class:`VolumeBackup` - """ - return self._get("/backups/%s" % backup_id, "backup") - - def list(self, detailed=True, search_opts=None, marker=None, limit=None, - sort=None): - """Get a list of all volume backups. - - :rtype: list of :class:`VolumeBackup` - """ - resource_type = "backups" - url = self._build_list_url(resource_type, detailed=detailed, - search_opts=search_opts, marker=marker, - limit=limit, sort=sort) - return self._list(url, resource_type, limit=limit) - - def delete(self, backup): - """Delete a volume backup. - - :param backup: The :class:`VolumeBackup` to delete. - """ - return self._delete("/backups/%s" % base.getid(backup)) - - def reset_state(self, backup, state): - """Update the specified volume backup with the provided state.""" - return self._action('os-reset_status', backup, {'status': state}) - - def _action(self, action, backup, info=None, **kwargs): - """Perform a volume backup action.""" - body = {action: info} - self.run_hooks('modify_body_for_action', body, **kwargs) - url = '/backups/%s/action' % base.getid(backup) - resp, body = self.api.client.post(url, body=body) - return common_base.TupleWithMeta((resp, body), resp) - - def export_record(self, backup_id): - """Export volume backup metadata record. - - :param backup_id: The ID of the backup to export. - :rtype: A dictionary containing 'backup_url' and 'backup_service'. - """ - resp, body = \ - self.api.client.get("/backups/%s/export_record" % backup_id) - return common_base.DictWithMeta(body['backup-record'], resp) - - def import_record(self, backup_service, backup_url): - """Import volume backup metadata record. - - :param backup_service: Backup service to use for importing the backup - :param backup_url: Backup URL for importing the backup metadata - :rtype: A dictionary containing volume backup metadata. - """ - body = {'backup-record': {'backup_service': backup_service, - 'backup_url': backup_url}} - self.run_hooks('modify_body_for_update', body, 'backup-record') - resp, body = self.api.client.post("/backups/import_record", body=body) - return common_base.DictWithMeta(body['backup'], resp) diff --git a/cinderclient/v2/volume_backups_restore.py b/cinderclient/v2/volume_backups_restore.py index 0eafa8220..700ca6cb1 100644 --- a/cinderclient/v2/volume_backups_restore.py +++ b/cinderclient/v2/volume_backups_restore.py @@ -13,31 +13,10 @@ # License for the specific language governing permissions and limitations # under the License. -"""Volume Backups Restore interface (1.1 extension). +"""Volume Backups Restore interface (v2 extension). This is part of the Volume Backups interface. """ -from cinderclient import base +from cinderclient.v3.volume_backups_restore import * # flake8: noqa - -class VolumeBackupsRestore(base.Resource): - """A Volume Backups Restore represents a restore operation.""" - def __repr__(self): - return "" % self.volume_id - - -class VolumeBackupRestoreManager(base.Manager): - """Manage :class:`VolumeBackupsRestore` resources.""" - resource_class = VolumeBackupsRestore - - def restore(self, backup_id, volume_id=None): - """Restore a backup to a volume. - - :param backup_id: The ID of the backup to restore. - :param volume_id: The ID of the volume to restore the backup to. - :rtype: :class:`Restore` - """ - body = {'restore': {'volume_id': volume_id}} - return self._create("/backups/%s/restore" % backup_id, - body, "restore") diff --git a/cinderclient/v2/volume_encryption_types.py b/cinderclient/v2/volume_encryption_types.py index c4c7a6981..9923af698 100644 --- a/cinderclient/v2/volume_encryption_types.py +++ b/cinderclient/v2/volume_encryption_types.py @@ -13,92 +13,9 @@ # License for the specific language governing permissions and limitations # under the License. - """ Volume Encryption Type interface """ -from cinderclient import base -from cinderclient.openstack.common.apiclient import base as common_base +from cinderclient.v3.volume_encryption_types import * # flake8: noqa - -class VolumeEncryptionType(base.Resource): - """ - A Volume Encryption Type is a collection of settings used to conduct - encryption for a specific volume type. - """ - def __repr__(self): - return "" % self.name - - -class VolumeEncryptionTypeManager(base.ManagerWithFind): - """ - Manage :class: `VolumeEncryptionType` resources. - """ - resource_class = VolumeEncryptionType - - def list(self, search_opts=None): - """ - List all volume encryption types. - - :param volume_types: a list of volume types - :return: a list of :class: VolumeEncryptionType instances - """ - # Since the encryption type is a volume type extension, we cannot get - # all encryption types without going through all volume types. - volume_types = self.api.volume_types.list() - encryption_types = [] - list_of_resp = [] - for volume_type in volume_types: - encryption_type = self._get("/types/%s/encryption" - % base.getid(volume_type)) - if hasattr(encryption_type, 'volume_type_id'): - encryption_types.append(encryption_type) - - list_of_resp.extend(encryption_type.request_ids) - - return common_base.ListWithMeta(encryption_types, list_of_resp) - - def get(self, volume_type): - """ - Get the volume encryption type for the specified volume type. - - :param volume_type: the volume type to query - :return: an instance of :class: VolumeEncryptionType - """ - return self._get("/types/%s/encryption" % base.getid(volume_type)) - - def create(self, volume_type, specs): - """ - Creates encryption type for a volume type. Default: admin only. - - :param volume_type: the volume type on which to add an encryption type - :param specs: the encryption type specifications to add - :return: an instance of :class: VolumeEncryptionType - """ - body = {'encryption': specs} - return self._create("/types/%s/encryption" % base.getid(volume_type), - body, "encryption") - - def update(self, volume_type, specs): - """ - Update the encryption type information for the specified volume type. - - :param volume_type: the volume type whose encryption type information - must be updated - :param specs: the encryption type specifications to update - :return: an instance of :class: VolumeEncryptionType - """ - body = {'encryption': specs} - return self._update("/types/%s/encryption/provider" % - base.getid(volume_type), body) - - def delete(self, volume_type): - """ - Delete the encryption type information for the specified volume type. - - :param volume_type: the volume type whose encryption type information - must be deleted - """ - return self._delete("/types/%s/encryption/provider" % - base.getid(volume_type)) diff --git a/cinderclient/v2/volume_snapshots.py b/cinderclient/v2/volume_snapshots.py index 46ae2bb03..b0fd89291 100644 --- a/cinderclient/v2/volume_snapshots.py +++ b/cinderclient/v2/volume_snapshots.py @@ -13,193 +13,7 @@ # License for the specific language governing permissions and limitations # under the License. -"""Volume snapshot interface (1.1 extension).""" +"""Volume snapshot interface (v2 extension).""" -from cinderclient import base -from cinderclient.openstack.common.apiclient import base as common_base +from cinderclient.v3.volume_snapshots import * # flake8: noqa - -class Snapshot(base.Resource): - """A Snapshot is a point-in-time snapshot of an openstack volume.""" - - def __repr__(self): - return "" % self.id - - def delete(self): - """Delete this snapshot.""" - return self.manager.delete(self) - - def update(self, **kwargs): - """Update the name or description for this snapshot.""" - return self.manager.update(self, **kwargs) - - @property - def progress(self): - return self._info.get('os-extended-snapshot-attributes:progress') - - @property - def project_id(self): - return self._info.get('os-extended-snapshot-attributes:project_id') - - def reset_state(self, state): - """Update the snapshot with the provided state.""" - return self.manager.reset_state(self, state) - - def set_metadata(self, metadata): - """Set metadata of this snapshot.""" - return self.manager.set_metadata(self, metadata) - - def delete_metadata(self, keys): - """Delete metadata of this snapshot.""" - return self.manager.delete_metadata(self, keys) - - def update_all_metadata(self, metadata): - """Update_all metadata of this snapshot.""" - return self.manager.update_all_metadata(self, metadata) - - def manage(self, volume_id, ref, name=None, description=None, - metadata=None): - """Manage an existing snapshot.""" - self.manager.manage(volume_id=volume_id, ref=ref, name=name, - description=description, metadata=metadata) - - def unmanage(self, snapshot): - """Unmanage a snapshot.""" - self.manager.unmanage(snapshot) - - -class SnapshotManager(base.ManagerWithFind): - """Manage :class:`Snapshot` resources.""" - resource_class = Snapshot - - def create(self, volume_id, force=False, - name=None, description=None, metadata=None): - - """Creates a snapshot of the given volume. - - :param volume_id: The ID of the volume to snapshot. - :param force: If force is True, create a snapshot even if the volume is - attached to an instance. Default is False. - :param name: Name of the snapshot - :param description: Description of the snapshot - :param metadata: Metadata of the snapshot - :rtype: :class:`Snapshot` - """ - - if metadata is None: - snapshot_metadata = {} - else: - snapshot_metadata = metadata - - body = {'snapshot': {'volume_id': volume_id, - 'force': force, - 'name': name, - 'description': description, - 'metadata': snapshot_metadata}} - return self._create('/snapshots', body, 'snapshot') - - def get(self, snapshot_id): - """Shows snapshot details. - - :param snapshot_id: The ID of the snapshot to get. - :rtype: :class:`Snapshot` - """ - return self._get("/snapshots/%s" % snapshot_id, "snapshot") - - def list(self, detailed=True, search_opts=None, marker=None, limit=None, - sort=None): - """Get a list of all snapshots. - - :rtype: list of :class:`Snapshot` - """ - resource_type = "snapshots" - url = self._build_list_url(resource_type, detailed=detailed, - search_opts=search_opts, marker=marker, - limit=limit, sort=sort) - return self._list(url, resource_type, limit=limit) - - def delete(self, snapshot): - """Delete a snapshot. - - :param snapshot: The :class:`Snapshot` to delete. - """ - return self._delete("/snapshots/%s" % base.getid(snapshot)) - - def update(self, snapshot, **kwargs): - """Update the name or description for a snapshot. - - :param snapshot: The :class:`Snapshot` to update. - """ - if not kwargs: - return - - body = {"snapshot": kwargs} - - return self._update("/snapshots/%s" % base.getid(snapshot), body) - - def reset_state(self, snapshot, state): - """Update the specified snapshot with the provided state.""" - return self._action('os-reset_status', snapshot, {'status': state}) - - def _action(self, action, snapshot, info=None, **kwargs): - """Perform a snapshot action.""" - body = {action: info} - self.run_hooks('modify_body_for_action', body, **kwargs) - url = '/snapshots/%s/action' % base.getid(snapshot) - resp, body = self.api.client.post(url, body=body) - return common_base.TupleWithMeta((resp, body), resp) - - def update_snapshot_status(self, snapshot, update_dict): - return self._action('os-update_snapshot_status', - base.getid(snapshot), update_dict) - - def set_metadata(self, snapshot, metadata): - """Update/Set a snapshots metadata. - - :param snapshot: The :class:`Snapshot`. - :param metadata: A list of keys to be set. - """ - body = {'metadata': metadata} - return self._create("/snapshots/%s/metadata" % base.getid(snapshot), - body, "metadata") - - def delete_metadata(self, snapshot, keys): - """Delete specified keys from snapshot metadata. - - :param snapshot: The :class:`Snapshot`. - :param keys: A list of keys to be removed. - """ - response_list = [] - snapshot_id = base.getid(snapshot) - for k in keys: - resp, body = self._delete("/snapshots/%s/metadata/%s" % - (snapshot_id, k)) - response_list.append(resp) - - return common_base.ListWithMeta([], response_list) - - def update_all_metadata(self, snapshot, metadata): - """Update_all snapshot metadata. - - :param snapshot: The :class:`Snapshot`. - :param metadata: A list of keys to be updated. - """ - body = {'metadata': metadata} - return self._update("/snapshots/%s/metadata" % base.getid(snapshot), - body) - - def manage(self, volume_id, ref, name=None, description=None, - metadata=None): - """Manage an existing snapshot.""" - body = {'snapshot': {'volume_id': volume_id, - 'ref': ref, - 'name': name, - 'description': description, - 'metadata': metadata - } - } - return self._create('/os-snapshot-manage', body, 'snapshot') - - def unmanage(self, snapshot): - """Unmanage a snapshot.""" - return self._action('os-unmanage', snapshot, None) diff --git a/cinderclient/v2/volume_transfers.py b/cinderclient/v2/volume_transfers.py index a2bfe3cc0..9e1706991 100644 --- a/cinderclient/v2/volume_transfers.py +++ b/cinderclient/v2/volume_transfers.py @@ -14,88 +14,8 @@ # under the License. """ -Volume transfer interface (1.1 extension). +Volume transfer interface (v2 extension). """ -try: - from urllib import urlencode -except ImportError: - from urllib.parse import urlencode -import six -from cinderclient import base +from cinderclient.v3.volume_transfers import * # flake8: noqa - -class VolumeTransfer(base.Resource): - """Transfer a volume from one tenant to another""" - - def __repr__(self): - return "" % self.id - - def delete(self): - """Delete this volume transfer.""" - return self.manager.delete(self) - - -class VolumeTransferManager(base.ManagerWithFind): - """Manage :class:`VolumeTransfer` resources.""" - resource_class = VolumeTransfer - - def create(self, volume_id, name=None): - """Creates a volume transfer. - - :param volume_id: The ID of the volume to transfer. - :param name: The name of the transfer. - :rtype: :class:`VolumeTransfer` - """ - body = {'transfer': {'volume_id': volume_id, - 'name': name}} - return self._create('/os-volume-transfer', body, 'transfer') - - def accept(self, transfer_id, auth_key): - """Accept a volume transfer. - - :param transfer_id: The ID of the transfer to accept. - :param auth_key: The auth_key of the transfer. - :rtype: :class:`VolumeTransfer` - """ - body = {'accept': {'auth_key': auth_key}} - return self._create('/os-volume-transfer/%s/accept' % transfer_id, - body, 'transfer') - - def get(self, transfer_id): - """Show details of a volume transfer. - - :param transfer_id: The ID of the volume transfer to display. - :rtype: :class:`VolumeTransfer` - """ - return self._get("/os-volume-transfer/%s" % transfer_id, "transfer") - - def list(self, detailed=True, search_opts=None): - """Get a list of all volume transfer. - - :rtype: list of :class:`VolumeTransfer` - """ - 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("/os-volume-transfer%s%s" % (detail, query_string), - "transfers") - - def delete(self, transfer_id): - """Delete a volume transfer. - - :param transfer_id: The :class:`VolumeTransfer` to delete. - """ - return self._delete("/os-volume-transfer/%s" % base.getid(transfer_id)) diff --git a/cinderclient/v2/volume_type_access.py b/cinderclient/v2/volume_type_access.py index abdfa8658..b18368eeb 100644 --- a/cinderclient/v2/volume_type_access.py +++ b/cinderclient/v2/volume_type_access.py @@ -14,40 +14,5 @@ """Volume type access interface.""" -from cinderclient import base -from cinderclient.openstack.common.apiclient import base as common_base +from cinderclient.v3.volume_type_access import * # flake8: noqa - -class VolumeTypeAccess(base.Resource): - def __repr__(self): - return "" % self.project_id - - -class VolumeTypeAccessManager(base.ManagerWithFind): - """ - Manage :class:`VolumeTypeAccess` resources. - """ - resource_class = VolumeTypeAccess - - def list(self, volume_type): - return self._list( - '/types/%s/os-volume-type-access' % base.getid(volume_type), - 'volume_type_access') - - def add_project_access(self, volume_type, project): - """Add a project to the given volume type access list.""" - info = {'project': project} - return self._action('addProjectAccess', volume_type, info) - - def remove_project_access(self, volume_type, project): - """Remove a project from the given volume type access list.""" - info = {'project': project} - return self._action('removeProjectAccess', volume_type, info) - - def _action(self, action, volume_type, info, **kwargs): - """Perform a volume type action.""" - body = {action: info} - self.run_hooks('modify_body_for_action', body, **kwargs) - url = '/types/%s/action' % base.getid(volume_type) - resp, body = self.api.client.post(url, body=body) - return common_base.TupleWithMeta((resp, body), resp) diff --git a/cinderclient/v2/volume_types.py b/cinderclient/v2/volume_types.py index 251e75b9f..84332e76e 100644 --- a/cinderclient/v2/volume_types.py +++ b/cinderclient/v2/volume_types.py @@ -13,139 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. - """Volume Type interface.""" -from cinderclient import base +from cinderclient.v3.volume_types import * # flake8: noqa - -class VolumeType(base.Resource): - """A Volume Type is the type of volume to be created.""" - def __repr__(self): - return "" % self.name - - @property - def is_public(self): - """ - Provide a user-friendly accessor to os-volume-type-access:is_public - """ - return self._info.get("os-volume-type-access:is_public", - self._info.get("is_public", 'N/A')) - - def get_keys(self): - """Get extra specs from a volume type. - - :param vol_type: The :class:`VolumeType` to get extra specs from - """ - _resp, body = self.manager.api.client.get( - "/types/%s/extra_specs" % - base.getid(self)) - return body["extra_specs"] - - def set_keys(self, metadata): - """Set extra specs on a volume type. - - :param type : The :class:`VolumeType` to set extra spec on - :param metadata: A dict of key/value pairs to be set - """ - body = {'extra_specs': metadata} - return self.manager._create( - "/types/%s/extra_specs" % base.getid(self), - body, - "extra_specs", - return_raw=True) - - def unset_keys(self, keys): - """Unset extra specs on a volue type. - - :param type_id: The :class:`VolumeType` to unset extra spec on - :param keys: A list of keys to be unset - """ - - # NOTE(jdg): This wasn't actually doing all of the keys before - # the return in the loop resulted in ony ONE key being unset. - # since on success the return was NONE, we'll only interrupt the loop - # and return if there's an error - for k in keys: - resp = self.manager._delete( - "/types/%s/extra_specs/%s" % ( - base.getid(self), k)) - if resp is not None: - return resp - - -class VolumeTypeManager(base.ManagerWithFind): - """Manage :class:`VolumeType` resources.""" - resource_class = VolumeType - - def list(self, search_opts=None, is_public=None): - """Lists all volume types. - - :rtype: list of :class:`VolumeType`. - """ - query_string = '' - if not is_public: - query_string = '?is_public=%s' % is_public - return self._list("/types%s" % (query_string), "volume_types") - - def get(self, volume_type): - """Get a specific volume type. - - :param volume_type: The ID of the :class:`VolumeType` to get. - :rtype: :class:`VolumeType` - """ - return self._get("/types/%s" % base.getid(volume_type), "volume_type") - - def default(self): - """Get the default volume type. - - :rtype: :class:`VolumeType` - """ - return self._get("/types/default", "volume_type") - - def delete(self, volume_type): - """Deletes a specific volume_type. - - :param volume_type: The name or ID of the :class:`VolumeType` to get. - """ - return self._delete("/types/%s" % base.getid(volume_type)) - - def create(self, name, description=None, is_public=True): - """Creates a volume type. - - :param name: Descriptive name of the volume type - :param description: Description of the the volume type - :param is_public: Volume type visibility - :rtype: :class:`VolumeType` - """ - - body = { - "volume_type": { - "name": name, - "description": description, - "os-volume-type-access:is_public": is_public, - } - } - - return self._create("/types", body, "volume_type") - - def update(self, volume_type, name=None, description=None, is_public=None): - """Update the name and/or description for a volume type. - - :param volume_type: The ID of the :class:`VolumeType` to update. - :param name: Descriptive name of the volume type. - :param description: Description of the the volume type. - :rtype: :class:`VolumeType` - """ - - body = { - "volume_type": { - "name": name, - "description": description - } - } - if is_public is not None: - body["volume_type"]["is_public"] = is_public - - return self._update("/types/%s" % base.getid(volume_type), - body, response_key="volume_type") diff --git a/cinderclient/v2/volumes.py b/cinderclient/v2/volumes.py index bab4796c8..093b911fe 100644 --- a/cinderclient/v2/volumes.py +++ b/cinderclient/v2/volumes.py @@ -15,590 +15,5 @@ """Volume interface (v2 extension).""" -from cinderclient import base -from cinderclient.openstack.common.apiclient import base as common_base +from cinderclient.v3.volumes import * # flake8: noqa - -class Volume(base.Resource): - """A volume is an extra block level storage to the OpenStack instances.""" - def __repr__(self): - return "" % self.id - - def delete(self, cascade=False): - """Delete this volume.""" - return self.manager.delete(self, cascade=cascade) - - def update(self, **kwargs): - """Update the name or description for this volume.""" - return self.manager.update(self, **kwargs) - - def attach(self, instance_uuid, mountpoint, mode='rw', host_name=None): - """Set attachment metadata. - - :param instance_uuid: uuid of the attaching instance. - :param mountpoint: mountpoint on the attaching instance or host. - :param mode: the access mode. - :param host_name: name of the attaching host. - """ - return self.manager.attach(self, instance_uuid, mountpoint, mode, - host_name) - - def detach(self): - """Clear attachment metadata.""" - return self.manager.detach(self) - - def reserve(self, volume): - """Reserve this volume.""" - return self.manager.reserve(self) - - def unreserve(self, volume): - """Unreserve this volume.""" - return self.manager.unreserve(self) - - def begin_detaching(self, volume): - """Begin detaching volume.""" - return self.manager.begin_detaching(self) - - def roll_detaching(self, volume): - """Roll detaching volume.""" - return self.manager.roll_detaching(self) - - def initialize_connection(self, volume, connector): - """Initialize a volume connection. - - :param connector: connector dict from nova. - """ - return self.manager.initialize_connection(self, connector) - - def terminate_connection(self, volume, connector): - """Terminate a volume connection. - - :param connector: connector dict from nova. - """ - return self.manager.terminate_connection(self, connector) - - def set_metadata(self, volume, metadata): - """Set or Append metadata to a volume. - - :param volume : The :class: `Volume` to set metadata on - :param metadata: A dict of key/value pairs to set - """ - return self.manager.set_metadata(self, metadata) - - def set_image_metadata(self, volume, metadata): - """Set a volume's image metadata. - - :param volume : The :class: `Volume` to set metadata on - :param metadata: A dict of key/value pairs to set - """ - return self.manager.set_image_metadata(self, volume, metadata) - - def delete_image_metadata(self, volume, keys): - """Delete specified keys from volume's image metadata. - - :param volume: The :class:`Volume`. - :param keys: A list of keys to be removed. - """ - return self.manager.delete_image_metadata(self, volume, keys) - - def show_image_metadata(self, volume): - """Show a volume's image metadata. - - :param volume : The :class: `Volume` where the image metadata - associated. - """ - return self.manager.show_image_metadata(self) - - def upload_to_image(self, force, image_name, container_format, - disk_format): - """Upload a volume to image service as an image.""" - return self.manager.upload_to_image(self, force, image_name, - container_format, disk_format) - - def force_delete(self): - """Delete the specified volume ignoring its current state. - - :param volume: The UUID of the volume to force-delete. - """ - return self.manager.force_delete(self) - - def reset_state(self, state, attach_status=None, migration_status=None): - """Update the volume with the provided state. - - :param state: The state of the volume to set. - :param attach_status: The attach_status of the volume to be set, - or None to keep the current status. - :param migration_status: The migration_status of the volume to be set, - or None to keep the current status. - """ - return self.manager.reset_state(self, state, attach_status, - migration_status) - - def extend(self, volume, new_size): - """Extend the size of the specified volume. - - :param volume: The UUID of the volume to extend - :param new_size: The desired size to extend volume to. - """ - return self.manager.extend(self, new_size) - - def migrate_volume(self, host, force_host_copy, lock_volume): - """Migrate the volume to a new host.""" - return self.manager.migrate_volume(self, host, force_host_copy, - lock_volume) - - def retype(self, volume_type, policy): - """Change a volume's type.""" - return self.manager.retype(self, volume_type, policy) - - def update_all_metadata(self, metadata): - """Update all metadata of this volume.""" - return self.manager.update_all_metadata(self, metadata) - - def update_readonly_flag(self, volume, read_only): - """Update the read-only access mode flag of the specified volume. - - :param volume: The UUID of the volume to update. - :param read_only: The value to indicate whether to update volume to - read-only access mode. - """ - return self.manager.update_readonly_flag(self, read_only) - - def manage(self, host, ref, name=None, description=None, - volume_type=None, availability_zone=None, metadata=None, - bootable=False): - """Manage an existing volume.""" - return self.manager.manage(host=host, ref=ref, name=name, - description=description, - volume_type=volume_type, - availability_zone=availability_zone, - metadata=metadata, bootable=bootable) - - def unmanage(self, volume): - """Unmanage a volume.""" - return self.manager.unmanage(volume) - - def promote(self, volume): - """Promote secondary to be primary in relationship.""" - return self.manager.promote(volume) - - def reenable(self, volume): - """Sync the secondary volume with primary for a relationship.""" - return self.manager.reenable(volume) - - def get_pools(self, detail): - """Show pool information for backends.""" - return self.manager.get_pools(detail) - - -class VolumeManager(base.ManagerWithFind): - """Manage :class:`Volume` resources.""" - resource_class = Volume - - 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, - source_replica=None, multiattach=False): - """Create 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 - :param volume_type: Type 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 - :param metadata: Optional metadata to set on volume creation - :param imageRef: reference to an image stored in glance - :param source_volid: ID of source volume to clone from - :param source_replica: ID of source volume to clone replica - :param scheduler_hints: (optional extension) arbitrary key-value pairs - specified by the client to help boot an instance - :param multiattach: Allow the volume to be attached to more than - one instance - :rtype: :class:`Volume` - """ - if metadata is None: - volume_metadata = {} - else: - volume_metadata = metadata - - body = {'volume': {'size': size, - 'consistencygroup_id': consistencygroup_id, - 'snapshot_id': snapshot_id, - 'name': name, - 'description': description, - 'volume_type': volume_type, - 'user_id': user_id, - 'project_id': project_id, - 'availability_zone': availability_zone, - 'status': "creating", - 'attach_status': "detached", - 'metadata': volume_metadata, - 'imageRef': imageRef, - 'source_volid': source_volid, - 'source_replica': source_replica, - 'multiattach': multiattach, - }} - - if scheduler_hints: - body['OS-SCH-HNT:scheduler_hints'] = scheduler_hints - - return self._create('/volumes', body, 'volume') - - def get(self, volume_id): - """Get a volume. - - :param volume_id: The ID of the volume to get. - :rtype: :class:`Volume` - """ - return self._get("/volumes/%s" % volume_id, "volume") - - def list(self, detailed=True, search_opts=None, marker=None, limit=None, - sort_key=None, sort_dir=None, sort=None): - """Lists all volumes. - - :param detailed: Whether to return detailed volume info. - :param search_opts: Search options to filter out volumes. - :param marker: Begin returning volumes that appear later in the volume - list than that represented by this volume id. - :param limit: Maximum number of volumes to return. - :param sort_key: Key to be sorted; deprecated in kilo - :param sort_dir: Sort direction, should be 'desc' or 'asc'; deprecated - in kilo - :param sort: Sort information - :rtype: list of :class:`Volume` - """ - - resource_type = "volumes" - url = self._build_list_url(resource_type, detailed=detailed, - search_opts=search_opts, marker=marker, - limit=limit, sort_key=sort_key, - sort_dir=sort_dir, sort=sort) - return self._list(url, resource_type, limit=limit) - - def delete(self, volume, cascade=False): - """Delete a volume. - - :param volume: The :class:`Volume` to delete. - :param cascade: Also delete dependent snapshots. - """ - - loc = "/volumes/%s" % base.getid(volume) - - if cascade: - loc += '?cascade=True' - - return self._delete(loc) - - def update(self, volume, **kwargs): - """Update the name or description for a volume. - - :param volume: The :class:`Volume` to update. - """ - if not kwargs: - return - - body = {"volume": kwargs} - - return self._update("/volumes/%s" % base.getid(volume), body) - - def _action(self, action, volume, info=None, **kwargs): - """Perform a volume "action." - """ - body = {action: info} - self.run_hooks('modify_body_for_action', body, **kwargs) - url = '/volumes/%s/action' % base.getid(volume) - resp, body = self.api.client.post(url, body=body) - return common_base.TupleWithMeta((resp, body), resp) - - def attach(self, volume, instance_uuid, mountpoint, mode='rw', - host_name=None): - """Set attachment metadata. - - :param volume: The :class:`Volume` (or its ID) - you would like to attach. - :param instance_uuid: uuid of the attaching instance. - :param mountpoint: mountpoint on the attaching instance or host. - :param mode: the access mode. - :param host_name: name of the attaching host. - """ - body = {'mountpoint': mountpoint, 'mode': mode} - if instance_uuid is not None: - body.update({'instance_uuid': instance_uuid}) - if host_name is not None: - body.update({'host_name': host_name}) - return self._action('os-attach', volume, body) - - def detach(self, volume, attachment_uuid=None): - """Clear attachment metadata. - - :param volume: The :class:`Volume` (or its ID) - you would like to detach. - :param attachment_uuid: The uuid of the volume attachment. - """ - return self._action('os-detach', volume, - {'attachment_id': attachment_uuid}) - - def reserve(self, volume): - """Reserve this volume. - - :param volume: The :class:`Volume` (or its ID) - you would like to reserve. - """ - return self._action('os-reserve', volume) - - def unreserve(self, volume): - """Unreserve this volume. - - :param volume: The :class:`Volume` (or its ID) - you would like to unreserve. - """ - return self._action('os-unreserve', volume) - - def begin_detaching(self, volume): - """Begin detaching this volume. - - :param volume: The :class:`Volume` (or its ID) - you would like to detach. - """ - return self._action('os-begin_detaching', volume) - - def roll_detaching(self, volume): - """Roll detaching this volume. - - :param volume: The :class:`Volume` (or its ID) - you would like to roll detaching. - """ - return self._action('os-roll_detaching', volume) - - def initialize_connection(self, volume, connector): - """Initialize a volume connection. - - :param volume: The :class:`Volume` (or its ID). - :param connector: connector dict from nova. - """ - resp, body = self._action('os-initialize_connection', volume, - {'connector': connector}) - return common_base.DictWithMeta(body['connection_info'], resp) - - def terminate_connection(self, volume, connector): - """Terminate a volume connection. - - :param volume: The :class:`Volume` (or its ID). - :param connector: connector dict from nova. - """ - return self._action('os-terminate_connection', volume, - {'connector': connector}) - - def set_metadata(self, volume, metadata): - """Update/Set a volumes metadata. - - :param volume: The :class:`Volume`. - :param metadata: A list of keys to be set. - """ - body = {'metadata': metadata} - return self._create("/volumes/%s/metadata" % base.getid(volume), - body, "metadata") - - def delete_metadata(self, volume, keys): - """Delete specified keys from volumes metadata. - - :param volume: The :class:`Volume`. - :param keys: A list of keys to be removed. - """ - response_list = [] - for k in keys: - resp, body = self._delete("/volumes/%s/metadata/%s" % - (base.getid(volume), k)) - response_list.append(resp) - - return common_base.ListWithMeta([], response_list) - - def set_image_metadata(self, volume, metadata): - """Set a volume's image metadata. - - :param volume: The :class:`Volume`. - :param metadata: keys and the values to be set with. - :type metadata: dict - """ - return self._action("os-set_image_metadata", volume, - {'metadata': metadata}) - - def delete_image_metadata(self, volume, keys): - """Delete specified keys from volume's image metadata. - - :param volume: The :class:`Volume`. - :param keys: A list of keys to be removed. - """ - response_list = [] - for key in keys: - resp, body = self._action("os-unset_image_metadata", volume, - {'key': key}) - response_list.append(resp) - - return common_base.ListWithMeta([], response_list) - - def show_image_metadata(self, volume): - """Show a volume's image metadata. - - :param volume : The :class: `Volume` where the image metadata - associated. - """ - return self._action("os-show_image_metadata", volume) - - def upload_to_image(self, volume, force, image_name, container_format, - disk_format): - """Upload volume to image service as image. - - :param volume: The :class:`Volume` to upload. - """ - return self._action('os-volume_upload_image', - volume, - {'force': force, - 'image_name': image_name, - 'container_format': container_format, - 'disk_format': disk_format}) - - def force_delete(self, volume): - """Delete the specified volume ignoring its current state. - - :param volume: The :class:`Volume` to force-delete. - """ - return self._action('os-force_delete', base.getid(volume)) - - def reset_state(self, volume, state, attach_status=None, - migration_status=None): - """Update the provided volume with the provided state. - - :param volume: The :class:`Volume` to set the state. - :param state: The state of the volume to be set. - :param attach_status: The attach_status of the volume to be set, - or None to keep the current status. - :param migration_status: The migration_status of the volume to be set, - or None to keep the current status. - """ - body = {'status': state} - if attach_status: - body.update({'attach_status': attach_status}) - if migration_status: - body.update({'migration_status': migration_status}) - return self._action('os-reset_status', volume, body) - - def extend(self, volume, new_size): - """Extend the size of the specified volume. - - :param volume: The UUID of the volume to extend. - :param new_size: The requested size to extend volume to. - """ - return self._action('os-extend', - base.getid(volume), - {'new_size': new_size}) - - def get_encryption_metadata(self, volume_id): - """ - Retrieve the encryption metadata from the desired volume. - - :param volume_id: the id of the volume to query - :return: a dictionary of volume encryption metadata - """ - metadata = self._get("/volumes/%s/encryption" % volume_id) - return common_base.DictWithMeta(metadata._info, metadata.request_ids) - - def migrate_volume(self, volume, host, force_host_copy, lock_volume): - """Migrate volume to new host. - - :param volume: The :class:`Volume` to migrate - :param host: The destination host - :param force_host_copy: Skip driver optimizations - :param lock_volume: Lock the volume and guarantee the migration - to finish - """ - return self._action('os-migrate_volume', - volume, - {'host': host, 'force_host_copy': force_host_copy, - 'lock_volume': lock_volume}) - - def migrate_volume_completion(self, old_volume, new_volume, error): - """Complete the migration from the old volume to the temp new one. - - :param old_volume: The original :class:`Volume` in the migration - :param new_volume: The new temporary :class:`Volume` in the migration - :param error: Inform of an error to cause migration cleanup - """ - new_volume_id = base.getid(new_volume) - resp, body = self._action('os-migrate_volume_completion', old_volume, - {'new_volume': new_volume_id, - 'error': error}) - return common_base.DictWithMeta(body, resp) - - def update_all_metadata(self, volume, metadata): - """Update all metadata of a volume. - - :param volume: The :class:`Volume`. - :param metadata: A list of keys to be updated. - """ - body = {'metadata': metadata} - return self._update("/volumes/%s/metadata" % base.getid(volume), - body) - - def update_readonly_flag(self, volume, flag): - return self._action('os-update_readonly_flag', - base.getid(volume), - {'readonly': flag}) - - def retype(self, volume, volume_type, policy): - """Change a volume's type. - - :param volume: The :class:`Volume` to retype - :param volume_type: New volume type - :param policy: Policy for migration during the retype - """ - return self._action('os-retype', - volume, - {'new_type': volume_type, - 'migration_policy': policy}) - - def set_bootable(self, volume, flag): - return self._action('os-set_bootable', - base.getid(volume), - {'bootable': flag}) - - def manage(self, host, ref, name=None, description=None, - volume_type=None, availability_zone=None, metadata=None, - bootable=False): - """Manage an existing volume.""" - body = {'volume': {'host': host, - 'ref': ref, - 'name': name, - 'description': description, - 'volume_type': volume_type, - 'availability_zone': availability_zone, - 'metadata': metadata, - 'bootable': bootable - }} - return self._create('/os-volume-manage', body, 'volume') - - def unmanage(self, volume): - """Unmanage a volume.""" - return self._action('os-unmanage', volume, None) - - def promote(self, volume): - """Promote secondary to be primary in relationship.""" - return self._action('os-promote-replica', volume, None) - - def reenable(self, volume): - """Sync the secondary volume with primary for a relationship.""" - return self._action('os-reenable-replica', volume, None) - - def get_pools(self, detail): - """Show pool information for backends.""" - query_string = "" - if detail: - query_string = "?detail=True" - - return self._get('/scheduler-stats/get_pools%s' % query_string, None) diff --git a/cinderclient/v3/__init__.py b/cinderclient/v3/__init__.py new file mode 100644 index 000000000..515d90c76 --- /dev/null +++ b/cinderclient/v3/__init__.py @@ -0,0 +1,17 @@ +# Copyright (c) 2013 OpenStack Foundation +# +# 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.v3.client import Client # noqa diff --git a/cinderclient/v3/availability_zones.py b/cinderclient/v3/availability_zones.py new file mode 100644 index 000000000..db6b8da26 --- /dev/null +++ b/cinderclient/v3/availability_zones.py @@ -0,0 +1,42 @@ +# Copyright 2011-2013 OpenStack Foundation +# Copyright 2013 IBM Corp. +# 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. + +"""Availability Zone interface (v3 extension)""" + +from cinderclient import base + + +class AvailabilityZone(base.Resource): + NAME_ATTR = 'display_name' + + def __repr__(self): + return "" % self.zoneName + + +class AvailabilityZoneManager(base.ManagerWithFind): + """Manage :class:`AvailabilityZone` resources.""" + resource_class = AvailabilityZone + + def list(self, detailed=False): + """Lists all availability zones. + + :rtype: list of :class:`AvailabilityZone` + """ + if detailed is True: + return self._list("/os-availability-zone/detail", + "availabilityZoneInfo") + else: + return self._list("/os-availability-zone", "availabilityZoneInfo") diff --git a/cinderclient/v3/capabilities.py b/cinderclient/v3/capabilities.py new file mode 100644 index 000000000..d0f8ab218 --- /dev/null +++ b/cinderclient/v3/capabilities.py @@ -0,0 +1,39 @@ +# Copyright (c) 2015 Hitachi Data Systems, Inc. +# 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. + +"""Capabilities interface (v3 extension)""" + + +from cinderclient import base + + +class Capabilities(base.Resource): + NAME_ATTR = 'name' + + def __repr__(self): + return "" % self.name + + +class CapabilitiesManager(base.Manager): + """Manage :class:`Capabilities` resources.""" + resource_class = Capabilities + + def get(self, host): + """Show backend volume stats and properties. + + :param host: Specified backend to obtain volume stats and properties. + :rtype: :class:`Capabilities` + """ + return self._get('/capabilities/%s' % host, None) diff --git a/cinderclient/v3/cgsnapshots.py b/cinderclient/v3/cgsnapshots.py new file mode 100644 index 000000000..951cedfd1 --- /dev/null +++ b/cinderclient/v3/cgsnapshots.py @@ -0,0 +1,126 @@ +# 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 (v3 extension).""" + +import six +try: + from urllib import urlencode +except ImportError: + from urllib.parse import urlencode + +from cinderclient import base +from cinderclient.openstack.common.apiclient import base as common_base + + +class Cgsnapshot(base.Resource): + """A cgsnapshot is snapshot of a consistency group.""" + def __repr__(self): + return "" % self.id + + def delete(self): + """Delete this cgsnapshot.""" + return self.manager.delete(self) + + def update(self, **kwargs): + """Update the name or description for this cgsnapshot.""" + return 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. + """ + return 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} + + return 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) + resp, body = self.api.client.post(url, body=body) + return common_base.TupleWithMeta((resp, body), resp) diff --git a/cinderclient/v3/client.py b/cinderclient/v3/client.py new file mode 100644 index 000000000..58f6ee5a9 --- /dev/null +++ b/cinderclient/v3/client.py @@ -0,0 +1,131 @@ +# Copyright (c) 2013 OpenStack Foundation +# 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 import client +from cinderclient.v3 import availability_zones +from cinderclient.v3 import cgsnapshots +from cinderclient.v3 import consistencygroups +from cinderclient.v3 import capabilities +from cinderclient.v3 import limits +from cinderclient.v3 import pools +from cinderclient.v3 import qos_specs +from cinderclient.v3 import quota_classes +from cinderclient.v3 import quotas +from cinderclient.v3 import services +from cinderclient.v3 import volumes +from cinderclient.v3 import volume_snapshots +from cinderclient.v3 import volume_types +from cinderclient.v3 import volume_type_access +from cinderclient.v3 import volume_encryption_types +from cinderclient.v3 import volume_backups +from cinderclient.v3 import volume_backups_restore +from cinderclient.v3 import volume_transfers + + +class Client(object): + """Top-level object to access the OpenStack Volume API. + + Create an instance with your creds:: + + >>> client = Client(USERNAME, PASSWORD, PROJECT_ID, AUTH_URL) + + Then call methods on its managers:: + + >>> client.volumes.list() + ... + """ + + version = '3' + + def __init__(self, username=None, api_key=None, project_id=None, + auth_url='', insecure=False, timeout=None, tenant_id=None, + proxy_tenant_id=None, proxy_token=None, region_name=None, + endpoint_type='publicURL', extensions=None, + service_type='volumev3', service_name=None, + volume_service_name=None, bypass_url=None, retries=None, + http_log_debug=False, cacert=None, auth_system='keystone', + auth_plugin=None, session=None, **kwargs): + # FIXME(comstud): Rename the api_key argument above when we + # know it's not being used as keyword argument + password = api_key + self.limits = limits.LimitsManager(self) + + # extensions + self.volumes = volumes.VolumeManager(self) + self.volume_snapshots = volume_snapshots.SnapshotManager(self) + self.volume_types = volume_types.VolumeTypeManager(self) + self.volume_type_access = \ + volume_type_access.VolumeTypeAccessManager(self) + self.volume_encryption_types = \ + volume_encryption_types.VolumeEncryptionTypeManager(self) + self.qos_specs = qos_specs.QoSSpecsManager(self) + self.quota_classes = quota_classes.QuotaClassSetManager(self) + self.quotas = quotas.QuotaSetManager(self) + self.backups = volume_backups.VolumeBackupManager(self) + 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) + self.pools = pools.PoolManager(self) + self.capabilities = capabilities.CapabilitiesManager(self) + + # Add in any extensions... + if extensions: + for extension in extensions: + if extension.manager_class: + setattr(self, extension.name, + extension.manager_class(self)) + + self.client = client._construct_http_client( + username=username, + password=password, + project_id=project_id, + auth_url=auth_url, + insecure=insecure, + timeout=timeout, + tenant_id=tenant_id, + proxy_tenant_id=tenant_id, + proxy_token=proxy_token, + region_name=region_name, + endpoint_type=endpoint_type, + service_type=service_type, + service_name=service_name, + volume_service_name=volume_service_name, + bypass_url=bypass_url, + retries=retries, + http_log_debug=http_log_debug, + cacert=cacert, + auth_system=auth_system, + auth_plugin=auth_plugin, + session=session, + **kwargs) + + def authenticate(self): + """Authenticate against the server. + + Normally this is called automatically when you first access the API, + but you can call this method to force authentication right now. + + Returns on success; raises :exc:`exceptions.Unauthorized` if the + credentials are wrong. + """ + self.client.authenticate() + + def get_volume_api_version_from_endpoint(self): + return self.client.get_volume_api_version_from_endpoint() diff --git a/cinderclient/v3/consistencygroups.py b/cinderclient/v3/consistencygroups.py new file mode 100644 index 000000000..2d5421e75 --- /dev/null +++ b/cinderclient/v3/consistencygroups.py @@ -0,0 +1,162 @@ +# 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 (v3 extension).""" + +import six +try: + from urllib import urlencode +except ImportError: + from urllib.parse import urlencode + +from cinderclient import base +from cinderclient.openstack.common.apiclient import base as common_base + + +class Consistencygroup(base.Resource): + """A Consistencygroup of volumes.""" + def __repr__(self): + return "" % self.id + + def delete(self, force='False'): + """Delete this consistencygroup.""" + return self.manager.delete(self, force) + + def update(self, **kwargs): + """Update the name or description for this consistencygroup.""" + return self.manager.update(self, **kwargs) + + +class ConsistencygroupManager(base.ManagerWithFind): + """Manage :class:`Consistencygroup` resources.""" + resource_class = Consistencygroup + + def create(self, volume_types, name=None, + description=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 create_from_src(self, cgsnapshot_id, source_cgid, name=None, + description=None, user_id=None, + project_id=None): + """Creates a consistencygroup from a cgsnapshot or a source CG. + + :param cgsnapshot_id: UUID of a CGSnapshot + :param source_cgid: UUID of a source CG + :param name: Name of the ConsistencyGroup + :param description: Description of the ConsistencyGroup + :param user_id: User id derived from context + :param project_id: Project id derived from context + :rtype: A dictionary containing Consistencygroup metadata + """ + body = {'consistencygroup-from-src': {'name': name, + 'description': description, + 'cgsnapshot_id': cgsnapshot_id, + 'source_cgid': source_cgid, + 'user_id': user_id, + 'project_id': project_id, + 'status': "creating", + }} + + self.run_hooks('modify_body_for_update', body, + 'consistencygroup-from-src') + resp, body = self.api.client.post( + "/consistencygroups/create_from_src", body=body) + return common_base.DictWithMeta(body['consistencygroup'], resp) + + 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) + resp, body = self.api.client.post(url, body=body) + return common_base.TupleWithMeta((resp, body), resp) + + 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} + + return 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) + resp, body = self.api.client.post(url, body=body) + return common_base.TupleWithMeta((resp, body), resp) diff --git a/cinderclient/v3/contrib/__init__.py b/cinderclient/v3/contrib/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/cinderclient/v3/contrib/list_extensions.py b/cinderclient/v3/contrib/list_extensions.py new file mode 100644 index 000000000..d9e17a8bc --- /dev/null +++ b/cinderclient/v3/contrib/list_extensions.py @@ -0,0 +1,47 @@ +# Copyright (c) 2013 OpenStack Foundation +# 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 import base +from cinderclient import utils + + +class ListExtResource(base.Resource): + @property + def summary(self): + descr = self.description.strip() + if not descr: + return '??' + lines = descr.split("\n") + if len(lines) == 1: + return lines[0] + else: + return lines[0] + "..." + + +class ListExtManager(base.Manager): + resource_class = ListExtResource + + def show_all(self): + return self._list("/extensions", 'extensions') + + +@utils.service_type('volumev3') +def do_list_extensions(client, _args): + """ + Lists all available os-api extensions. + """ + extensions = client.list_extensions.show_all() + fields = ["Name", "Summary", "Alias", "Updated"] + utils.print_list(extensions, fields) diff --git a/cinderclient/v3/limits.py b/cinderclient/v3/limits.py new file mode 100644 index 000000000..512a58dec --- /dev/null +++ b/cinderclient/v3/limits.py @@ -0,0 +1,91 @@ +# Copyright 2013 OpenStack Foundation +# +# 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 import base + + +class Limits(base.Resource): + """A collection of RateLimit and AbsoluteLimit objects.""" + + def __repr__(self): + return "" + + @property + def absolute(self): + for (name, value) in list(self._info['absolute'].items()): + yield AbsoluteLimit(name, value) + + @property + def rate(self): + for group in self._info['rate']: + uri = group['uri'] + regex = group['regex'] + for rate in group['limit']: + yield RateLimit(rate['verb'], uri, regex, rate['value'], + rate['remaining'], rate['unit'], + rate['next-available']) + + +class RateLimit(object): + """Data model that represents a flattened view of a single rate limit.""" + + def __init__(self, verb, uri, regex, value, remain, + unit, next_available): + self.verb = verb + self.uri = uri + self.regex = regex + self.value = value + self.remain = remain + self.unit = unit + self.next_available = next_available + + def __eq__(self, other): + return self.uri == other.uri \ + and self.regex == other.regex \ + and self.value == other.value \ + and self.verb == other.verb \ + and self.remain == other.remain \ + and self.unit == other.unit \ + and self.next_available == other.next_available + + def __repr__(self): + return "" % (self.verb, self.uri) + + +class AbsoluteLimit(object): + """Data model that represents a single absolute limit.""" + + def __init__(self, name, value): + self.name = name + self.value = value + + def __eq__(self, other): + return self.value == other.value and self.name == other.name + + def __repr__(self): + return "" % (self.name) + + +class LimitsManager(base.Manager): + """Manager object used to interact with limits resource.""" + + resource_class = Limits + + def get(self): + """Get a specific extension. + + :rtype: :class:`Limits` + """ + return self._get("/limits", "limits") diff --git a/cinderclient/v3/pools.py b/cinderclient/v3/pools.py new file mode 100644 index 000000000..2f7260588 --- /dev/null +++ b/cinderclient/v3/pools.py @@ -0,0 +1,62 @@ +# Copyright (C) 2015 Hewlett-Packard Development Company, L.P. +# 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. + +"""Pools interface (v3 extension)""" + +import six + +from cinderclient import base + + +class Pool(base.Resource): + NAME_ATTR = 'name' + + def __repr__(self): + return "" % self.name + + +class PoolManager(base.Manager): + """Manage :class:`Pool` resources.""" + resource_class = Pool + + def list(self, detailed=False): + """Lists all + + :rtype: list of :class:`Pool` + """ + if detailed is True: + pools = self._list("/scheduler-stats/get_pools?detail=True", + "pools") + # Other than the name, all of the pool data is buried below in + # a 'capabilities' dictionary. In order to be consistent with the + # get-pools command line, these elements are moved up a level to + # be attributes of the pool itself. + for pool in pools: + if hasattr(pool, 'capabilities'): + for k, v in six.iteritems(pool.capabilities): + setattr(pool, k, v) + + # Remove the capabilities dictionary since all of its + # elements have been copied up to the containing pool + del pool.capabilities + return pools + else: + pools = self._list("/scheduler-stats/get_pools", "pools") + + # avoid cluttering the basic pool list with capabilities dict + for pool in pools: + if hasattr(pool, 'capabilities'): + del pool.capabilities + return pools diff --git a/cinderclient/v3/qos_specs.py b/cinderclient/v3/qos_specs.py new file mode 100644 index 000000000..84b8e0ac5 --- /dev/null +++ b/cinderclient/v3/qos_specs.py @@ -0,0 +1,156 @@ +# Copyright (c) 2013 eBay Inc. +# Copyright (c) OpenStack LLC. +# +# 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. + + +""" +QoS Specs interface. +""" + +from cinderclient import base +from cinderclient.openstack.common.apiclient import base as common_base + + +class QoSSpecs(base.Resource): + """QoS specs entity represents quality-of-service parameters/requirements. + + A QoS specs is a set of parameters or requirements for quality-of-service + purpose, which can be associated with volume types (for now). In future, + QoS specs may be extended to be associated other entities, such as single + volume. + """ + def __repr__(self): + return "" % self.name + + def delete(self): + return self.manager.delete(self) + + +class QoSSpecsManager(base.ManagerWithFind): + """ + Manage :class:`QoSSpecs` resources. + """ + resource_class = QoSSpecs + + def list(self, search_opts=None): + """Get a list of all qos specs. + + :rtype: list of :class:`QoSSpecs`. + """ + return self._list("/qos-specs", "qos_specs") + + def get(self, qos_specs): + """Get a specific qos specs. + + :param qos_specs: The ID of the :class:`QoSSpecs` to get. + :rtype: :class:`QoSSpecs` + """ + return self._get("/qos-specs/%s" % base.getid(qos_specs), "qos_specs") + + def delete(self, qos_specs, force=False): + """Delete a specific qos specs. + + :param qos_specs: The ID of the :class:`QoSSpecs` to be removed. + :param force: Flag that indicates whether to delete target qos specs + if it was in-use. + """ + return self._delete("/qos-specs/%s?force=%s" % + (base.getid(qos_specs), force)) + + def create(self, name, specs): + """Create a qos specs. + + :param name: Descriptive name of the qos specs, must be unique + :param specs: A dict of key/value pairs to be set + :rtype: :class:`QoSSpecs` + """ + + body = { + "qos_specs": { + "name": name, + } + } + + body["qos_specs"].update(specs) + return self._create("/qos-specs", body, "qos_specs") + + def set_keys(self, qos_specs, specs): + """Add/Update keys in qos specs. + + :param qos_specs: The ID of qos specs + :param specs: A dict of key/value pairs to be set + :rtype: :class:`QoSSpecs` + """ + + body = { + "qos_specs": {} + } + + body["qos_specs"].update(specs) + return self._update("/qos-specs/%s" % qos_specs, body) + + def unset_keys(self, qos_specs, specs): + """Remove keys from a qos specs. + + :param qos_specs: The ID of qos specs + :param specs: A list of key to be unset + :rtype: :class:`QoSSpecs` + """ + + body = {'keys': specs} + + return self._update("/qos-specs/%s/delete_keys" % qos_specs, + body) + + def get_associations(self, qos_specs): + """Get associated entities of a qos specs. + + :param qos_specs: The id of the :class: `QoSSpecs` + :return: a list of entities that associated with specific qos specs. + """ + return self._list("/qos-specs/%s/associations" % base.getid(qos_specs), + "qos_associations") + + def associate(self, qos_specs, vol_type_id): + """Associate a volume type with specific qos specs. + + :param qos_specs: The qos specs to be associated with + :param vol_type_id: The volume type id to be associated with + """ + resp, body = self.api.client.get( + "/qos-specs/%s/associate?vol_type_id=%s" % + (base.getid(qos_specs), vol_type_id)) + return common_base.TupleWithMeta((resp, body), resp) + + def disassociate(self, qos_specs, vol_type_id): + """Disassociate qos specs from volume type. + + :param qos_specs: The qos specs to be associated with + :param vol_type_id: The volume type id to be associated with + """ + resp, body = self.api.client.get( + "/qos-specs/%s/disassociate?vol_type_id=%s" % + (base.getid(qos_specs), vol_type_id)) + return common_base.TupleWithMeta((resp, body), resp) + + def disassociate_all(self, qos_specs): + """Disassociate all entities from specific qos specs. + + :param qos_specs: The qos specs to be associated with + """ + resp, body = self.api.client.get( + "/qos-specs/%s/disassociate_all" % + base.getid(qos_specs)) + return common_base.TupleWithMeta((resp, body), resp) diff --git a/cinderclient/v3/quota_classes.py b/cinderclient/v3/quota_classes.py new file mode 100644 index 000000000..0e5fb5b83 --- /dev/null +++ b/cinderclient/v3/quota_classes.py @@ -0,0 +1,46 @@ +# Copyright (c) 2013 OpenStack Foundation +# 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 import base + + +class QuotaClassSet(base.Resource): + + @property + def id(self): + """Needed by base.Resource to self-refresh and be indexed.""" + return self.class_name + + def update(self, *args, **kwargs): + return self.manager.update(self.class_name, *args, **kwargs) + + +class QuotaClassSetManager(base.Manager): + resource_class = QuotaClassSet + + def get(self, class_name): + return self._get("/os-quota-class-sets/%s" % (class_name), + "quota_class_set") + + def update(self, class_name, **updates): + body = {'quota_class_set': {'class_name': class_name}} + + for update in updates: + body['quota_class_set'][update] = updates[update] + + result = self._update('/os-quota-class-sets/%s' % (class_name), body) + return self.resource_class(self, + result['quota_class_set'], loaded=True, + resp=result.request_ids) diff --git a/cinderclient/v3/quotas.py b/cinderclient/v3/quotas.py new file mode 100644 index 000000000..bebf32a39 --- /dev/null +++ b/cinderclient/v3/quotas.py @@ -0,0 +1,56 @@ +# Copyright (c) 2013 OpenStack Foundation +# 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 import base + + +class QuotaSet(base.Resource): + + @property + def id(self): + """Needed by base.Resource to self-refresh and be indexed.""" + return self.tenant_id + + def update(self, *args, **kwargs): + return self.manager.update(self.tenant_id, *args, **kwargs) + + +class QuotaSetManager(base.Manager): + resource_class = QuotaSet + + def get(self, tenant_id, usage=False): + if hasattr(tenant_id, 'tenant_id'): + tenant_id = tenant_id.tenant_id + return self._get("/os-quota-sets/%s?usage=%s" % (tenant_id, usage), + "quota_set") + + def update(self, tenant_id, **updates): + body = {'quota_set': {'tenant_id': tenant_id}} + + for update in updates: + body['quota_set'][update] = updates[update] + + result = self._update('/os-quota-sets/%s' % (tenant_id), body) + return self.resource_class(self, result['quota_set'], loaded=True, + resp=result.request_ids) + + def defaults(self, tenant_id): + return self._get('/os-quota-sets/%s/defaults' % tenant_id, + 'quota_set') + + def delete(self, tenant_id): + if hasattr(tenant_id, 'tenant_id'): + tenant_id = tenant_id.tenant_id + return self._delete("/os-quota-sets/%s" % tenant_id) diff --git a/cinderclient/v3/services.py b/cinderclient/v3/services.py new file mode 100644 index 000000000..8cefc342b --- /dev/null +++ b/cinderclient/v3/services.py @@ -0,0 +1,79 @@ +# Copyright (c) 2013 OpenStack Foundation +# 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. + +""" +service interface +""" +from cinderclient import base + + +class Service(base.Resource): + + def __repr__(self): + return "" % self.service + + +class ServiceManager(base.ManagerWithFind): + resource_class = Service + + def list(self, host=None, binary=None): + """ + Describes service list for host. + + :param host: destination host name. + :param binary: service binary. + """ + url = "/os-services" + filters = [] + if host: + filters.append("host=%s" % host) + if binary: + filters.append("binary=%s" % binary) + if filters: + url = "%s?%s" % (url, "&".join(filters)) + return self._list(url, "services") + + def enable(self, host, binary): + """Enable the service specified by hostname and binary.""" + body = {"host": host, "binary": binary} + result = self._update("/os-services/enable", body) + return self.resource_class(self, result, resp=result.request_ids) + + def disable(self, host, binary): + """Disable the service specified by hostname and binary.""" + body = {"host": host, "binary": binary} + result = self._update("/os-services/disable", body) + return self.resource_class(self, result, resp=result.request_ids) + + def disable_log_reason(self, host, binary, reason): + """Disable the service with reason.""" + body = {"host": host, "binary": binary, "disabled_reason": reason} + result = self._update("/os-services/disable-log-reason", body) + return self.resource_class(self, result, resp=result.request_ids) + + def freeze_host(self, host): + """Freeze the service specified by hostname.""" + body = {"host": host} + return self._update("/os-services/freeze", body) + + def thaw_host(self, host): + """Thaw the service specified by hostname.""" + body = {"host": host} + return self._update("/os-services/thaw", body) + + def failover_host(self, host, backend_id): + """Failover a replicated backend by hostname.""" + body = {"host": host, "backend_id": backend_id} + return self._update("/os-services/failover_host", body) diff --git a/cinderclient/v3/shell.py b/cinderclient/v3/shell.py new file mode 100644 index 000000000..f28dedb0b --- /dev/null +++ b/cinderclient/v3/shell.py @@ -0,0 +1,2655 @@ +# Copyright (c) 2013-2014 OpenStack Foundation +# 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 __future__ import print_function + +import argparse +import copy +import os +import sys +import time + +import six + +from cinderclient import base +from cinderclient import exceptions +from cinderclient import utils +from cinderclient.v3 import availability_zones +from oslo_utils import strutils + + +def _poll_for_status(poll_fn, obj_id, action, final_ok_states, + poll_period=5, show_progress=True): + """Blocks while an action occurs. Periodically shows progress.""" + def print_progress(progress): + if show_progress: + msg = ('\rInstance %(action)s... %(progress)s%% complete' + % dict(action=action, progress=progress)) + else: + msg = '\rInstance %(action)s...' % dict(action=action) + + sys.stdout.write(msg) + sys.stdout.flush() + + print() + while True: + obj = poll_fn(obj_id) + status = obj.status.lower() + progress = getattr(obj, 'progress', None) or 0 + if status in final_ok_states: + print_progress(100) + print("\nFinished") + break + elif status == "error": + print("\nError %(action)s instance" % {'action': action}) + break + else: + print_progress(progress) + time.sleep(poll_period) + + +def _find_volume_snapshot(cs, snapshot): + """Gets a volume snapshot by name or ID.""" + return utils.find_resource(cs.volume_snapshots, snapshot) + + +def _find_vtype(cs, vtype): + """Gets a volume type by name or ID.""" + return utils.find_resource(cs.volume_types, vtype) + + +def _find_backup(cs, backup): + """Gets a backup by name or ID.""" + 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) + + +def _find_qos_specs(cs, qos_specs): + """Gets a qos specs by ID.""" + return utils.find_resource(cs.qos_specs, qos_specs) + + +def _print_volume_snapshot(snapshot): + utils.print_dict(snapshot._info) + + +def _print_volume_image(image): + utils.print_dict(image[1]['os-volume_upload_image']) + + +def _translate_keys(collection, convert): + for item in collection: + keys = item.__dict__ + for from_key, to_key in convert: + if from_key in keys and to_key not in keys: + setattr(item, to_key, item._info[from_key]) + + +def _translate_volume_keys(collection): + convert = [('volumeType', 'volume_type'), + ('os-vol-tenant-attr:tenant_id', 'tenant_id')] + _translate_keys(collection, convert) + + +def _translate_volume_snapshot_keys(collection): + convert = [('volumeId', 'volume_id')] + _translate_keys(collection, convert) + + +def _translate_availability_zone_keys(collection): + convert = [('zoneName', 'name'), ('zoneState', 'status')] + _translate_keys(collection, convert) + + +def _extract_metadata(args): + metadata = {} + for metadatum in args.metadata: + # unset doesn't require a val, so we have the if/else + if '=' in metadatum: + (key, value) = metadatum.split('=', 1) + else: + key = metadatum + value = None + + metadata[key] = value + return metadata + + +@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('--all_tenants', + nargs='?', + type=int, + const=1, + help=argparse.SUPPRESS) +@utils.arg('--name', + metavar='', + default=None, + help='Filters results by a name. Default=None.') +@utils.arg('--display-name', + help=argparse.SUPPRESS) +@utils.arg('--status', + metavar='', + default=None, + help='Filters results by a status. Default=None.') +@utils.arg('--bootable', + metavar='', + const=True, + nargs='?', + choices=['True', 'true', 'False', 'false'], + help='Filters results by bootable status. Default=None.') +@utils.arg('--migration_status', + metavar='', + default=None, + help='Filters results by a migration status. Default=None. ' + 'Admin only.') +@utils.arg('--metadata', + type=str, + nargs='*', + metavar='', + default=None, + help='Filters results by a metadata key and value pair. ' + 'Default=None.') +@utils.arg('--marker', + metavar='', + default=None, + help='Begin returning volumes that appear later in the volume ' + 'list than that represented by this volume id. ' + 'Default=None.') +@utils.arg('--limit', + metavar='', + default=None, + help='Maximum number of volumes to return. Default=None.') +@utils.arg('--fields', + default=None, + metavar='', + help='Comma-separated list of fields to display. ' + 'Use the show command to see which fields are available. ' + 'Unavailable/non-existent fields will be ignored. ' + 'Default=None.') +@utils.arg('--sort_key', + metavar='', + default=None, + help=argparse.SUPPRESS) +@utils.arg('--sort_dir', + metavar='', + default=None, + help=argparse.SUPPRESS) +@utils.arg('--sort', + metavar='[:]', + default=None, + help=(('Comma-separated list of sort keys and directions in the ' + 'form of [:]. ' + 'Valid keys: %s. ' + 'Default=None.') % ', '.join(base.SORT_KEY_VALUES))) +@utils.arg('--tenant', + type=str, + dest='tenant', + nargs='?', + metavar='', + help='Display information from single tenant (Admin only).') +@utils.service_type('volumev3') +def do_list(cs, args): + """Lists all volumes.""" + # NOTE(thingee): Backwards-compatibility with v1 args + if args.display_name is not None: + args.name = args.display_name + + all_tenants = 1 if args.tenant else \ + int(os.environ.get("ALL_TENANTS", args.all_tenants)) + search_opts = { + 'all_tenants': all_tenants, + 'project_id': args.tenant, + 'name': args.name, + 'status': args.status, + 'bootable': args.bootable, + 'migration_status': args.migration_status, + 'metadata': _extract_metadata(args) if args.metadata else None, + } + + # If unavailable/non-existent fields are specified, these fields will + # be removed from key_list at the print_list() during key validation. + field_titles = [] + if args.fields: + for field_title in args.fields.split(','): + field_titles.append(field_title) + + # --sort_key and --sort_dir deprecated in kilo and is not supported + # with --sort + if args.sort and (args.sort_key or args.sort_dir): + raise exceptions.CommandError( + 'The --sort_key and --sort_dir arguments are deprecated and are ' + 'not supported with --sort.') + + volumes = cs.volumes.list(search_opts=search_opts, marker=args.marker, + limit=args.limit, sort_key=args.sort_key, + sort_dir=args.sort_dir, sort=args.sort) + _translate_volume_keys(volumes) + + # Create a list of servers to which the volume is attached + for vol in volumes: + servers = [s.get('server_id') for s in vol.attachments] + setattr(vol, 'attached_to', ','.join(map(str, servers))) + + if field_titles: + key_list = ['ID'] + field_titles + else: + key_list = ['ID', 'Status', 'Name', 'Size', 'Volume Type', + 'Bootable', 'Attached to'] + # If all_tenants is specified, print + # Tenant ID as well. + if search_opts['all_tenants']: + key_list.insert(1, 'Tenant ID') + + if args.sort_key or args.sort_dir or args.sort: + sortby_index = None + else: + sortby_index = 0 + utils.print_list(volumes, key_list, exclude_unavailable=True, + sortby_index=sortby_index) + + +@utils.arg('volume', + metavar='', + help='Name or ID of volume.') +@utils.service_type('volumev3') +def do_show(cs, args): + """Shows volume details.""" + info = dict() + volume = utils.find_volume(cs, args.volume) + info.update(volume._info) + + info.pop('links', None) + utils.print_dict(info, + formatters=['metadata', 'volume_image_metadata']) + + +class CheckSizeArgForCreate(argparse.Action): + def __call__(self, parser, args, values, option_string=None): + if ((args.snapshot_id or args.source_volid or args.source_replica) + is None and values is None): + parser.error('Size is a required parameter if snapshot ' + 'or source volume is not specified.') + setattr(args, self.dest, values) + + +@utils.arg('size', + metavar='', + nargs='?', + type=int, + action=CheckSizeArgForCreate, + help='Size of volume, in GiBs. (Required unless ' + 'snapshot-id/source-volid is specified).') +@utils.arg('--consisgroup-id', + metavar='', + default=None, + help='ID of a consistency group where the new volume belongs to. ' + 'Default=None.') +@utils.arg('--snapshot-id', + metavar='', + default=None, + help='Creates volume from snapshot ID. Default=None.') +@utils.arg('--snapshot_id', + help=argparse.SUPPRESS) +@utils.arg('--source-volid', + metavar='', + default=None, + help='Creates volume from volume ID. Default=None.') +@utils.arg('--source_volid', + help=argparse.SUPPRESS) +@utils.arg('--source-replica', + metavar='', + default=None, + help='Creates volume from replicated volume ID. Default=None.') +@utils.arg('--image-id', + metavar='', + default=None, + help='Creates volume from image ID. Default=None.') +@utils.arg('--image_id', + help=argparse.SUPPRESS) +@utils.arg('--image', + metavar='', + default=None, + help='Creates a volume from image (ID or name). Default=None.') +@utils.arg('--image_ref', + help=argparse.SUPPRESS) +@utils.arg('--name', + metavar='', + default=None, + help='Volume name. Default=None.') +@utils.arg('--display-name', + help=argparse.SUPPRESS) +@utils.arg('--display_name', + help=argparse.SUPPRESS) +@utils.arg('--description', + metavar='', + default=None, + help='Volume description. Default=None.') +@utils.arg('--display-description', + help=argparse.SUPPRESS) +@utils.arg('--display_description', + help=argparse.SUPPRESS) +@utils.arg('--volume-type', + metavar='', + default=None, + help='Volume type. Default=None.') +@utils.arg('--volume_type', + help=argparse.SUPPRESS) +@utils.arg('--availability-zone', + metavar='', + default=None, + help='Availability zone for volume. Default=None.') +@utils.arg('--availability_zone', + help=argparse.SUPPRESS) +@utils.arg('--metadata', + type=str, + nargs='*', + metavar='', + default=None, + help='Metadata key and value pairs. Default=None.') +@utils.arg('--hint', + metavar='', + dest='scheduler_hints', + action='append', + default=[], + help='Scheduler hint, like in nova.') +@utils.arg('--allow-multiattach', + dest='multiattach', + action="store_true", + help=('Allow volume to be attached more than once.' + ' Default=False'), + default=False) +@utils.service_type('volumev3') +def do_create(cs, args): + """Creates a volume.""" + # NOTE(thingee): Backwards-compatibility with v1 args + if args.display_name is not None: + args.name = args.display_name + + if args.display_description is not None: + args.description = args.display_description + + volume_metadata = None + if args.metadata is not None: + volume_metadata = _extract_metadata(args) + + # NOTE(N.S.): take this piece from novaclient + hints = {} + if args.scheduler_hints: + for hint in args.scheduler_hints: + key, _sep, value = hint.partition('=') + # NOTE(vish): multiple copies of same hint will + # result in a list of values + if key in hints: + if isinstance(hints[key], six.string_types): + hints[key] = [hints[key]] + hints[key] += [value] + else: + hints[key] = value + # NOTE(N.S.): end of taken piece + + # Keep backward compatibility with image_id, favoring explicit ID + image_ref = args.image_id or args.image or args.image_ref + + volume = cs.volumes.create(args.size, + args.consisgroup_id, + args.snapshot_id, + args.source_volid, + args.name, + args.description, + args.volume_type, + availability_zone=args.availability_zone, + imageRef=image_ref, + metadata=volume_metadata, + scheduler_hints=hints, + source_replica=args.source_replica, + multiattach=args.multiattach) + + info = dict() + volume = cs.volumes.get(volume.id) + info.update(volume._info) + + info.pop('links', None) + utils.print_dict(info) + + +@utils.arg('--cascade', + metavar='', + default=False, + const=True, + nargs='?', + help='Remove any snapshots along with volume. Default=False.') +@utils.arg('volume', + metavar='', nargs='+', + help='Name or ID of volume or volumes to delete.') +@utils.service_type('volumev3') +def do_delete(cs, args): + """Removes one or more volumes.""" + failure_count = 0 + for volume in args.volume: + try: + utils.find_volume(cs, volume).delete(cascade=args.cascade) + print("Request to delete volume %s has been accepted." % (volume)) + except Exception as e: + failure_count += 1 + print("Delete for volume %s failed: %s" % (volume, e)) + if failure_count == len(args.volume): + raise exceptions.CommandError("Unable to delete any of the specified " + "volumes.") + + +@utils.arg('volume', + metavar='', nargs='+', + help='Name or ID of volume or volumes to delete.') +@utils.service_type('volumev3') +def do_force_delete(cs, args): + """Attempts force-delete of volume, regardless of state.""" + failure_count = 0 + for volume in args.volume: + try: + utils.find_volume(cs, volume).force_delete() + except Exception as e: + failure_count += 1 + print("Delete for volume %s failed: %s" % (volume, e)) + if failure_count == len(args.volume): + raise exceptions.CommandError("Unable to force delete any of the " + "specified volumes.") + + +@utils.arg('volume', metavar='', nargs='+', + help='Name or ID of volume to modify.') +@utils.arg('--state', metavar='', default='available', + help=('The state to assign to the volume. Valid values are ' + '"available", "error", "creating", "deleting", "in-use", ' + '"attaching", "detaching", "error_deleting" and ' + '"maintenance". ' + 'NOTE: This command simply changes the state of the ' + 'Volume in the DataBase with no regard to actual status, ' + 'exercise caution when using. Default=available.')) +@utils.arg('--attach-status', metavar='', default=None, + help=('The attach status to assign to the volume in the DataBase, ' + 'with no regard to the actual status. Valid values are ' + '"attached" and "detached". Default=None, that means the ' + 'status is unchanged.')) +@utils.arg('--reset-migration-status', + action='store_true', + help=('Clears the migration status of the volume in the DataBase ' + 'that indicates the volume is source or destination of ' + 'volume migration, with no regard to the actual status.')) +@utils.service_type('volumev3') +def do_reset_state(cs, args): + """Explicitly updates the volume state in the Cinder database. + + Note that this does not affect whether the volume is actually attached to + the Nova compute host or instance and can result in an unusable volume. + Being a database change only, this has no impact on the true state of the + volume and may not match the actual state. This can render a volume + unusable in the case of change to the 'available' state. + """ + failure_flag = False + migration_status = 'none' if args.reset_migration_status else None + + for volume in args.volume: + try: + utils.find_volume(cs, volume).reset_state(args.state, + args.attach_status, + migration_status) + except Exception as e: + failure_flag = True + msg = "Reset state for volume %s failed: %s" % (volume, e) + print(msg) + + if failure_flag: + msg = "Unable to reset the state for the specified volume(s)." + raise exceptions.CommandError(msg) + + +@utils.arg('volume', + metavar='', + help='Name or ID of volume to rename.') +@utils.arg('name', + nargs='?', + metavar='', + help='New name for volume.') +@utils.arg('--description', metavar='', + help='Volume description. Default=None.', + default=None) +@utils.arg('--display-description', + help=argparse.SUPPRESS) +@utils.arg('--display_description', + help=argparse.SUPPRESS) +@utils.service_type('volumev3') +def do_rename(cs, args): + """Renames a volume.""" + kwargs = {} + + if args.name is not None: + kwargs['name'] = args.name + if args.display_description is not None: + kwargs['description'] = args.display_description + elif args.description is not None: + kwargs['description'] = args.description + + if not any(kwargs): + msg = 'Must supply either name or description.' + raise exceptions.ClientException(code=1, message=msg) + + utils.find_volume(cs, args.volume).update(**kwargs) + + +@utils.arg('volume', + metavar='', + help='Name or ID of volume for which to update metadata.') +@utils.arg('action', + metavar='', + choices=['set', 'unset'], + help='The action. Valid values are "set" or "unset."') +@utils.arg('metadata', + metavar='', + nargs='+', + default=[], + help='Metadata key and value pair to set or unset. ' + 'For unset, specify only the key.') +@utils.service_type('volumev3') +def do_metadata(cs, args): + """Sets or deletes volume metadata.""" + volume = utils.find_volume(cs, args.volume) + metadata = _extract_metadata(args) + + if args.action == 'set': + cs.volumes.set_metadata(volume, metadata) + elif args.action == 'unset': + # NOTE(zul): Make sure py2/py3 sorting is the same + cs.volumes.delete_metadata(volume, sorted(metadata.keys(), + reverse=True)) + + +@utils.arg('volume', + metavar='', + help='Name or ID of volume for which to update metadata.') +@utils.arg('action', + metavar='', + choices=['set', 'unset'], + help="The action. Valid values are 'set' or 'unset.'") +@utils.arg('metadata', + metavar='', + nargs='+', + default=[], + help='Metadata key and value pair to set or unset. ' + 'For unset, specify only the key.') +@utils.service_type('volumev3') +def do_image_metadata(cs, args): + """Sets or deletes volume image metadata.""" + volume = utils.find_volume(cs, args.volume) + metadata = _extract_metadata(args) + + if args.action == 'set': + cs.volumes.set_image_metadata(volume, metadata) + elif args.action == 'unset': + cs.volumes.delete_image_metadata(volume, sorted(metadata.keys(), + reverse=True)) + + +@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('--all_tenants', + nargs='?', + type=int, + const=1, + help=argparse.SUPPRESS) +@utils.arg('--name', + metavar='', + default=None, + help='Filters results by a name. Default=None.') +@utils.arg('--display-name', + help=argparse.SUPPRESS) +@utils.arg('--display_name', + help=argparse.SUPPRESS) +@utils.arg('--status', + metavar='', + default=None, + help='Filters results by a status. Default=None.') +@utils.arg('--volume-id', + metavar='', + default=None, + help='Filters results by a volume ID. Default=None.') +@utils.arg('--volume_id', + help=argparse.SUPPRESS) +@utils.arg('--marker', + metavar='', + default=None, + help='Begin returning snapshots that appear later in the snapshot ' + 'list than that represented by this id. ' + 'Default=None.') +@utils.arg('--limit', + metavar='', + default=None, + help='Maximum number of snapshots to return. Default=None.') +@utils.arg('--sort', + metavar='[:]', + default=None, + help=(('Comma-separated list of sort keys and directions in the ' + 'form of [:]. ' + 'Valid keys: %s. ' + 'Default=None.') % ', '.join(base.SORT_KEY_VALUES))) +@utils.arg('--tenant', + type=str, + dest='tenant', + nargs='?', + metavar='', + help='Display information from single tenant (Admin only).') +@utils.service_type('volumev3') +def do_snapshot_list(cs, args): + """Lists all snapshots.""" + all_tenants = (1 if args.tenant else + int(os.environ.get("ALL_TENANTS", args.all_tenants))) + + if args.display_name is not None: + args.name = args.display_name + + search_opts = { + 'all_tenants': all_tenants, + 'display_name': args.name, + 'status': args.status, + 'volume_id': args.volume_id, + 'project_id': args.tenant, + } + + snapshots = cs.volume_snapshots.list(search_opts=search_opts, + marker=args.marker, + limit=args.limit, + sort=args.sort) + _translate_volume_snapshot_keys(snapshots) + if args.sort: + sortby_index = None + else: + sortby_index = 0 + + utils.print_list(snapshots, + ['ID', 'Volume ID', 'Status', 'Name', 'Size'], + sortby_index=sortby_index) + + +@utils.arg('snapshot', + metavar='', + help='Name or ID of snapshot.') +@utils.service_type('volumev3') +def do_snapshot_show(cs, args): + """Shows snapshot details.""" + snapshot = _find_volume_snapshot(cs, args.snapshot) + _print_volume_snapshot(snapshot) + + +@utils.arg('volume', + metavar='', + help='Name or ID of volume to snapshot.') +@utils.arg('--force', + metavar='', + const=True, + nargs='?', + default=False, + help='Allows or disallows snapshot of ' + 'a volume when the volume is attached to an instance. ' + 'If set to True, ignores the current status of the ' + 'volume when attempting to snapshot it rather ' + 'than forcing it to be available. ' + 'Default=False.') +@utils.arg('--name', + metavar='', + default=None, + help='Snapshot name. Default=None.') +@utils.arg('--display-name', + help=argparse.SUPPRESS) +@utils.arg('--display_name', + help=argparse.SUPPRESS) +@utils.arg('--description', + metavar='', + default=None, + help='Snapshot description. Default=None.') +@utils.arg('--display-description', + help=argparse.SUPPRESS) +@utils.arg('--display_description', + help=argparse.SUPPRESS) +@utils.arg('--metadata', + type=str, + nargs='*', + metavar='', + default=None, + help='Snapshot metadata key and value pairs. Default=None.') +@utils.service_type('volumev3') +def do_snapshot_create(cs, args): + """Creates a snapshot.""" + if args.display_name is not None: + args.name = args.display_name + + if args.display_description is not None: + args.description = args.display_description + + snapshot_metadata = None + if args.metadata is not None: + snapshot_metadata = _extract_metadata(args) + + volume = utils.find_volume(cs, args.volume) + snapshot = cs.volume_snapshots.create(volume.id, + args.force, + args.name, + args.description, + metadata=snapshot_metadata) + _print_volume_snapshot(snapshot) + + +@utils.arg('snapshot', + metavar='', nargs='+', + help='Name or ID of the snapshot(s) to delete.') +@utils.service_type('volumev3') +def do_snapshot_delete(cs, args): + """Removes one or more snapshots.""" + failure_count = 0 + for snapshot in args.snapshot: + try: + _find_volume_snapshot(cs, snapshot).delete() + except Exception as e: + failure_count += 1 + print("Delete for snapshot %s failed: %s" % (snapshot, e)) + if failure_count == len(args.snapshot): + raise exceptions.CommandError("Unable to delete any of the specified " + "snapshots.") + + +@utils.arg('snapshot', metavar='', + help='Name or ID of snapshot.') +@utils.arg('name', nargs='?', metavar='', + help='New name for snapshot.') +@utils.arg('--description', metavar='', + default=None, + help='Snapshot description. Default=None.') +@utils.arg('--display-description', + help=argparse.SUPPRESS) +@utils.arg('--display_description', + help=argparse.SUPPRESS) +@utils.service_type('volumev3') +def do_snapshot_rename(cs, args): + """Renames a snapshot.""" + kwargs = {} + + if args.name is not None: + kwargs['name'] = args.name + + if args.description is not None: + kwargs['description'] = args.description + elif args.display_description is not None: + kwargs['description'] = args.display_description + + if not any(kwargs): + msg = 'Must supply either name or description.' + raise exceptions.ClientException(code=1, message=msg) + + _find_volume_snapshot(cs, args.snapshot).update(**kwargs) + + +@utils.arg('snapshot', metavar='', nargs='+', + help='Name or ID of snapshot to modify.') +@utils.arg('--state', metavar='', + default='available', + help=('The state to assign to the snapshot. Valid values are ' + '"available", "error", "creating", "deleting", and ' + '"error_deleting". NOTE: This command simply changes ' + 'the state of the Snapshot in the DataBase with no regard ' + 'to actual status, exercise caution when using. ' + 'Default=available.')) +@utils.service_type('volumev3') +def do_snapshot_reset_state(cs, args): + """Explicitly updates the snapshot state.""" + failure_count = 0 + + single = (len(args.snapshot) == 1) + + for snapshot in args.snapshot: + try: + _find_volume_snapshot(cs, snapshot).reset_state(args.state) + except Exception as e: + failure_count += 1 + msg = "Reset state for snapshot %s failed: %s" % (snapshot, e) + if not single: + print(msg) + + if failure_count == len(args.snapshot): + if not single: + msg = ("Unable to reset the state for any of the specified " + "snapshots.") + raise exceptions.CommandError(msg) + + +def _print_volume_type_list(vtypes): + utils.print_list(vtypes, ['ID', 'Name', 'Description', 'Is_Public']) + + +@utils.service_type('volumev3') +def do_type_list(cs, args): + """Lists available 'volume types'. (Admin only will see private types)""" + vtypes = cs.volume_types.list() + _print_volume_type_list(vtypes) + + +@utils.service_type('volumev3') +def do_type_default(cs, args): + """List the default volume type.""" + vtype = cs.volume_types.default() + _print_volume_type_list([vtype]) + + +@utils.arg('volume_type', + metavar='', + help='Name or ID of the volume type.') +@utils.service_type('volumev3') +def do_type_show(cs, args): + """Show volume type details.""" + vtype = _find_vtype(cs, args.volume_type) + info = dict() + info.update(vtype._info) + + info.pop('links', None) + utils.print_dict(info) + + +@utils.arg('id', + metavar='', + help='ID of the volume type.') +@utils.arg('--name', + metavar='', + help='Name of the volume type.') +@utils.arg('--description', + metavar='', + help='Description of the volume type.') +@utils.arg('--is-public', + metavar='', + help='Make type accessible to the public or not.') +@utils.service_type('volumev3') +def do_type_update(cs, args): + """Updates volume type name, description, and/or is_public.""" + is_public = strutils.bool_from_string(args.is_public) + vtype = cs.volume_types.update(args.id, args.name, args.description, + is_public) + _print_volume_type_list([vtype]) + + +@utils.service_type('volumev3') +def do_extra_specs_list(cs, args): + """Lists current volume types and extra specs.""" + vtypes = cs.volume_types.list() + utils.print_list(vtypes, ['ID', 'Name', 'extra_specs']) + + +@utils.arg('name', + metavar='', + help='Name of new volume type.') +@utils.arg('--description', + metavar='', + help='Description of new volume type.') +@utils.arg('--is-public', + metavar='', + default=True, + help='Make type accessible to the public (default true).') +@utils.service_type('volumev3') +def do_type_create(cs, args): + """Creates a volume type.""" + is_public = strutils.bool_from_string(args.is_public) + vtype = cs.volume_types.create(args.name, args.description, is_public) + _print_volume_type_list([vtype]) + + +@utils.arg('vol_type', + metavar='', nargs='+', + help='Name or ID of volume type or types to delete.') +@utils.service_type('volumev3') +def do_type_delete(cs, args): + """Deletes volume type or types.""" + failure_count = 0 + for vol_type in args.vol_type: + try: + vtype = _find_volume_type(cs, vol_type) + cs.volume_types.delete(vtype) + print("Request to delete volume type %s has been accepted." + % (vol_type)) + except Exception as e: + failure_count += 1 + print("Delete for volume type %s failed: %s" % (vol_type, e)) + if failure_count == len(args.vol_type): + raise exceptions.CommandError("Unable to delete any of the " + "specified types.") + + +@utils.arg('vtype', + metavar='', + help='Name or ID of volume type.') +@utils.arg('action', + metavar='', + choices=['set', 'unset'], + help='The action. Valid values are "set" or "unset."') +@utils.arg('metadata', + metavar='', + nargs='+', + default=[], + help='The extra specs key and value pair to set or unset. ' + 'For unset, specify only the key.') +@utils.service_type('volumev3') +def do_type_key(cs, args): + """Sets or unsets extra_spec for a volume type.""" + vtype = _find_volume_type(cs, args.vtype) + keypair = _extract_metadata(args) + + if args.action == 'set': + vtype.set_keys(keypair) + elif args.action == 'unset': + vtype.unset_keys(list(keypair)) + + +@utils.arg('--volume-type', metavar='', required=True, + help='Filter results by volume type name or ID.') +@utils.service_type('volumev3') +def do_type_access_list(cs, args): + """Print access information about the given volume type.""" + volume_type = _find_volume_type(cs, args.volume_type) + if volume_type.is_public: + raise exceptions.CommandError("Failed to get access list " + "for public volume type.") + access_list = cs.volume_type_access.list(volume_type) + + columns = ['Volume_type_ID', 'Project_ID'] + utils.print_list(access_list, columns) + + +@utils.arg('--volume-type', metavar='', required=True, + help='Volume type name or ID to add access for the given project.') +@utils.arg('--project-id', metavar='', required=True, + help='Project ID to add volume type access for.') +@utils.service_type('volumev3') +def do_type_access_add(cs, args): + """Adds volume type access for the given project.""" + vtype = _find_volume_type(cs, args.volume_type) + cs.volume_type_access.add_project_access(vtype, args.project_id) + + +@utils.arg('--volume-type', metavar='', required=True, + help=('Volume type name or ID to remove access ' + 'for the given project.')) +@utils.arg('--project-id', metavar='', required=True, + help='Project ID to remove volume type access for.') +@utils.service_type('volumev3') +def do_type_access_remove(cs, args): + """Removes volume type access for the given project.""" + vtype = _find_volume_type(cs, args.volume_type) + cs.volume_type_access.remove_project_access( + vtype, args.project_id) + + +@utils.service_type('volumev3') +def do_endpoints(cs, args): + """Discovers endpoints registered by authentication service.""" + catalog = cs.client.service_catalog.catalog + for e in catalog['serviceCatalog']: + utils.print_dict(e['endpoints'][0], e['name']) + + +@utils.service_type('volumev3') +def do_credentials(cs, args): + """Shows user credentials returned from auth.""" + catalog = cs.client.service_catalog.catalog + + # formatters defines field to be converted from unicode to string + utils.print_dict(catalog['user'], "User Credentials", + formatters=['domain', 'roles']) + utils.print_dict(catalog['token'], "Token", + formatters=['audit_ids', 'tenant']) + +_quota_resources = ['volumes', 'snapshots', 'gigabytes', + 'backups', 'backup_gigabytes', + 'consistencygroups', 'per_volume_gigabytes'] +_quota_infos = ['Type', 'In_use', 'Reserved', 'Limit'] + + +def _quota_show(quotas): + quota_dict = {} + for resource in quotas._info: + good_name = False + for name in _quota_resources: + if resource.startswith(name): + good_name = True + if not good_name: + continue + quota_dict[resource] = getattr(quotas, resource, None) + utils.print_dict(quota_dict) + + +def _quota_usage_show(quotas): + quota_list = [] + for resource in quotas._info.keys(): + good_name = False + for name in _quota_resources: + if resource.startswith(name): + good_name = True + if not good_name: + continue + quota_info = getattr(quotas, resource, None) + quota_info['Type'] = resource + quota_info = dict((k.capitalize(), v) for k, v in quota_info.items()) + quota_list.append(quota_info) + utils.print_list(quota_list, _quota_infos) + + +def _quota_update(manager, identifier, args): + updates = {} + for resource in _quota_resources: + val = getattr(args, resource, None) + if val is not None: + if args.volume_type: + resource = resource + '_%s' % args.volume_type + updates[resource] = val + + if updates: + _quota_show(manager.update(identifier, **updates)) + + +@utils.arg('tenant', + metavar='', + help='ID of tenant for which to list quotas.') +@utils.service_type('volumev3') +def do_quota_show(cs, args): + """Lists quotas for a tenant.""" + + _quota_show(cs.quotas.get(args.tenant)) + + +@utils.arg('tenant', metavar='', + help='ID of tenant for which to list quota usage.') +@utils.service_type('volumev3') +def do_quota_usage(cs, args): + """Lists quota usage for a tenant.""" + + _quota_usage_show(cs.quotas.get(args.tenant, usage=True)) + + +@utils.arg('tenant', + metavar='', + help='ID of tenant for which to list quota defaults.') +@utils.service_type('volumev3') +def do_quota_defaults(cs, args): + """Lists default quotas for a tenant.""" + + _quota_show(cs.quotas.defaults(args.tenant)) + + +@utils.arg('tenant', + metavar='', + help='ID of tenant for which to set quotas.') +@utils.arg('--volumes', + metavar='', + type=int, default=None, + help='The new "volumes" quota value. Default=None.') +@utils.arg('--snapshots', + metavar='', + type=int, default=None, + help='The new "snapshots" quota value. Default=None.') +@utils.arg('--gigabytes', + metavar='', + type=int, default=None, + help='The new "gigabytes" quota value. Default=None.') +@utils.arg('--backups', + metavar='', + type=int, default=None, + help='The new "backups" quota value. Default=None.') +@utils.arg('--backup-gigabytes', + metavar='', + type=int, default=None, + help='The new "backup_gigabytes" quota value. Default=None.') +@utils.arg('--consistencygroups', + metavar='', + type=int, default=None, + help='The new "consistencygroups" quota value. Default=None.') +@utils.arg('--volume-type', + metavar='', + default=None, + help='Volume type. Default=None.') +@utils.arg('--per-volume-gigabytes', + metavar='', + type=int, default=None, + help='Set max volume size limit. Default=None.') +@utils.service_type('volumev3') +def do_quota_update(cs, args): + """Updates quotas for a tenant.""" + + _quota_update(cs.quotas, args.tenant, args) + + +@utils.arg('tenant', metavar='', + help='UUID of tenant to delete the quotas for.') +@utils.service_type('volumev3') +def do_quota_delete(cs, args): + """Delete the quotas for a tenant.""" + + cs.quotas.delete(args.tenant) + + +@utils.arg('class_name', + metavar='', + help='Name of quota class for which to list quotas.') +@utils.service_type('volumev3') +def do_quota_class_show(cs, args): + """Lists quotas for a quota class.""" + + _quota_show(cs.quota_classes.get(args.class_name)) + + +@utils.arg('class_name', + metavar='', + help='Name of quota class for which to set quotas.') +@utils.arg('--volumes', + metavar='', + type=int, default=None, + help='The new "volumes" quota value. Default=None.') +@utils.arg('--snapshots', + metavar='', + type=int, default=None, + help='The new "snapshots" quota value. Default=None.') +@utils.arg('--gigabytes', + metavar='', + type=int, default=None, + help='The new "gigabytes" quota value. Default=None.') +@utils.arg('--volume-type', + metavar='', + default=None, + help='Volume type. Default=None.') +@utils.service_type('volumev3') +def do_quota_class_update(cs, args): + """Updates quotas for a quota class.""" + + _quota_update(cs.quota_classes, args.class_name, args) + + +@utils.service_type('volumev3') +def do_absolute_limits(cs, args): + """Lists absolute limits for a user.""" + limits = cs.limits.get().absolute + columns = ['Name', 'Value'] + utils.print_list(limits, columns) + + +@utils.service_type('volumev3') +def do_rate_limits(cs, args): + """Lists rate limits for a user.""" + limits = cs.limits.get().rate + columns = ['Verb', 'URI', 'Value', 'Remain', 'Unit', 'Next_Available'] + utils.print_list(limits, columns) + + +def _find_volume_type(cs, vtype): + """Gets a volume type by name or ID.""" + return utils.find_resource(cs.volume_types, vtype) + + +@utils.arg('volume', + metavar='', + help='Name or ID of volume to snapshot.') +@utils.arg('--force', + metavar='', + const=True, + nargs='?', + default=False, + help='Enables or disables upload of ' + 'a volume that is attached to an instance. ' + 'Default=False.') +@utils.arg('--container-format', + metavar='', + default='bare', + help='Container format type. ' + 'Default is bare.') +@utils.arg('--container_format', + help=argparse.SUPPRESS) +@utils.arg('--disk-format', + metavar='', + default='raw', + help='Disk format type. ' + 'Default is raw.') +@utils.arg('--disk_format', + help=argparse.SUPPRESS) +@utils.arg('image_name', + metavar='', + help='The new image name.') +@utils.arg('--image_name', + help=argparse.SUPPRESS) +@utils.service_type('volumev3') +def do_upload_to_image(cs, args): + """Uploads volume to Image Service as an image.""" + volume = utils.find_volume(cs, args.volume) + _print_volume_image(volume.upload_to_image(args.force, + args.image_name, + args.container_format, + args.disk_format)) + + +@utils.arg('volume', metavar='', help='ID of volume to migrate.') +@utils.arg('host', metavar='', help='Destination host. Takes the form: ' + 'host@backend-name#pool') +@utils.arg('--force-host-copy', metavar='', + choices=['True', 'False'], + required=False, + const=True, + nargs='?', + default=False, + help='Enables or disables generic host-based ' + 'force-migration, which bypasses driver ' + 'optimizations. Default=False.') +@utils.arg('--lock-volume', metavar='', + choices=['True', 'False'], + required=False, + const=True, + nargs='?', + default=False, + help='Enables or disables the termination of volume migration ' + 'caused by other commands. This option applies to the ' + 'available volume. True means it locks the volume ' + 'state and does not allow the migration to be aborted. The ' + 'volume status will be in maintenance during the ' + 'migration. False means it allows the volume migration ' + 'to be aborted. The volume status is still in the original ' + 'status. Default=False.') +@utils.service_type('volumev3') +def do_migrate(cs, args): + """Migrates volume to a new host.""" + volume = utils.find_volume(cs, args.volume) + try: + volume.migrate_volume(args.host, args.force_host_copy, + args.lock_volume) + print("Request to migrate volume %s has been accepted." % (volume)) + except Exception as e: + print("Migration for volume %s failed: %s." % (volume, + six.text_type(e))) + + +@utils.arg('volume', metavar='', + help='Name or ID of volume for which to modify type.') +@utils.arg('new_type', metavar='', help='New volume type.') +@utils.arg('--migration-policy', metavar='', required=False, + choices=['never', 'on-demand'], default='never', + help='Migration policy during retype of volume.') +@utils.service_type('volumev3') +def do_retype(cs, args): + """Changes the volume type for a volume.""" + volume = utils.find_volume(cs, args.volume) + volume.retype(args.new_type, args.migration_policy) + + +@utils.arg('volume', metavar='', + help='Name or ID of volume to backup.') +@utils.arg('--container', metavar='', + default=None, + help='Backup container name. Default=None.') +@utils.arg('--display-name', + help=argparse.SUPPRESS) +@utils.arg('--name', metavar='', + default=None, + help='Backup name. Default=None.') +@utils.arg('--display-description', + help=argparse.SUPPRESS) +@utils.arg('--description', + metavar='', + default=None, + help='Backup description. Default=None.') +@utils.arg('--incremental', + action='store_true', + help='Incremental backup. Default=False.', + default=False) +@utils.arg('--force', + action='store_true', + help='Allows or disallows backup of a volume ' + 'when the volume is attached to an instance. ' + 'If set to True, backs up the volume whether ' + 'its status is "available" or "in-use". The backup ' + 'of an "in-use" volume means your data is crash ' + 'consistent. Default=False.', + default=False) +@utils.arg('--snapshot-id', + metavar='', + default=None, + help='ID of snapshot to backup. Default=None.') +@utils.service_type('volumev3') +def do_backup_create(cs, args): + """Creates a volume backup.""" + if args.display_name is not None: + args.name = args.display_name + + if args.display_description is not None: + args.description = args.display_description + + volume = utils.find_volume(cs, args.volume) + backup = cs.backups.create(volume.id, + args.container, + args.name, + args.description, + args.incremental, + args.force, + args.snapshot_id) + + info = {"volume_id": volume.id} + info.update(backup._info) + + if 'links' in info: + info.pop('links') + + utils.print_dict(info) + + +@utils.arg('backup', metavar='', help='Name or ID of backup.') +@utils.service_type('volumev3') +def do_backup_show(cs, args): + """Shows backup details.""" + backup = _find_backup(cs, args.backup) + info = dict() + info.update(backup._info) + + info.pop('links', None) + utils.print_dict(info) + + +@utils.arg('--all-tenants', + metavar='', + nargs='?', + type=int, + const=1, + default=0, + help='Shows details for all tenants. Admin only.') +@utils.arg('--all_tenants', + nargs='?', + type=int, + const=1, + help=argparse.SUPPRESS) +@utils.arg('--name', + metavar='', + default=None, + help='Filters results by a name. Default=None.') +@utils.arg('--status', + metavar='', + default=None, + help='Filters results by a status. Default=None.') +@utils.arg('--volume-id', + metavar='', + default=None, + help='Filters results by a volume ID. Default=None.') +@utils.arg('--volume_id', + help=argparse.SUPPRESS) +@utils.arg('--marker', + metavar='', + default=None, + help='Begin returning backups that appear later in the backup ' + 'list than that represented by this id. ' + 'Default=None.') +@utils.arg('--limit', + metavar='', + default=None, + help='Maximum number of backups to return. Default=None.') +@utils.arg('--sort', + metavar='[:]', + default=None, + help=(('Comma-separated list of sort keys and directions in the ' + 'form of [:]. ' + 'Valid keys: %s. ' + 'Default=None.') % ', '.join(base.SORT_KEY_VALUES))) +@utils.service_type('volumev3') +def do_backup_list(cs, args): + """Lists all backups.""" + + search_opts = { + 'all_tenants': args.all_tenants, + 'name': args.name, + 'status': args.status, + 'volume_id': args.volume_id, + } + + backups = cs.backups.list(search_opts=search_opts, + marker=args.marker, + limit=args.limit, + sort=args.sort) + _translate_volume_snapshot_keys(backups) + columns = ['ID', 'Volume ID', 'Status', 'Name', 'Size', 'Object Count', + 'Container'] + if args.sort: + sortby_index = None + else: + sortby_index = 0 + utils.print_list(backups, columns, sortby_index=sortby_index) + + +@utils.arg('backup', metavar='', nargs='+', + help='Name or ID of backup(s) to delete.') +@utils.service_type('volumev3') +def do_backup_delete(cs, args): + """Removes one or more backups.""" + failure_count = 0 + for backup in args.backup: + try: + _find_backup(cs, backup).delete() + print("Request to delete backup %s has been accepted." % (backup)) + except Exception as e: + failure_count += 1 + print("Delete for backup %s failed: %s" % (backup, e)) + if failure_count == len(args.backup): + raise exceptions.CommandError("Unable to delete any of the specified " + "backups.") + + +@utils.arg('backup', metavar='', + help='ID of backup to restore.') +@utils.arg('--volume-id', metavar='', + default=None, + help=argparse.SUPPRESS) +@utils.arg('--volume', metavar='', + default=None, + help='Name or ID of volume to which to restore. ' + 'Default=None.') +@utils.service_type('volumev3') +def do_backup_restore(cs, args): + """Restores a backup.""" + vol = args.volume or args.volume_id + if vol: + volume_id = utils.find_volume(cs, vol).id + else: + volume_id = None + + restore = cs.restores.restore(args.backup, volume_id) + + info = {"backup_id": args.backup} + info.update(restore._info) + + info.pop('links', None) + + utils.print_dict(info) + + +@utils.arg('backup', metavar='', + help='ID of the backup to export.') +@utils.service_type('volumev3') +def do_backup_export(cs, args): + """Export backup metadata record.""" + info = cs.backups.export_record(args.backup) + utils.print_dict(info) + + +@utils.arg('backup_service', metavar='', + help='Backup service to use for importing the backup.') +@utils.arg('backup_url', metavar='', + help='Backup URL for importing the backup metadata.') +@utils.service_type('volumev3') +def do_backup_import(cs, args): + """Import backup metadata record.""" + info = cs.backups.import_record(args.backup_service, args.backup_url) + info.pop('links', None) + + utils.print_dict(info) + + +@utils.arg('backup', metavar='', nargs='+', + help='Name or ID of the backup to modify.') +@utils.arg('--state', metavar='', + default='available', + help='The state to assign to the backup. Valid values are ' + '"available", "error". Default=available.') +@utils.service_type('volumev3') +def do_backup_reset_state(cs, args): + """Explicitly updates the backup state.""" + failure_count = 0 + + single = (len(args.backup) == 1) + + for backup in args.backup: + try: + _find_backup(cs, backup).reset_state(args.state) + except Exception as e: + failure_count += 1 + msg = "Reset state for backup %s failed: %s" % (backup, e) + if not single: + print(msg) + + if failure_count == len(args.backup): + if not single: + msg = ("Unable to reset the state for any of the specified " + "backups.") + raise exceptions.CommandError(msg) + + +@utils.arg('volume', metavar='', + help='Name or ID of volume to transfer.') +@utils.arg('--name', + metavar='', + default=None, + help='Transfer name. Default=None.') +@utils.arg('--display-name', + help=argparse.SUPPRESS) +@utils.service_type('volumev3') +def do_transfer_create(cs, args): + """Creates a volume transfer.""" + if args.display_name is not None: + args.name = args.display_name + + volume = utils.find_volume(cs, args.volume) + transfer = cs.transfers.create(volume.id, + args.name) + info = dict() + info.update(transfer._info) + + info.pop('links', None) + utils.print_dict(info) + + +@utils.arg('transfer', metavar='', + help='Name or ID of transfer to delete.') +@utils.service_type('volumev3') +def do_transfer_delete(cs, args): + """Undoes a transfer.""" + transfer = _find_transfer(cs, args.transfer) + transfer.delete() + + +@utils.arg('transfer', metavar='', + help='ID of transfer to accept.') +@utils.arg('auth_key', metavar='', + help='Authentication key of transfer to accept.') +@utils.service_type('volumev3') +def do_transfer_accept(cs, args): + """Accepts a volume transfer.""" + transfer = cs.transfers.accept(args.transfer, args.auth_key) + info = dict() + info.update(transfer._info) + + info.pop('links', None) + utils.print_dict(info) + + +@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('--all_tenants', + nargs='?', + type=int, + const=1, + help=argparse.SUPPRESS) +@utils.service_type('volumev3') +def do_transfer_list(cs, args): + """Lists all transfers.""" + all_tenants = int(os.environ.get("ALL_TENANTS", args.all_tenants)) + search_opts = { + 'all_tenants': all_tenants, + } + transfers = cs.transfers.list(search_opts=search_opts) + columns = ['ID', 'Volume ID', 'Name'] + utils.print_list(transfers, columns) + + +@utils.arg('transfer', metavar='', + help='Name or ID of transfer to accept.') +@utils.service_type('volumev3') +def do_transfer_show(cs, args): + """Shows transfer details.""" + transfer = _find_transfer(cs, args.transfer) + info = dict() + info.update(transfer._info) + + info.pop('links', None) + utils.print_dict(info) + + +@utils.arg('volume', metavar='', + help='Name or ID of volume to extend.') +@utils.arg('new_size', + metavar='', + type=int, + help='New size of volume, in GiBs.') +@utils.service_type('volumev3') +def do_extend(cs, args): + """Attempts to extend size of an existing volume.""" + volume = utils.find_volume(cs, args.volume) + cs.volumes.extend(volume, args.new_size) + + +@utils.arg('--host', metavar='', default=None, + help='Host name. Default=None.') +@utils.arg('--binary', metavar='', default=None, + help='Service binary. Default=None.') +@utils.arg('--withreplication', + metavar='', + const=True, + nargs='?', + default=False, + help='Enables or disables display of ' + 'Replication info for c-vol services. Default=False.') +@utils.service_type('volumev3') +def do_service_list(cs, args): + """Lists all services. Filter by host and service binary.""" + replication = strutils.bool_from_string(args.withreplication) + result = cs.services.list(host=args.host, binary=args.binary) + columns = ["Binary", "Host", "Zone", "Status", "State", "Updated_at"] + if replication: + columns.extend(["Replication Status", "Active Backend ID", "Frozen"]) + # NOTE(jay-lau-513): we check if the response has disabled_reason + # so as not to add the column when the extended ext is not enabled. + if result and hasattr(result[0], 'disabled_reason'): + columns.append("Disabled Reason") + utils.print_list(result, columns) + + +@utils.arg('host', metavar='', help='Host name.') +@utils.arg('binary', metavar='', help='Service binary.') +@utils.service_type('volumev3') +def do_service_enable(cs, args): + """Enables the service.""" + result = cs.services.enable(args.host, args.binary) + columns = ["Host", "Binary", "Status"] + utils.print_list([result], columns) + + +@utils.arg('host', metavar='', help='Host name.') +@utils.arg('binary', metavar='', help='Service binary.') +@utils.arg('--reason', metavar='', + help='Reason for disabling service.') +@utils.service_type('volumev3') +def do_service_disable(cs, args): + """Disables the service.""" + columns = ["Host", "Binary", "Status"] + if args.reason: + columns.append('Disabled Reason') + result = cs.services.disable_log_reason(args.host, args.binary, + args.reason) + else: + result = cs.services.disable(args.host, args.binary) + utils.print_list([result], columns) + + +@utils.service_type('volumev3') +def _treeizeAvailabilityZone(zone): + """Builds a tree view for availability zones.""" + AvailabilityZone = availability_zones.AvailabilityZone + + az = AvailabilityZone(zone.manager, + copy.deepcopy(zone._info), zone._loaded) + result = [] + + # Zone tree view item + az.zoneName = zone.zoneName + az.zoneState = ('available' + if zone.zoneState['available'] else 'not available') + az._info['zoneName'] = az.zoneName + az._info['zoneState'] = az.zoneState + result.append(az) + + if getattr(zone, "hosts", None) and zone.hosts is not None: + for (host, services) in zone.hosts.items(): + # Host tree view item + az = AvailabilityZone(zone.manager, + copy.deepcopy(zone._info), zone._loaded) + az.zoneName = '|- %s' % host + az.zoneState = '' + az._info['zoneName'] = az.zoneName + az._info['zoneState'] = az.zoneState + result.append(az) + + for (svc, state) in services.items(): + # Service tree view item + az = AvailabilityZone(zone.manager, + copy.deepcopy(zone._info), zone._loaded) + az.zoneName = '| |- %s' % svc + az.zoneState = '%s %s %s' % ( + 'enabled' if state['active'] else 'disabled', + ':-)' if state['available'] else 'XXX', + state['updated_at']) + az._info['zoneName'] = az.zoneName + az._info['zoneState'] = az.zoneState + result.append(az) + return result + + +@utils.service_type('volumev3') +def do_availability_zone_list(cs, _args): + """Lists all availability zones.""" + try: + availability_zones = cs.availability_zones.list() + except exceptions.Forbidden as e: # policy doesn't allow probably + try: + availability_zones = cs.availability_zones.list(detailed=False) + except Exception: + raise e + + result = [] + for zone in availability_zones: + result += _treeizeAvailabilityZone(zone) + _translate_availability_zone_keys(result) + utils.print_list(result, ['Name', 'Status']) + + +def _print_volume_encryption_type_list(encryption_types): + """ + Lists volume encryption types. + + :param encryption_types: a list of :class: VolumeEncryptionType instances + """ + utils.print_list(encryption_types, ['Volume Type ID', 'Provider', + 'Cipher', 'Key Size', + 'Control Location']) + + +@utils.service_type('volumev3') +def do_encryption_type_list(cs, args): + """Shows encryption type details for volume types. Admin only.""" + result = cs.volume_encryption_types.list() + utils.print_list(result, ['Volume Type ID', 'Provider', 'Cipher', + 'Key Size', 'Control Location']) + + +@utils.arg('volume_type', + metavar='', + type=str, + help='Name or ID of volume type.') +@utils.service_type('volumev3') +def do_encryption_type_show(cs, args): + """Shows encryption type details for a volume type. Admin only.""" + volume_type = _find_volume_type(cs, args.volume_type) + + result = cs.volume_encryption_types.get(volume_type) + + # Display result or an empty table if no result + if hasattr(result, 'volume_type_id'): + _print_volume_encryption_type_list([result]) + else: + _print_volume_encryption_type_list([]) + + +@utils.arg('volume_type', + metavar='', + type=str, + help='Name or ID of volume type.') +@utils.arg('provider', + metavar='', + type=str, + help='The class that provides encryption support. ' + 'For example, LuksEncryptor.') +@utils.arg('--cipher', + metavar='', + type=str, + required=False, + default=None, + help='The encryption algorithm or mode. ' + 'For example, aes-xts-plain64. Default=None.') +@utils.arg('--key_size', + metavar='', + type=int, + required=False, + default=None, + help='Size of encryption key, in bits. ' + 'For example, 128 or 256. Default=None.') +@utils.arg('--control_location', + metavar='', + choices=['front-end', 'back-end'], + type=str, + required=False, + default='front-end', + help='Notional service where encryption is performed. ' + 'Valid values are "front-end" or "back-end." ' + 'For example, front-end=Nova. Default is "front-end."') +@utils.service_type('volumev3') +def do_encryption_type_create(cs, args): + """Creates encryption type for a volume type. Admin only.""" + volume_type = _find_volume_type(cs, args.volume_type) + + body = { + 'provider': args.provider, + 'cipher': args.cipher, + 'key_size': args.key_size, + 'control_location': args.control_location + } + + result = cs.volume_encryption_types.create(volume_type, body) + _print_volume_encryption_type_list([result]) + + +@utils.arg('volume_type', + metavar='', + type=str, + help="Name or ID of the volume type") +@utils.arg('--provider', + metavar='', + type=str, + required=False, + default=argparse.SUPPRESS, + help="Class providing encryption support (e.g. LuksEncryptor) " + "(Optional)") +@utils.arg('--cipher', + metavar='', + type=str, + nargs='?', + required=False, + default=argparse.SUPPRESS, + const=None, + help="Encryption algorithm/mode to use (e.g., aes-xts-plain64). " + "Provide parameter without value to set to provider default. " + "(Optional)") +@utils.arg('--key-size', + dest='key_size', + metavar='', + type=int, + nargs='?', + required=False, + default=argparse.SUPPRESS, + const=None, + help="Size of the encryption key, in bits (e.g., 128, 256). " + "Provide parameter without value to set to provider default. " + "(Optional)") +@utils.arg('--control-location', + dest='control_location', + metavar='', + choices=['front-end', 'back-end'], + type=str, + required=False, + default=argparse.SUPPRESS, + help="Notional service where encryption is performed (e.g., " + "front-end=Nova). Values: 'front-end', 'back-end' (Optional)") +@utils.service_type('volumev3') +def do_encryption_type_update(cs, args): + """Update encryption type information for a volume type (Admin Only).""" + volume_type = _find_volume_type(cs, args.volume_type) + + # An argument should only be pulled if the user specified the parameter. + body = {} + for attr in ['provider', 'cipher', 'key_size', 'control_location']: + if hasattr(args, attr): + body[attr] = getattr(args, attr) + + cs.volume_encryption_types.update(volume_type, body) + result = cs.volume_encryption_types.get(volume_type) + _print_volume_encryption_type_list([result]) + + +@utils.arg('volume_type', + metavar='', + type=str, + help='Name or ID of volume type.') +@utils.service_type('volumev3') +def do_encryption_type_delete(cs, args): + """Deletes encryption type for a volume type. Admin only.""" + volume_type = _find_volume_type(cs, args.volume_type) + cs.volume_encryption_types.delete(volume_type) + + +def _print_qos_specs(qos_specs): + + # formatters defines field to be converted from unicode to string + utils.print_dict(qos_specs._info, formatters=['specs']) + + +def _print_qos_specs_list(q_specs): + utils.print_list(q_specs, ['ID', 'Name', 'Consumer', 'specs']) + + +def _print_qos_specs_and_associations_list(q_specs): + utils.print_list(q_specs, ['ID', 'Name', 'Consumer', 'specs']) + + +def _print_associations_list(associations): + utils.print_list(associations, ['Association_Type', 'Name', 'ID']) + + +@utils.arg('name', + metavar='', + help='Name of new QoS specifications.') +@utils.arg('metadata', + metavar='', + nargs='+', + default=[], + help='QoS specifications.') +@utils.service_type('volumev3') +def do_qos_create(cs, args): + """Creates a qos specs.""" + keypair = None + if args.metadata is not None: + keypair = _extract_metadata(args) + qos_specs = cs.qos_specs.create(args.name, keypair) + _print_qos_specs(qos_specs) + + +@utils.service_type('volumev3') +def do_qos_list(cs, args): + """Lists qos specs.""" + qos_specs = cs.qos_specs.list() + _print_qos_specs_list(qos_specs) + + +@utils.arg('qos_specs', metavar='', + help='ID of QoS specifications to show.') +@utils.service_type('volumev3') +def do_qos_show(cs, args): + """Shows qos specs details.""" + qos_specs = _find_qos_specs(cs, args.qos_specs) + _print_qos_specs(qos_specs) + + +@utils.arg('qos_specs', metavar='', + help='ID of QoS specifications to delete.') +@utils.arg('--force', + metavar='', + const=True, + nargs='?', + default=False, + help='Enables or disables deletion of in-use ' + 'QoS specifications. Default=False.') +@utils.service_type('volumev3') +def do_qos_delete(cs, args): + """Deletes a specified qos specs.""" + force = strutils.bool_from_string(args.force) + qos_specs = _find_qos_specs(cs, args.qos_specs) + cs.qos_specs.delete(qos_specs, force) + + +@utils.arg('qos_specs', metavar='', + help='ID of QoS specifications.') +@utils.arg('vol_type_id', metavar='', + help='ID of volume type with which to associate ' + 'QoS specifications.') +@utils.service_type('volumev3') +def do_qos_associate(cs, args): + """Associates qos specs with specified volume type.""" + cs.qos_specs.associate(args.qos_specs, args.vol_type_id) + + +@utils.arg('qos_specs', metavar='', + help='ID of QoS specifications.') +@utils.arg('vol_type_id', metavar='', + help='ID of volume type with which to associate ' + 'QoS specifications.') +@utils.service_type('volumev3') +def do_qos_disassociate(cs, args): + """Disassociates qos specs from specified volume type.""" + cs.qos_specs.disassociate(args.qos_specs, args.vol_type_id) + + +@utils.arg('qos_specs', metavar='', + help='ID of QoS specifications on which to operate.') +@utils.service_type('volumev3') +def do_qos_disassociate_all(cs, args): + """Disassociates qos specs from all its associations.""" + cs.qos_specs.disassociate_all(args.qos_specs) + + +@utils.arg('qos_specs', metavar='', + help='ID of QoS specifications.') +@utils.arg('action', + metavar='', + choices=['set', 'unset'], + help='The action. Valid values are "set" or "unset."') +@utils.arg('metadata', metavar='key=value', + nargs='+', + default=[], + help='Metadata key and value pair to set or unset. ' + 'For unset, specify only the key.') +@utils.service_type('volumev3') +def do_qos_key(cs, args): + """Sets or unsets specifications for a qos spec.""" + keypair = _extract_metadata(args) + + if args.action == 'set': + cs.qos_specs.set_keys(args.qos_specs, keypair) + elif args.action == 'unset': + cs.qos_specs.unset_keys(args.qos_specs, list(keypair)) + + +@utils.arg('qos_specs', metavar='', + help='ID of QoS specifications.') +@utils.service_type('volumev3') +def do_qos_get_association(cs, args): + """Lists all associations for specified qos specs.""" + associations = cs.qos_specs.get_associations(args.qos_specs) + _print_associations_list(associations) + + +@utils.arg('snapshot', + metavar='', + help='ID of snapshot for which to update metadata.') +@utils.arg('action', + metavar='', + choices=['set', 'unset'], + help='The action. Valid values are "set" or "unset."') +@utils.arg('metadata', + metavar='', + nargs='+', + default=[], + help='Metadata key and value pair to set or unset. ' + 'For unset, specify only the key.') +@utils.service_type('volumev3') +def do_snapshot_metadata(cs, args): + """Sets or deletes snapshot metadata.""" + snapshot = _find_volume_snapshot(cs, args.snapshot) + metadata = _extract_metadata(args) + + if args.action == 'set': + metadata = snapshot.set_metadata(metadata) + utils.print_dict(metadata._info) + elif args.action == 'unset': + snapshot.delete_metadata(list(metadata.keys())) + + +@utils.arg('snapshot', metavar='', + help='ID of snapshot.') +@utils.service_type('volumev3') +def do_snapshot_metadata_show(cs, args): + """Shows snapshot metadata.""" + snapshot = _find_volume_snapshot(cs, args.snapshot) + utils.print_dict(snapshot._info['metadata'], 'Metadata-property') + + +@utils.arg('volume', metavar='', + help='ID of volume.') +@utils.service_type('volumev3') +def do_metadata_show(cs, args): + """Shows volume metadata.""" + volume = utils.find_volume(cs, args.volume) + utils.print_dict(volume._info['metadata'], 'Metadata-property') + + +@utils.arg('volume', metavar='', + help='ID of volume.') +@utils.service_type('volumev3') +def do_image_metadata_show(cs, args): + """Shows volume image metadata.""" + volume = utils.find_volume(cs, args.volume) + resp, body = volume.show_image_metadata(volume) + utils.print_dict(body['metadata'], 'Metadata-property') + + +@utils.arg('volume', + metavar='', + help='ID of volume for which to update metadata.') +@utils.arg('metadata', + metavar='', + nargs='+', + default=[], + help='Metadata key and value pair or pairs to update.') +@utils.service_type('volumev3') +def do_metadata_update_all(cs, args): + """Updates volume metadata.""" + volume = utils.find_volume(cs, args.volume) + metadata = _extract_metadata(args) + metadata = volume.update_all_metadata(metadata) + utils.print_dict(metadata['metadata'], 'Metadata-property') + + +@utils.arg('snapshot', + metavar='', + help='ID of snapshot for which to update metadata.') +@utils.arg('metadata', + metavar='', + nargs='+', + default=[], + help='Metadata key and value pair to update.') +@utils.service_type('volumev3') +def do_snapshot_metadata_update_all(cs, args): + """Updates snapshot metadata.""" + snapshot = _find_volume_snapshot(cs, args.snapshot) + metadata = _extract_metadata(args) + metadata = snapshot.update_all_metadata(metadata) + utils.print_dict(metadata) + + +@utils.arg('volume', metavar='', help='ID of volume to update.') +@utils.arg('read_only', + metavar='', + choices=['True', 'true', 'False', 'false'], + help='Enables or disables update of volume to ' + 'read-only access mode.') +@utils.service_type('volumev3') +def do_readonly_mode_update(cs, args): + """Updates volume read-only access-mode flag.""" + volume = utils.find_volume(cs, args.volume) + cs.volumes.update_readonly_flag(volume, + strutils.bool_from_string(args.read_only)) + + +@utils.arg('volume', metavar='', help='ID of the volume to update.') +@utils.arg('bootable', + metavar='', + choices=['True', 'true', 'False', 'false'], + help='Flag to indicate whether volume is bootable.') +@utils.service_type('volumev3') +def do_set_bootable(cs, args): + """Update bootable status of a volume.""" + volume = utils.find_volume(cs, args.volume) + cs.volumes.set_bootable(volume, + strutils.bool_from_string(args.bootable)) + + +@utils.arg('host', + metavar='', + help='Cinder host on which the existing volume resides; ' + 'takes the form: host@backend-name#pool') +@utils.arg('identifier', + metavar='', + help='Name or other Identifier for existing volume') +@utils.arg('--id-type', + metavar='', + default='source-name', + help='Type of backend device identifier provided, ' + 'typically source-name or source-id (Default=source-name)') +@utils.arg('--name', + metavar='', + help='Volume name (Default=None)') +@utils.arg('--description', + metavar='', + help='Volume description (Default=None)') +@utils.arg('--volume-type', + metavar='', + help='Volume type (Default=None)') +@utils.arg('--availability-zone', + metavar='', + help='Availability zone for volume (Default=None)') +@utils.arg('--metadata', + type=str, + nargs='*', + metavar='', + help='Metadata key=value pairs (Default=None)') +@utils.arg('--bootable', + action='store_true', + help='Specifies that the newly created volume should be' + ' marked as bootable') +@utils.service_type('volumev3') +def do_manage(cs, args): + """Manage an existing volume.""" + volume_metadata = None + if args.metadata is not None: + volume_metadata = _extract_metadata(args) + + # Build a dictionary of key/value pairs to pass to the API. + ref_dict = {args.id_type: args.identifier} + + # The recommended way to specify an existing volume is by ID or name, and + # have the Cinder driver look for 'source-name' or 'source-id' elements in + # the ref structure. To make things easier for the user, we have special + # --source-name and --source-id CLI options that add the appropriate + # element to the ref structure. + # + # Note how argparse converts hyphens to underscores. We use hyphens in the + # dictionary so that it is consistent with what the user specified on the + # CLI. + + if hasattr(args, 'source_name') and args.source_name is not None: + ref_dict['source-name'] = args.source_name + if hasattr(args, 'source_id') and args.source_id is not None: + ref_dict['source-id'] = args.source_id + + volume = cs.volumes.manage(host=args.host, + ref=ref_dict, + name=args.name, + description=args.description, + volume_type=args.volume_type, + availability_zone=args.availability_zone, + metadata=volume_metadata, + bootable=args.bootable) + + info = {} + volume = cs.volumes.get(volume.id) + info.update(volume._info) + info.pop('links', None) + utils.print_dict(info) + + +@utils.arg('volume', metavar='', + help='Name or ID of the volume to unmanage.') +@utils.service_type('volumev3') +def do_unmanage(cs, args): + """Stop managing a volume.""" + volume = utils.find_volume(cs, args.volume) + cs.volumes.unmanage(volume.id) + + +@utils.arg('volume', metavar='', + help='Name or ID of the volume to promote. ' + 'The volume should have the replica volume created with ' + 'source-replica argument.') +@utils.service_type('volumev3') +def do_replication_promote(cs, args): + """Promote a secondary volume to primary for a relationship.""" + volume = utils.find_volume(cs, args.volume) + cs.volumes.promote(volume.id) + + +@utils.arg('volume', metavar='', + help='Name or ID of the volume to reenable replication. ' + 'The replication-status of the volume should be inactive.') +@utils.service_type('volumev3') +def do_replication_reenable(cs, args): + """Sync the secondary volume with primary for a relationship.""" + volume = utils.find_volume(cs, args.volume) + cs.volumes.reenable(volume.id) + + +@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('volumev3') +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='', + help='Name or ID of a consistency group.') +@utils.service_type('volumev3') +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('volumetypes', + metavar='', + help='Volume types.') +@utils.arg('--name', + metavar='', + help='Name of a consistency group.') +@utils.arg('--description', + metavar='', + default=None, + help='Description of a consistency group. Default=None.') +@utils.arg('--availability-zone', + metavar='', + default=None, + help='Availability zone for volume. Default=None.') +@utils.service_type('volumev3') +def do_consisgroup_create(cs, args): + """Creates a consistency group.""" + + consistencygroup = cs.consistencygroups.create( + args.volumetypes, + args.name, + args.description, + 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('--cgsnapshot', + metavar='', + help='Name or ID of a cgsnapshot. Default=None.') +@utils.arg('--source-cg', + metavar='', + help='Name or ID of a source CG. Default=None.') +@utils.arg('--name', + metavar='', + help='Name of a consistency group. Default=None.') +@utils.arg('--description', + metavar='', + help='Description of a consistency group. Default=None.') +@utils.service_type('volumev3') +def do_consisgroup_create_from_src(cs, args): + """Creates a consistency group from a cgsnapshot or a source CG.""" + if not args.cgsnapshot and not args.source_cg: + msg = ('Cannot create consistency group because neither ' + 'cgsnapshot nor source CG is provided.') + raise exceptions.ClientException(code=1, message=msg) + if args.cgsnapshot and args.source_cg: + msg = ('Cannot create consistency group because both ' + 'cgsnapshot and source CG are provided.') + raise exceptions.ClientException(code=1, message=msg) + cgsnapshot = None + if args.cgsnapshot: + cgsnapshot = _find_cgsnapshot(cs, args.cgsnapshot) + source_cg = None + if args.source_cg: + source_cg = _find_consistencygroup(cs, args.source_cg) + info = cs.consistencygroups.create_from_src( + cgsnapshot.id if cgsnapshot else None, + source_cg.id if source_cg else None, + args.name, + args.description) + + info.pop('links', None) + utils.print_dict(info) + + +@utils.arg('consistencygroup', + metavar='', nargs='+', + help='Name or ID of one or more consistency groups ' + 'to be deleted.') +@utils.arg('--force', + action='store_true', + default=False, + 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.') +@utils.service_type('volumev3') +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 the specified " + "consistency groups.") + + +@utils.arg('consistencygroup', + metavar='', + help='Name or ID of a consistency group.') +@utils.arg('--name', metavar='', + help='New name for consistency group. Default=None.') +@utils.arg('--description', metavar='', + help='New description for consistency group. Default=None.') +@utils.arg('--add-volumes', + metavar='', + help='UUID of one or more volumes ' + 'to be added to the consistency group, ' + 'separated by commas. Default=None.') +@utils.arg('--remove-volumes', + metavar='', + help='UUID of one or more volumes ' + 'to be removed from the consistency group, ' + 'separated by commas. Default=None.') +@utils.service_type('volumev3') +def do_consisgroup_update(cs, args): + """Updates a consistencygroup.""" + kwargs = {} + + if args.name is not None: + kwargs['name'] = args.name + + if args.description is not None: + kwargs['description'] = args.description + + if args.add_volumes is not None: + kwargs['add_volumes'] = args.add_volumes + + if args.remove_volumes is not None: + kwargs['remove_volumes'] = args.remove_volumes + + if not kwargs: + msg = ('At least one of the following args must be supplied: ' + 'name, description, add-volumes, remove-volumes.') + raise exceptions.ClientException(code=1, message=msg) + + _find_consistencygroup(cs, args.consistencygroup).update(**kwargs) + + +@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='', + default=None, + help='Filters results by a status. Default=None.') +@utils.arg('--consistencygroup-id', + metavar='', + default=None, + help='Filters results by a consistency group ID. Default=None.') +@utils.service_type('volumev3') +def do_cgsnapshot_list(cs, args): + """Lists all cgsnapshots.""" + + 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='', + help='Name or ID of cgsnapshot.') +@utils.service_type('volumev3') +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='', + help='Name or ID of a consistency group.') +@utils.arg('--name', + metavar='', + default=None, + help='Cgsnapshot name. Default=None.') +@utils.arg('--description', + metavar='', + default=None, + help='Cgsnapshot description. Default=None.') +@utils.service_type('volumev3') +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='', nargs='+', + help='Name or ID of one or more cgsnapshots to be deleted.') +@utils.service_type('volumev3') +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 the specified " + "cgsnapshots.") + + +@utils.arg('--detail', + action='store_true', + help='Show detailed information about pools.') +@utils.service_type('volumev3') +def do_get_pools(cs, args): + """Show pool information for backends. Admin only.""" + pools = cs.volumes.get_pools(args.detail) + infos = dict() + infos.update(pools._info) + + for info in infos['pools']: + backend = dict() + backend['name'] = info['name'] + if args.detail: + backend.update(info['capabilities']) + utils.print_dict(backend) + + +@utils.arg('host', + metavar='', + help='Cinder host to show backend volume stats and properties; ' + 'takes the form: host@backend-name') +@utils.service_type('volumev3') +def do_get_capabilities(cs, args): + """Show backend volume stats and properties. Admin only.""" + + capabilities = cs.capabilities.get(args.host) + infos = dict() + infos.update(capabilities._info) + + prop = infos.pop('properties', None) + utils.print_dict(infos, "Volume stats") + utils.print_dict(prop, "Backend properties") + + +@utils.arg('volume', + metavar='', + help='Cinder volume already exists in volume backend') +@utils.arg('identifier', + metavar='', + help='Name or other Identifier for existing snapshot') +@utils.arg('--id-type', + metavar='', + default='source-name', + help='Type of backend device identifier provided, ' + 'typically source-name or source-id (Default=source-name)') +@utils.arg('--name', + metavar='', + help='Snapshot name (Default=None)') +@utils.arg('--description', + metavar='', + help='Snapshot description (Default=None)') +@utils.arg('--metadata', + type=str, + nargs='*', + metavar='', + help='Metadata key=value pairs (Default=None)') +@utils.service_type('volumev3') +def do_snapshot_manage(cs, args): + """Manage an existing snapshot.""" + snapshot_metadata = None + if args.metadata is not None: + snapshot_metadata = _extract_metadata(args) + + # Build a dictionary of key/value pairs to pass to the API. + ref_dict = {args.id_type: args.identifier} + + if hasattr(args, 'source_name') and args.source_name is not None: + ref_dict['source-name'] = args.source_name + if hasattr(args, 'source_id') and args.source_id is not None: + ref_dict['source-id'] = args.source_id + + volume = utils.find_volume(cs, args.volume) + snapshot = cs.volume_snapshots.manage(volume_id=volume.id, + ref=ref_dict, + name=args.name, + description=args.description, + metadata=snapshot_metadata) + + info = {} + snapshot = cs.volume_snapshots.get(snapshot.id) + info.update(snapshot._info) + info.pop('links', None) + utils.print_dict(info) + + +@utils.arg('snapshot', metavar='', + help='Name or ID of the snapshot to unmanage.') +@utils.service_type('volumev3') +def do_snapshot_unmanage(cs, args): + """Stop managing a snapshot.""" + snapshot = _find_volume_snapshot(cs, args.snapshot) + cs.volume_snapshots.unmanage(snapshot.id) + + +@utils.arg('host', metavar='', help='Host name.') +@utils.service_type('volumev3') +def do_freeze_host(cs, args): + """Freeze and disable the specified cinder-volume host.""" + cs.services.freeze_host(args.host) + + +@utils.arg('host', metavar='', help='Host name.') +@utils.service_type('volumev3') +def do_thaw_host(cs, args): + """Thaw and enable the specified cinder-volume host.""" + cs.services.thaw_host(args.host) + + +@utils.arg('host', metavar='', help='Host name.') +@utils.arg('--backend_id', + metavar='', + help='ID of backend to failover to (Default=None)') +@utils.service_type('volumev3') +def do_failover_host(cs, args): + """Failover a replicating cinder-volume host.""" + cs.services.failover_host(args.host, args.backend_id) diff --git a/cinderclient/v3/volume_backups.py b/cinderclient/v3/volume_backups.py new file mode 100644 index 000000000..b264dec89 --- /dev/null +++ b/cinderclient/v3/volume_backups.py @@ -0,0 +1,124 @@ +# Copyright (C) 2013 Hewlett-Packard Development Company, L.P. +# 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. + +""" +Volume Backups interface (v3 extension). +""" +from cinderclient import base +from cinderclient.openstack.common.apiclient import base as common_base + + +class VolumeBackup(base.Resource): + """A volume backup is a block level backup of a volume.""" + + def __repr__(self): + return "" % self.id + + def delete(self): + """Delete this volume backup.""" + return self.manager.delete(self) + + def reset_state(self, state): + return self.manager.reset_state(self, state) + + +class VolumeBackupManager(base.ManagerWithFind): + """Manage :class:`VolumeBackup` resources.""" + resource_class = VolumeBackup + + def create(self, volume_id, container=None, + name=None, description=None, + incremental=False, force=False, + snapshot_id=None): + """Creates a volume backup. + + :param volume_id: The ID of the volume to backup. + :param container: The name of the backup service container. + :param name: The name of the backup. + :param description: The description of the backup. + :param incremental: Incremental backup. + :param force: If True, allows an in-use volume to be backed up. + :rtype: :class:`VolumeBackup` + """ + body = {'backup': {'volume_id': volume_id, + 'container': container, + 'name': name, + 'description': description, + 'incremental': incremental, + 'force': force, + 'snapshot_id': snapshot_id, }} + return self._create('/backups', body, 'backup') + + def get(self, backup_id): + """Show volume backup details. + + :param backup_id: The ID of the backup to display. + :rtype: :class:`VolumeBackup` + """ + return self._get("/backups/%s" % backup_id, "backup") + + def list(self, detailed=True, search_opts=None, marker=None, limit=None, + sort=None): + """Get a list of all volume backups. + + :rtype: list of :class:`VolumeBackup` + """ + resource_type = "backups" + url = self._build_list_url(resource_type, detailed=detailed, + search_opts=search_opts, marker=marker, + limit=limit, sort=sort) + return self._list(url, resource_type, limit=limit) + + def delete(self, backup): + """Delete a volume backup. + + :param backup: The :class:`VolumeBackup` to delete. + """ + return self._delete("/backups/%s" % base.getid(backup)) + + def reset_state(self, backup, state): + """Update the specified volume backup with the provided state.""" + return self._action('os-reset_status', backup, {'status': state}) + + def _action(self, action, backup, info=None, **kwargs): + """Perform a volume backup action.""" + body = {action: info} + self.run_hooks('modify_body_for_action', body, **kwargs) + url = '/backups/%s/action' % base.getid(backup) + resp, body = self.api.client.post(url, body=body) + return common_base.TupleWithMeta((resp, body), resp) + + def export_record(self, backup_id): + """Export volume backup metadata record. + + :param backup_id: The ID of the backup to export. + :rtype: A dictionary containing 'backup_url' and 'backup_service'. + """ + resp, body = \ + self.api.client.get("/backups/%s/export_record" % backup_id) + return common_base.DictWithMeta(body['backup-record'], resp) + + def import_record(self, backup_service, backup_url): + """Import volume backup metadata record. + + :param backup_service: Backup service to use for importing the backup + :param backup_url: Backup URL for importing the backup metadata + :rtype: A dictionary containing volume backup metadata. + """ + body = {'backup-record': {'backup_service': backup_service, + 'backup_url': backup_url}} + self.run_hooks('modify_body_for_update', body, 'backup-record') + resp, body = self.api.client.post("/backups/import_record", body=body) + return common_base.DictWithMeta(body['backup'], resp) diff --git a/cinderclient/v3/volume_backups_restore.py b/cinderclient/v3/volume_backups_restore.py new file mode 100644 index 000000000..911356d03 --- /dev/null +++ b/cinderclient/v3/volume_backups_restore.py @@ -0,0 +1,43 @@ +# Copyright (C) 2013 Hewlett-Packard Development Company, L.P. +# 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. + +"""Volume Backups Restore interface (v3 extension). + +This is part of the Volume Backups interface. +""" + +from cinderclient import base + + +class VolumeBackupsRestore(base.Resource): + """A Volume Backups Restore represents a restore operation.""" + def __repr__(self): + return "" % self.volume_id + + +class VolumeBackupRestoreManager(base.Manager): + """Manage :class:`VolumeBackupsRestore` resources.""" + resource_class = VolumeBackupsRestore + + def restore(self, backup_id, volume_id=None): + """Restore a backup to a volume. + + :param backup_id: The ID of the backup to restore. + :param volume_id: The ID of the volume to restore the backup to. + :rtype: :class:`Restore` + """ + body = {'restore': {'volume_id': volume_id}} + return self._create("/backups/%s/restore" % backup_id, + body, "restore") diff --git a/cinderclient/v3/volume_encryption_types.py b/cinderclient/v3/volume_encryption_types.py new file mode 100644 index 000000000..c4c7a6981 --- /dev/null +++ b/cinderclient/v3/volume_encryption_types.py @@ -0,0 +1,104 @@ +# Copyright (c) 2013 The Johns Hopkins University/Applied Physics Laboratory +# 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. + + +""" +Volume Encryption Type interface +""" + +from cinderclient import base +from cinderclient.openstack.common.apiclient import base as common_base + + +class VolumeEncryptionType(base.Resource): + """ + A Volume Encryption Type is a collection of settings used to conduct + encryption for a specific volume type. + """ + def __repr__(self): + return "" % self.name + + +class VolumeEncryptionTypeManager(base.ManagerWithFind): + """ + Manage :class: `VolumeEncryptionType` resources. + """ + resource_class = VolumeEncryptionType + + def list(self, search_opts=None): + """ + List all volume encryption types. + + :param volume_types: a list of volume types + :return: a list of :class: VolumeEncryptionType instances + """ + # Since the encryption type is a volume type extension, we cannot get + # all encryption types without going through all volume types. + volume_types = self.api.volume_types.list() + encryption_types = [] + list_of_resp = [] + for volume_type in volume_types: + encryption_type = self._get("/types/%s/encryption" + % base.getid(volume_type)) + if hasattr(encryption_type, 'volume_type_id'): + encryption_types.append(encryption_type) + + list_of_resp.extend(encryption_type.request_ids) + + return common_base.ListWithMeta(encryption_types, list_of_resp) + + def get(self, volume_type): + """ + Get the volume encryption type for the specified volume type. + + :param volume_type: the volume type to query + :return: an instance of :class: VolumeEncryptionType + """ + return self._get("/types/%s/encryption" % base.getid(volume_type)) + + def create(self, volume_type, specs): + """ + Creates encryption type for a volume type. Default: admin only. + + :param volume_type: the volume type on which to add an encryption type + :param specs: the encryption type specifications to add + :return: an instance of :class: VolumeEncryptionType + """ + body = {'encryption': specs} + return self._create("/types/%s/encryption" % base.getid(volume_type), + body, "encryption") + + def update(self, volume_type, specs): + """ + Update the encryption type information for the specified volume type. + + :param volume_type: the volume type whose encryption type information + must be updated + :param specs: the encryption type specifications to update + :return: an instance of :class: VolumeEncryptionType + """ + body = {'encryption': specs} + return self._update("/types/%s/encryption/provider" % + base.getid(volume_type), body) + + def delete(self, volume_type): + """ + Delete the encryption type information for the specified volume type. + + :param volume_type: the volume type whose encryption type information + must be deleted + """ + return self._delete("/types/%s/encryption/provider" % + base.getid(volume_type)) diff --git a/cinderclient/v3/volume_snapshots.py b/cinderclient/v3/volume_snapshots.py new file mode 100644 index 000000000..0039b8dca --- /dev/null +++ b/cinderclient/v3/volume_snapshots.py @@ -0,0 +1,205 @@ +# Copyright (c) 2013 OpenStack Foundation +# 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. + +"""Volume snapshot interface (v3 extension).""" + +from cinderclient import base +from cinderclient.openstack.common.apiclient import base as common_base + + +class Snapshot(base.Resource): + """A Snapshot is a point-in-time snapshot of an openstack volume.""" + + def __repr__(self): + return "" % self.id + + def delete(self): + """Delete this snapshot.""" + return self.manager.delete(self) + + def update(self, **kwargs): + """Update the name or description for this snapshot.""" + return self.manager.update(self, **kwargs) + + @property + def progress(self): + return self._info.get('os-extended-snapshot-attributes:progress') + + @property + def project_id(self): + return self._info.get('os-extended-snapshot-attributes:project_id') + + def reset_state(self, state): + """Update the snapshot with the provided state.""" + return self.manager.reset_state(self, state) + + def set_metadata(self, metadata): + """Set metadata of this snapshot.""" + return self.manager.set_metadata(self, metadata) + + def delete_metadata(self, keys): + """Delete metadata of this snapshot.""" + return self.manager.delete_metadata(self, keys) + + def update_all_metadata(self, metadata): + """Update_all metadata of this snapshot.""" + return self.manager.update_all_metadata(self, metadata) + + def manage(self, volume_id, ref, name=None, description=None, + metadata=None): + """Manage an existing snapshot.""" + self.manager.manage(volume_id=volume_id, ref=ref, name=name, + description=description, metadata=metadata) + + def unmanage(self, snapshot): + """Unmanage a snapshot.""" + self.manager.unmanage(snapshot) + + +class SnapshotManager(base.ManagerWithFind): + """Manage :class:`Snapshot` resources.""" + resource_class = Snapshot + + def create(self, volume_id, force=False, + name=None, description=None, metadata=None): + + """Creates a snapshot of the given volume. + + :param volume_id: The ID of the volume to snapshot. + :param force: If force is True, create a snapshot even if the volume is + attached to an instance. Default is False. + :param name: Name of the snapshot + :param description: Description of the snapshot + :param metadata: Metadata of the snapshot + :rtype: :class:`Snapshot` + """ + + if metadata is None: + snapshot_metadata = {} + else: + snapshot_metadata = metadata + + body = {'snapshot': {'volume_id': volume_id, + 'force': force, + 'name': name, + 'description': description, + 'metadata': snapshot_metadata}} + return self._create('/snapshots', body, 'snapshot') + + def get(self, snapshot_id): + """Shows snapshot details. + + :param snapshot_id: The ID of the snapshot to get. + :rtype: :class:`Snapshot` + """ + return self._get("/snapshots/%s" % snapshot_id, "snapshot") + + def list(self, detailed=True, search_opts=None, marker=None, limit=None, + sort=None): + """Get a list of all snapshots. + + :rtype: list of :class:`Snapshot` + """ + resource_type = "snapshots" + url = self._build_list_url(resource_type, detailed=detailed, + search_opts=search_opts, marker=marker, + limit=limit, sort=sort) + return self._list(url, resource_type, limit=limit) + + def delete(self, snapshot): + """Delete a snapshot. + + :param snapshot: The :class:`Snapshot` to delete. + """ + return self._delete("/snapshots/%s" % base.getid(snapshot)) + + def update(self, snapshot, **kwargs): + """Update the name or description for a snapshot. + + :param snapshot: The :class:`Snapshot` to update. + """ + if not kwargs: + return + + body = {"snapshot": kwargs} + + return self._update("/snapshots/%s" % base.getid(snapshot), body) + + def reset_state(self, snapshot, state): + """Update the specified snapshot with the provided state.""" + return self._action('os-reset_status', snapshot, {'status': state}) + + def _action(self, action, snapshot, info=None, **kwargs): + """Perform a snapshot action.""" + body = {action: info} + self.run_hooks('modify_body_for_action', body, **kwargs) + url = '/snapshots/%s/action' % base.getid(snapshot) + resp, body = self.api.client.post(url, body=body) + return common_base.TupleWithMeta((resp, body), resp) + + def update_snapshot_status(self, snapshot, update_dict): + return self._action('os-update_snapshot_status', + base.getid(snapshot), update_dict) + + def set_metadata(self, snapshot, metadata): + """Update/Set a snapshots metadata. + + :param snapshot: The :class:`Snapshot`. + :param metadata: A list of keys to be set. + """ + body = {'metadata': metadata} + return self._create("/snapshots/%s/metadata" % base.getid(snapshot), + body, "metadata") + + def delete_metadata(self, snapshot, keys): + """Delete specified keys from snapshot metadata. + + :param snapshot: The :class:`Snapshot`. + :param keys: A list of keys to be removed. + """ + response_list = [] + snapshot_id = base.getid(snapshot) + for k in keys: + resp, body = self._delete("/snapshots/%s/metadata/%s" % + (snapshot_id, k)) + response_list.append(resp) + + return common_base.ListWithMeta([], response_list) + + def update_all_metadata(self, snapshot, metadata): + """Update_all snapshot metadata. + + :param snapshot: The :class:`Snapshot`. + :param metadata: A list of keys to be updated. + """ + body = {'metadata': metadata} + return self._update("/snapshots/%s/metadata" % base.getid(snapshot), + body) + + def manage(self, volume_id, ref, name=None, description=None, + metadata=None): + """Manage an existing snapshot.""" + body = {'snapshot': {'volume_id': volume_id, + 'ref': ref, + 'name': name, + 'description': description, + 'metadata': metadata + } + } + return self._create('/os-snapshot-manage', body, 'snapshot') + + def unmanage(self, snapshot): + """Unmanage a snapshot.""" + return self._action('os-unmanage', snapshot, None) diff --git a/cinderclient/v3/volume_transfers.py b/cinderclient/v3/volume_transfers.py new file mode 100644 index 000000000..40aa15a8b --- /dev/null +++ b/cinderclient/v3/volume_transfers.py @@ -0,0 +1,101 @@ +# Copyright (C) 2013 Hewlett-Packard Development Company, L.P. +# 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. + +""" +Volume transfer interface (v3 extension). +""" + +try: + from urllib import urlencode +except ImportError: + from urllib.parse import urlencode +import six +from cinderclient import base + + +class VolumeTransfer(base.Resource): + """Transfer a volume from one tenant to another""" + + def __repr__(self): + return "" % self.id + + def delete(self): + """Delete this volume transfer.""" + return self.manager.delete(self) + + +class VolumeTransferManager(base.ManagerWithFind): + """Manage :class:`VolumeTransfer` resources.""" + resource_class = VolumeTransfer + + def create(self, volume_id, name=None): + """Creates a volume transfer. + + :param volume_id: The ID of the volume to transfer. + :param name: The name of the transfer. + :rtype: :class:`VolumeTransfer` + """ + body = {'transfer': {'volume_id': volume_id, + 'name': name}} + return self._create('/os-volume-transfer', body, 'transfer') + + def accept(self, transfer_id, auth_key): + """Accept a volume transfer. + + :param transfer_id: The ID of the transfer to accept. + :param auth_key: The auth_key of the transfer. + :rtype: :class:`VolumeTransfer` + """ + body = {'accept': {'auth_key': auth_key}} + return self._create('/os-volume-transfer/%s/accept' % transfer_id, + body, 'transfer') + + def get(self, transfer_id): + """Show details of a volume transfer. + + :param transfer_id: The ID of the volume transfer to display. + :rtype: :class:`VolumeTransfer` + """ + return self._get("/os-volume-transfer/%s" % transfer_id, "transfer") + + def list(self, detailed=True, search_opts=None): + """Get a list of all volume transfer. + + :rtype: list of :class:`VolumeTransfer` + """ + 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("/os-volume-transfer%s%s" % (detail, query_string), + "transfers") + + def delete(self, transfer_id): + """Delete a volume transfer. + + :param transfer_id: The :class:`VolumeTransfer` to delete. + """ + return self._delete("/os-volume-transfer/%s" % base.getid(transfer_id)) diff --git a/cinderclient/v3/volume_type_access.py b/cinderclient/v3/volume_type_access.py new file mode 100644 index 000000000..abdfa8658 --- /dev/null +++ b/cinderclient/v3/volume_type_access.py @@ -0,0 +1,53 @@ +# Copyright 2014 OpenStack Foundation +# +# 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. + +"""Volume type access interface.""" + +from cinderclient import base +from cinderclient.openstack.common.apiclient import base as common_base + + +class VolumeTypeAccess(base.Resource): + def __repr__(self): + return "" % self.project_id + + +class VolumeTypeAccessManager(base.ManagerWithFind): + """ + Manage :class:`VolumeTypeAccess` resources. + """ + resource_class = VolumeTypeAccess + + def list(self, volume_type): + return self._list( + '/types/%s/os-volume-type-access' % base.getid(volume_type), + 'volume_type_access') + + def add_project_access(self, volume_type, project): + """Add a project to the given volume type access list.""" + info = {'project': project} + return self._action('addProjectAccess', volume_type, info) + + def remove_project_access(self, volume_type, project): + """Remove a project from the given volume type access list.""" + info = {'project': project} + return self._action('removeProjectAccess', volume_type, info) + + def _action(self, action, volume_type, info, **kwargs): + """Perform a volume type action.""" + body = {action: info} + self.run_hooks('modify_body_for_action', body, **kwargs) + url = '/types/%s/action' % base.getid(volume_type) + resp, body = self.api.client.post(url, body=body) + return common_base.TupleWithMeta((resp, body), resp) diff --git a/cinderclient/v3/volume_types.py b/cinderclient/v3/volume_types.py new file mode 100644 index 000000000..251e75b9f --- /dev/null +++ b/cinderclient/v3/volume_types.py @@ -0,0 +1,151 @@ +# Copyright (c) 2013 OpenStack Foundation +# +# 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. + + +"""Volume Type interface.""" + +from cinderclient import base + + +class VolumeType(base.Resource): + """A Volume Type is the type of volume to be created.""" + def __repr__(self): + return "" % self.name + + @property + def is_public(self): + """ + Provide a user-friendly accessor to os-volume-type-access:is_public + """ + return self._info.get("os-volume-type-access:is_public", + self._info.get("is_public", 'N/A')) + + def get_keys(self): + """Get extra specs from a volume type. + + :param vol_type: The :class:`VolumeType` to get extra specs from + """ + _resp, body = self.manager.api.client.get( + "/types/%s/extra_specs" % + base.getid(self)) + return body["extra_specs"] + + def set_keys(self, metadata): + """Set extra specs on a volume type. + + :param type : The :class:`VolumeType` to set extra spec on + :param metadata: A dict of key/value pairs to be set + """ + body = {'extra_specs': metadata} + return self.manager._create( + "/types/%s/extra_specs" % base.getid(self), + body, + "extra_specs", + return_raw=True) + + def unset_keys(self, keys): + """Unset extra specs on a volue type. + + :param type_id: The :class:`VolumeType` to unset extra spec on + :param keys: A list of keys to be unset + """ + + # NOTE(jdg): This wasn't actually doing all of the keys before + # the return in the loop resulted in ony ONE key being unset. + # since on success the return was NONE, we'll only interrupt the loop + # and return if there's an error + for k in keys: + resp = self.manager._delete( + "/types/%s/extra_specs/%s" % ( + base.getid(self), k)) + if resp is not None: + return resp + + +class VolumeTypeManager(base.ManagerWithFind): + """Manage :class:`VolumeType` resources.""" + resource_class = VolumeType + + def list(self, search_opts=None, is_public=None): + """Lists all volume types. + + :rtype: list of :class:`VolumeType`. + """ + query_string = '' + if not is_public: + query_string = '?is_public=%s' % is_public + return self._list("/types%s" % (query_string), "volume_types") + + def get(self, volume_type): + """Get a specific volume type. + + :param volume_type: The ID of the :class:`VolumeType` to get. + :rtype: :class:`VolumeType` + """ + return self._get("/types/%s" % base.getid(volume_type), "volume_type") + + def default(self): + """Get the default volume type. + + :rtype: :class:`VolumeType` + """ + return self._get("/types/default", "volume_type") + + def delete(self, volume_type): + """Deletes a specific volume_type. + + :param volume_type: The name or ID of the :class:`VolumeType` to get. + """ + return self._delete("/types/%s" % base.getid(volume_type)) + + def create(self, name, description=None, is_public=True): + """Creates a volume type. + + :param name: Descriptive name of the volume type + :param description: Description of the the volume type + :param is_public: Volume type visibility + :rtype: :class:`VolumeType` + """ + + body = { + "volume_type": { + "name": name, + "description": description, + "os-volume-type-access:is_public": is_public, + } + } + + return self._create("/types", body, "volume_type") + + def update(self, volume_type, name=None, description=None, is_public=None): + """Update the name and/or description for a volume type. + + :param volume_type: The ID of the :class:`VolumeType` to update. + :param name: Descriptive name of the volume type. + :param description: Description of the the volume type. + :rtype: :class:`VolumeType` + """ + + body = { + "volume_type": { + "name": name, + "description": description + } + } + if is_public is not None: + body["volume_type"]["is_public"] = is_public + + return self._update("/types/%s" % base.getid(volume_type), + body, response_key="volume_type") diff --git a/cinderclient/v3/volumes.py b/cinderclient/v3/volumes.py new file mode 100644 index 000000000..f8d2432e7 --- /dev/null +++ b/cinderclient/v3/volumes.py @@ -0,0 +1,604 @@ +# Copyright (c) 2013 OpenStack Foundation +# 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. + +"""Volume interface (v3 extension).""" + +from cinderclient import base +from cinderclient.openstack.common.apiclient import base as common_base + + +class Volume(base.Resource): + """A volume is an extra block level storage to the OpenStack instances.""" + def __repr__(self): + return "" % self.id + + def delete(self, cascade=False): + """Delete this volume.""" + return self.manager.delete(self, cascade=cascade) + + def update(self, **kwargs): + """Update the name or description for this volume.""" + return self.manager.update(self, **kwargs) + + def attach(self, instance_uuid, mountpoint, mode='rw', host_name=None): + """Set attachment metadata. + + :param instance_uuid: uuid of the attaching instance. + :param mountpoint: mountpoint on the attaching instance or host. + :param mode: the access mode. + :param host_name: name of the attaching host. + """ + return self.manager.attach(self, instance_uuid, mountpoint, mode, + host_name) + + def detach(self): + """Clear attachment metadata.""" + return self.manager.detach(self) + + def reserve(self, volume): + """Reserve this volume.""" + return self.manager.reserve(self) + + def unreserve(self, volume): + """Unreserve this volume.""" + return self.manager.unreserve(self) + + def begin_detaching(self, volume): + """Begin detaching volume.""" + return self.manager.begin_detaching(self) + + def roll_detaching(self, volume): + """Roll detaching volume.""" + return self.manager.roll_detaching(self) + + def initialize_connection(self, volume, connector): + """Initialize a volume connection. + + :param connector: connector dict from nova. + """ + return self.manager.initialize_connection(self, connector) + + def terminate_connection(self, volume, connector): + """Terminate a volume connection. + + :param connector: connector dict from nova. + """ + return self.manager.terminate_connection(self, connector) + + def set_metadata(self, volume, metadata): + """Set or Append metadata to a volume. + + :param volume : The :class: `Volume` to set metadata on + :param metadata: A dict of key/value pairs to set + """ + return self.manager.set_metadata(self, metadata) + + def set_image_metadata(self, volume, metadata): + """Set a volume's image metadata. + + :param volume : The :class: `Volume` to set metadata on + :param metadata: A dict of key/value pairs to set + """ + return self.manager.set_image_metadata(self, volume, metadata) + + def delete_image_metadata(self, volume, keys): + """Delete specified keys from volume's image metadata. + + :param volume: The :class:`Volume`. + :param keys: A list of keys to be removed. + """ + return self.manager.delete_image_metadata(self, volume, keys) + + def show_image_metadata(self, volume): + """Show a volume's image metadata. + + :param volume : The :class: `Volume` where the image metadata + associated. + """ + return self.manager.show_image_metadata(self) + + def upload_to_image(self, force, image_name, container_format, + disk_format): + """Upload a volume to image service as an image.""" + return self.manager.upload_to_image(self, force, image_name, + container_format, disk_format) + + def force_delete(self): + """Delete the specified volume ignoring its current state. + + :param volume: The UUID of the volume to force-delete. + """ + return self.manager.force_delete(self) + + def reset_state(self, state, attach_status=None, migration_status=None): + """Update the volume with the provided state. + + :param state: The state of the volume to set. + :param attach_status: The attach_status of the volume to be set, + or None to keep the current status. + :param migration_status: The migration_status of the volume to be set, + or None to keep the current status. + """ + return self.manager.reset_state(self, state, attach_status, + migration_status) + + def extend(self, volume, new_size): + """Extend the size of the specified volume. + + :param volume: The UUID of the volume to extend + :param new_size: The desired size to extend volume to. + """ + return self.manager.extend(self, new_size) + + def migrate_volume(self, host, force_host_copy, lock_volume): + """Migrate the volume to a new host.""" + return self.manager.migrate_volume(self, host, force_host_copy, + lock_volume) + + def retype(self, volume_type, policy): + """Change a volume's type.""" + return self.manager.retype(self, volume_type, policy) + + def update_all_metadata(self, metadata): + """Update all metadata of this volume.""" + return self.manager.update_all_metadata(self, metadata) + + def update_readonly_flag(self, volume, read_only): + """Update the read-only access mode flag of the specified volume. + + :param volume: The UUID of the volume to update. + :param read_only: The value to indicate whether to update volume to + read-only access mode. + """ + return self.manager.update_readonly_flag(self, read_only) + + def manage(self, host, ref, name=None, description=None, + volume_type=None, availability_zone=None, metadata=None, + bootable=False): + """Manage an existing volume.""" + return self.manager.manage(host=host, ref=ref, name=name, + description=description, + volume_type=volume_type, + availability_zone=availability_zone, + metadata=metadata, bootable=bootable) + + def unmanage(self, volume): + """Unmanage a volume.""" + return self.manager.unmanage(volume) + + def promote(self, volume): + """Promote secondary to be primary in relationship.""" + return self.manager.promote(volume) + + def reenable(self, volume): + """Sync the secondary volume with primary for a relationship.""" + return self.manager.reenable(volume) + + def get_pools(self, detail): + """Show pool information for backends.""" + return self.manager.get_pools(detail) + + +class VolumeManager(base.ManagerWithFind): + """Manage :class:`Volume` resources.""" + resource_class = Volume + + 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, + source_replica=None, multiattach=False): + """Create 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 + :param volume_type: Type 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 + :param metadata: Optional metadata to set on volume creation + :param imageRef: reference to an image stored in glance + :param source_volid: ID of source volume to clone from + :param source_replica: ID of source volume to clone replica + :param scheduler_hints: (optional extension) arbitrary key-value pairs + specified by the client to help boot an instance + :param multiattach: Allow the volume to be attached to more than + one instance + :rtype: :class:`Volume` + """ + if metadata is None: + volume_metadata = {} + else: + volume_metadata = metadata + + body = {'volume': {'size': size, + 'consistencygroup_id': consistencygroup_id, + 'snapshot_id': snapshot_id, + 'name': name, + 'description': description, + 'volume_type': volume_type, + 'user_id': user_id, + 'project_id': project_id, + 'availability_zone': availability_zone, + 'status': "creating", + 'attach_status': "detached", + 'metadata': volume_metadata, + 'imageRef': imageRef, + 'source_volid': source_volid, + 'source_replica': source_replica, + 'multiattach': multiattach, + }} + + if scheduler_hints: + body['OS-SCH-HNT:scheduler_hints'] = scheduler_hints + + return self._create('/volumes', body, 'volume') + + def get(self, volume_id): + """Get a volume. + + :param volume_id: The ID of the volume to get. + :rtype: :class:`Volume` + """ + return self._get("/volumes/%s" % volume_id, "volume") + + def list(self, detailed=True, search_opts=None, marker=None, limit=None, + sort_key=None, sort_dir=None, sort=None): + """Lists all volumes. + + :param detailed: Whether to return detailed volume info. + :param search_opts: Search options to filter out volumes. + :param marker: Begin returning volumes that appear later in the volume + list than that represented by this volume id. + :param limit: Maximum number of volumes to return. + :param sort_key: Key to be sorted; deprecated in kilo + :param sort_dir: Sort direction, should be 'desc' or 'asc'; deprecated + in kilo + :param sort: Sort information + :rtype: list of :class:`Volume` + """ + + resource_type = "volumes" + url = self._build_list_url(resource_type, detailed=detailed, + search_opts=search_opts, marker=marker, + limit=limit, sort_key=sort_key, + sort_dir=sort_dir, sort=sort) + return self._list(url, resource_type, limit=limit) + + def delete(self, volume, cascade=False): + """Delete a volume. + + :param volume: The :class:`Volume` to delete. + :param cascade: Also delete dependent snapshots. + """ + + loc = "/volumes/%s" % base.getid(volume) + + if cascade: + loc += '?cascade=True' + + return self._delete(loc) + + def update(self, volume, **kwargs): + """Update the name or description for a volume. + + :param volume: The :class:`Volume` to update. + """ + if not kwargs: + return + + body = {"volume": kwargs} + + return self._update("/volumes/%s" % base.getid(volume), body) + + def _action(self, action, volume, info=None, **kwargs): + """Perform a volume "action." + """ + body = {action: info} + self.run_hooks('modify_body_for_action', body, **kwargs) + url = '/volumes/%s/action' % base.getid(volume) + resp, body = self.api.client.post(url, body=body) + return common_base.TupleWithMeta((resp, body), resp) + + def attach(self, volume, instance_uuid, mountpoint, mode='rw', + host_name=None): + """Set attachment metadata. + + :param volume: The :class:`Volume` (or its ID) + you would like to attach. + :param instance_uuid: uuid of the attaching instance. + :param mountpoint: mountpoint on the attaching instance or host. + :param mode: the access mode. + :param host_name: name of the attaching host. + """ + body = {'mountpoint': mountpoint, 'mode': mode} + if instance_uuid is not None: + body.update({'instance_uuid': instance_uuid}) + if host_name is not None: + body.update({'host_name': host_name}) + return self._action('os-attach', volume, body) + + def detach(self, volume, attachment_uuid=None): + """Clear attachment metadata. + + :param volume: The :class:`Volume` (or its ID) + you would like to detach. + :param attachment_uuid: The uuid of the volume attachment. + """ + return self._action('os-detach', volume, + {'attachment_id': attachment_uuid}) + + def reserve(self, volume): + """Reserve this volume. + + :param volume: The :class:`Volume` (or its ID) + you would like to reserve. + """ + return self._action('os-reserve', volume) + + def unreserve(self, volume): + """Unreserve this volume. + + :param volume: The :class:`Volume` (or its ID) + you would like to unreserve. + """ + return self._action('os-unreserve', volume) + + def begin_detaching(self, volume): + """Begin detaching this volume. + + :param volume: The :class:`Volume` (or its ID) + you would like to detach. + """ + return self._action('os-begin_detaching', volume) + + def roll_detaching(self, volume): + """Roll detaching this volume. + + :param volume: The :class:`Volume` (or its ID) + you would like to roll detaching. + """ + return self._action('os-roll_detaching', volume) + + def initialize_connection(self, volume, connector): + """Initialize a volume connection. + + :param volume: The :class:`Volume` (or its ID). + :param connector: connector dict from nova. + """ + resp, body = self._action('os-initialize_connection', volume, + {'connector': connector}) + return common_base.DictWithMeta(body['connection_info'], resp) + + def terminate_connection(self, volume, connector): + """Terminate a volume connection. + + :param volume: The :class:`Volume` (or its ID). + :param connector: connector dict from nova. + """ + return self._action('os-terminate_connection', volume, + {'connector': connector}) + + def set_metadata(self, volume, metadata): + """Update/Set a volumes metadata. + + :param volume: The :class:`Volume`. + :param metadata: A list of keys to be set. + """ + body = {'metadata': metadata} + return self._create("/volumes/%s/metadata" % base.getid(volume), + body, "metadata") + + def delete_metadata(self, volume, keys): + """Delete specified keys from volumes metadata. + + :param volume: The :class:`Volume`. + :param keys: A list of keys to be removed. + """ + response_list = [] + for k in keys: + resp, body = self._delete("/volumes/%s/metadata/%s" % + (base.getid(volume), k)) + response_list.append(resp) + + return common_base.ListWithMeta([], response_list) + + def set_image_metadata(self, volume, metadata): + """Set a volume's image metadata. + + :param volume: The :class:`Volume`. + :param metadata: keys and the values to be set with. + :type metadata: dict + """ + return self._action("os-set_image_metadata", volume, + {'metadata': metadata}) + + def delete_image_metadata(self, volume, keys): + """Delete specified keys from volume's image metadata. + + :param volume: The :class:`Volume`. + :param keys: A list of keys to be removed. + """ + response_list = [] + for key in keys: + resp, body = self._action("os-unset_image_metadata", volume, + {'key': key}) + response_list.append(resp) + + return common_base.ListWithMeta([], response_list) + + def show_image_metadata(self, volume): + """Show a volume's image metadata. + + :param volume : The :class: `Volume` where the image metadata + associated. + """ + return self._action("os-show_image_metadata", volume) + + def upload_to_image(self, volume, force, image_name, container_format, + disk_format): + """Upload volume to image service as image. + + :param volume: The :class:`Volume` to upload. + """ + return self._action('os-volume_upload_image', + volume, + {'force': force, + 'image_name': image_name, + 'container_format': container_format, + 'disk_format': disk_format}) + + def force_delete(self, volume): + """Delete the specified volume ignoring its current state. + + :param volume: The :class:`Volume` to force-delete. + """ + return self._action('os-force_delete', base.getid(volume)) + + def reset_state(self, volume, state, attach_status=None, + migration_status=None): + """Update the provided volume with the provided state. + + :param volume: The :class:`Volume` to set the state. + :param state: The state of the volume to be set. + :param attach_status: The attach_status of the volume to be set, + or None to keep the current status. + :param migration_status: The migration_status of the volume to be set, + or None to keep the current status. + """ + body = {'status': state} + if attach_status: + body.update({'attach_status': attach_status}) + if migration_status: + body.update({'migration_status': migration_status}) + return self._action('os-reset_status', volume, body) + + def extend(self, volume, new_size): + """Extend the size of the specified volume. + + :param volume: The UUID of the volume to extend. + :param new_size: The requested size to extend volume to. + """ + return self._action('os-extend', + base.getid(volume), + {'new_size': new_size}) + + def get_encryption_metadata(self, volume_id): + """ + Retrieve the encryption metadata from the desired volume. + + :param volume_id: the id of the volume to query + :return: a dictionary of volume encryption metadata + """ + metadata = self._get("/volumes/%s/encryption" % volume_id) + return common_base.DictWithMeta(metadata._info, metadata.request_ids) + + def migrate_volume(self, volume, host, force_host_copy, lock_volume): + """Migrate volume to new host. + + :param volume: The :class:`Volume` to migrate + :param host: The destination host + :param force_host_copy: Skip driver optimizations + :param lock_volume: Lock the volume and guarantee the migration + to finish + """ + return self._action('os-migrate_volume', + volume, + {'host': host, 'force_host_copy': force_host_copy, + 'lock_volume': lock_volume}) + + def migrate_volume_completion(self, old_volume, new_volume, error): + """Complete the migration from the old volume to the temp new one. + + :param old_volume: The original :class:`Volume` in the migration + :param new_volume: The new temporary :class:`Volume` in the migration + :param error: Inform of an error to cause migration cleanup + """ + new_volume_id = base.getid(new_volume) + resp, body = self._action('os-migrate_volume_completion', old_volume, + {'new_volume': new_volume_id, + 'error': error}) + return common_base.DictWithMeta(body, resp) + + def update_all_metadata(self, volume, metadata): + """Update all metadata of a volume. + + :param volume: The :class:`Volume`. + :param metadata: A list of keys to be updated. + """ + body = {'metadata': metadata} + return self._update("/volumes/%s/metadata" % base.getid(volume), + body) + + def update_readonly_flag(self, volume, flag): + return self._action('os-update_readonly_flag', + base.getid(volume), + {'readonly': flag}) + + def retype(self, volume, volume_type, policy): + """Change a volume's type. + + :param volume: The :class:`Volume` to retype + :param volume_type: New volume type + :param policy: Policy for migration during the retype + """ + return self._action('os-retype', + volume, + {'new_type': volume_type, + 'migration_policy': policy}) + + def set_bootable(self, volume, flag): + return self._action('os-set_bootable', + base.getid(volume), + {'bootable': flag}) + + def manage(self, host, ref, name=None, description=None, + volume_type=None, availability_zone=None, metadata=None, + bootable=False): + """Manage an existing volume.""" + body = {'volume': {'host': host, + 'ref': ref, + 'name': name, + 'description': description, + 'volume_type': volume_type, + 'availability_zone': availability_zone, + 'metadata': metadata, + 'bootable': bootable + }} + return self._create('/os-volume-manage', body, 'volume') + + def unmanage(self, volume): + """Unmanage a volume.""" + return self._action('os-unmanage', volume, None) + + def promote(self, volume): + """Promote secondary to be primary in relationship.""" + return self._action('os-promote-replica', volume, None) + + def reenable(self, volume): + """Sync the secondary volume with primary for a relationship.""" + return self._action('os-reenable-replica', volume, None) + + def get_pools(self, detail): + """Show pool information for backends.""" + query_string = "" + if detail: + query_string = "?detail=True" + + return self._get('/scheduler-stats/get_pools%s' % query_string, None)