diff options
Diffstat (limited to 'openstackclient/volume')
| -rw-r--r-- | openstackclient/volume/client.py | 61 | ||||
| -rw-r--r-- | openstackclient/volume/v1/volume.py | 10 | ||||
| -rw-r--r-- | openstackclient/volume/v2/volume_backup.py | 300 | ||||
| -rw-r--r-- | openstackclient/volume/v2/volume_transfer_request.py | 36 | ||||
| -rw-r--r-- | openstackclient/volume/v3/volume_attachment.py | 511 | ||||
| -rw-r--r-- | openstackclient/volume/v3/volume_group.py | 506 | ||||
| -rw-r--r-- | openstackclient/volume/v3/volume_group_snapshot.py | 234 | ||||
| -rw-r--r-- | openstackclient/volume/v3/volume_group_type.py | 410 | ||||
| -rw-r--r-- | openstackclient/volume/v3/volume_message.py | 165 |
9 files changed, 2180 insertions, 53 deletions
diff --git a/openstackclient/volume/client.py b/openstackclient/volume/client.py index 64e8b9f3..0712fa7b 100644 --- a/openstackclient/volume/client.py +++ b/openstackclient/volume/client.py @@ -15,6 +15,7 @@ import logging +from osc_lib import exceptions from osc_lib import utils from openstackclient.i18n import _ @@ -29,9 +30,11 @@ API_VERSIONS = { "1": "cinderclient.v1.client.Client", "2": "cinderclient.v2.client.Client", "3": "cinderclient.v3.client.Client", - "3.42": "cinderclient.v3.client.Client", } +# Save the microversion if in use +_volume_api_version = None + def make_client(instance): """Returns a volume service client.""" @@ -42,16 +45,23 @@ def make_client(instance): from cinderclient.v3 import volume_snapshots from cinderclient.v3 import volumes - # Try a small import to check if cinderclient v1 is supported + # Check whether the available cinderclient supports v1 or v2 try: from cinderclient.v1 import services # noqa except Exception: del API_VERSIONS['1'] + try: + from cinderclient.v2 import services # noqa + except Exception: + del API_VERSIONS['2'] - version = instance._api_version[API_NAME] - from cinderclient import api_versions - # convert to APIVersion object - version = api_versions.get_api_version(version) + if _volume_api_version is not None: + version = _volume_api_version + else: + version = instance._api_version[API_NAME] + from cinderclient import api_versions + # convert to APIVersion object + version = api_versions.get_api_version(version) if version.ver_major == '1': # Monkey patch for v1 cinderclient @@ -99,3 +109,42 @@ def build_option_parser(parser): '(Env: OS_VOLUME_API_VERSION)') % DEFAULT_API_VERSION ) return parser + + +def check_api_version(check_version): + """Validate version supplied by user + + Returns: + + * True if version is OK + * False if the version has not been checked and the previous plugin + check should be performed + * throws an exception if the version is no good + """ + + # Defer client imports until we actually need them + from cinderclient import api_versions + + global _volume_api_version + + # Copy some logic from novaclient 3.3.0 for basic version detection + # NOTE(dtroyer): This is only enough to resume operations using API + # version 3.0 or any valid version supplied by the user. + _volume_api_version = api_versions.get_api_version(check_version) + + # Bypass X.latest format microversion + if not _volume_api_version.is_latest(): + if _volume_api_version > api_versions.APIVersion("3.0"): + if not _volume_api_version.matches( + api_versions.MIN_VERSION, + api_versions.MAX_VERSION, + ): + msg = _("versions supported by client: %(min)s - %(max)s") % { + "min": api_versions.MIN_VERSION, + "max": api_versions.MAX_VERSION, + } + raise exceptions.CommandError(msg) + + return True + + return False diff --git a/openstackclient/volume/v1/volume.py b/openstackclient/volume/v1/volume.py index 460bd85a..dfbb0c54 100644 --- a/openstackclient/volume/v1/volume.py +++ b/openstackclient/volume/v1/volume.py @@ -328,6 +328,13 @@ class ListVolume(command.Lister): help=_('List additional fields in output'), ) parser.add_argument( + '--offset', + type=int, + action=parseractions.NonNegativeAction, + metavar='<offset>', + help=_('Index from which to start listing volumes'), + ) + parser.add_argument( '--limit', type=int, action=parseractions.NonNegativeAction, @@ -395,6 +402,9 @@ class ListVolume(command.Lister): 'status': parsed_args.status, } + if parsed_args.offset: + search_opts['offset'] = parsed_args.offset + data = volume_client.volumes.list( search_opts=search_opts, limit=parsed_args.limit, diff --git a/openstackclient/volume/v2/volume_backup.py b/openstackclient/volume/v2/volume_backup.py index c336f6c9..96b22a68 100644 --- a/openstackclient/volume/v2/volume_backup.py +++ b/openstackclient/volume/v2/volume_backup.py @@ -18,6 +18,7 @@ import copy import functools import logging +from cinderclient import api_versions from cliff import columns as cliff_columns from osc_lib.cli import parseractions from osc_lib.command import command @@ -61,7 +62,7 @@ class CreateVolumeBackup(command.ShowOne): _description = _("Create new volume backup") def get_parser(self, prog_name): - parser = super(CreateVolumeBackup, self).get_parser(prog_name) + parser = super().get_parser(prog_name) parser.add_argument( "volume", metavar="<volume>", @@ -99,16 +100,67 @@ class CreateVolumeBackup(command.ShowOne): default=False, help=_("Perform an incremental backup") ) + parser.add_argument( + '--no-incremental', + action='store_false', + help=_("Do not perform an incremental backup") + ) + parser.add_argument( + '--property', + metavar='<key=value>', + action=parseractions.KeyValueAction, + dest='properties', + help=_( + 'Set a property on this backup ' + '(repeat option to remove multiple values) ' + '(supported by --os-volume-api-version 3.43 or above)' + ), + ) + parser.add_argument( + '--availability-zone', + metavar='<zone-name>', + help=_( + 'AZ where the backup should be stored; by default it will be ' + 'the same as the source ' + '(supported by --os-volume-api-version 3.51 or above)' + ), + ) return parser def take_action(self, parsed_args): volume_client = self.app.client_manager.volume + volume_id = utils.find_resource( - volume_client.volumes, parsed_args.volume).id - snapshot_id = None + volume_client.volumes, parsed_args.volume, + ).id + + kwargs = {} + if parsed_args.snapshot: - snapshot_id = utils.find_resource( - volume_client.volume_snapshots, parsed_args.snapshot).id + kwargs['snapshot_id'] = utils.find_resource( + volume_client.volume_snapshots, parsed_args.snapshot, + ).id + + if parsed_args.properties: + if volume_client.api_version < api_versions.APIVersion('3.43'): + msg = _( + '--os-volume-api-version 3.43 or greater is required to ' + 'support the --property option' + ) + raise exceptions.CommandError(msg) + + kwargs['metadata'] = parsed_args.properties + + if parsed_args.availability_zone: + if volume_client.api_version < api_versions.APIVersion('3.51'): + msg = _( + '--os-volume-api-version 3.51 or greater is required to ' + 'support the --availability-zone option' + ) + raise exceptions.CommandError(msg) + + kwargs['availability_zone'] = parsed_args.availability_zone + backup = volume_client.backups.create( volume_id, container=parsed_args.container, @@ -116,7 +168,7 @@ class CreateVolumeBackup(command.ShowOne): description=parsed_args.description, force=parsed_args.force, incremental=parsed_args.incremental, - snapshot_id=snapshot_id, + **kwargs, ) backup._info.pop("links", None) return zip(*sorted(backup._info.items())) @@ -148,7 +200,8 @@ class DeleteVolumeBackup(command.Command): for i in parsed_args.backups: try: backup_id = utils.find_resource( - volume_client.backups, i).id + volume_client.backups, i, + ).id volume_client.backups.delete(backup_id, parsed_args.force) except Exception as e: result += 1 @@ -158,8 +211,9 @@ class DeleteVolumeBackup(command.Command): if result > 0: total = len(parsed_args.backups) - msg = (_("%(result)s of %(total)s backups failed " - "to delete.") % {'result': result, 'total': total}) + msg = _("%(result)s of %(total)s backups failed to delete.") % { + 'result': result, 'total': total, + } raise exceptions.CommandError(msg) @@ -182,17 +236,22 @@ class ListVolumeBackup(command.Lister): parser.add_argument( "--status", metavar="<status>", - choices=['creating', 'available', 'deleting', - 'error', 'restoring', 'error_restoring'], - help=_("Filters results by the backup status " - "('creating', 'available', 'deleting', " - "'error', 'restoring' or 'error_restoring')") + choices=[ + 'creating', 'available', 'deleting', + 'error', 'restoring', 'error_restoring', + ], + help=_( + "Filters results by the backup status, one of: " + "creating, available, deleting, error, restoring or " + "error_restoring" + ), ) parser.add_argument( "--volume", metavar="<volume>", - help=_("Filters results by the volume which they " - "backup (name or ID)") + help=_( + "Filters results by the volume which they backup (name or ID)" + ), ) parser.add_argument( '--marker', @@ -212,19 +271,30 @@ class ListVolumeBackup(command.Lister): default=False, help=_('Include all projects (admin only)'), ) + # TODO(stephenfin): Add once we have an equivalent command for + # 'cinder list-filters' + # parser.add_argument( + # '--filter', + # metavar='<key=value>', + # action=parseractions.KeyValueAction, + # dest='filters', + # help=_( + # "Filter key and value pairs. Use 'foo' to " + # "check enabled filters from server. Use 'key~=value' for " + # "inexact filtering if the key supports " + # "(supported by --os-volume-api-version 3.33 or above)" + # ), + # ) return parser def take_action(self, parsed_args): volume_client = self.app.client_manager.volume + columns = ('id', 'name', 'description', 'status', 'size') + column_headers = ('ID', 'Name', 'Description', 'Status', 'Size') if parsed_args.long: - columns = ['ID', 'Name', 'Description', 'Status', 'Size', - 'Availability Zone', 'Volume ID', 'Container'] - column_headers = copy.deepcopy(columns) - column_headers[6] = 'Volume' - else: - columns = ['ID', 'Name', 'Description', 'Status', 'Size'] - column_headers = columns + columns += ('availability_zone', 'volume_id', 'container') + column_headers += ('Availability Zone', 'Volume', 'Container') # Cache the volume list volume_cache = {} @@ -234,17 +304,22 @@ class ListVolumeBackup(command.Lister): except Exception: # Just forget it if there's any trouble pass - _VolumeIdColumn = functools.partial(VolumeIdColumn, - volume_cache=volume_cache) + + _VolumeIdColumn = functools.partial( + VolumeIdColumn, volume_cache=volume_cache) filter_volume_id = None if parsed_args.volume: - filter_volume_id = utils.find_resource(volume_client.volumes, - parsed_args.volume).id + filter_volume_id = utils.find_resource( + volume_client.volumes, parsed_args.volume, + ).id + marker_backup_id = None if parsed_args.marker: - marker_backup_id = utils.find_resource(volume_client.backups, - parsed_args.marker).id + marker_backup_id = utils.find_resource( + volume_client.backups, parsed_args.marker, + ).id + search_opts = { 'name': parsed_args.name, 'status': parsed_args.status, @@ -257,11 +332,14 @@ class ListVolumeBackup(command.Lister): limit=parsed_args.limit, ) - return (column_headers, - (utils.get_item_properties( - s, columns, - formatters={'Volume ID': _VolumeIdColumn}, - ) for s in data)) + return ( + column_headers, + ( + utils.get_item_properties( + s, columns, formatters={'volume_id': _VolumeIdColumn}, + ) for s in data + ), + ) class RestoreVolumeBackup(command.ShowOne): @@ -295,7 +373,7 @@ class SetVolumeBackup(command.Command): _description = _("Set volume backup properties") def get_parser(self, prog_name): - parser = super(SetVolumeBackup, self).get_parser(prog_name) + parser = super().get_parser(prog_name) parser.add_argument( "backup", metavar="<backup>", @@ -304,28 +382,58 @@ class SetVolumeBackup(command.Command): parser.add_argument( '--name', metavar='<name>', - help=_('New backup name') + help=_( + 'New backup name' + '(supported by --os-volume-api-version 3.9 or above)' + ), ) parser.add_argument( '--description', metavar='<description>', - help=_('New backup description') + help=_( + 'New backup description ' + '(supported by --os-volume-api-version 3.9 or above)' + ), ) parser.add_argument( '--state', metavar='<state>', choices=['available', 'error'], - help=_('New backup state ("available" or "error") (admin only) ' - '(This option simply changes the state of the backup ' - 'in the database with no regard to actual status, ' - 'exercise caution when using)'), + help=_( + 'New backup state ("available" or "error") (admin only) ' + '(This option simply changes the state of the backup ' + 'in the database with no regard to actual status; ' + 'exercise caution when using)' + ), + ) + parser.add_argument( + '--no-property', + action='store_true', + help=_( + 'Remove all properties from this backup ' + '(specify both --no-property and --property to remove the ' + 'current properties before setting new properties)' + ), + ) + parser.add_argument( + '--property', + metavar='<key=value>', + action=parseractions.KeyValueAction, + dest='properties', + default={}, + help=_( + 'Set a property on this backup ' + '(repeat option to set multiple values) ' + '(supported by --os-volume-api-version 3.43 or above)' + ), ) return parser def take_action(self, parsed_args): volume_client = self.app.client_manager.volume - backup = utils.find_resource(volume_client.backups, - parsed_args.backup) + backup = utils.find_resource( + volume_client.backups, parsed_args.backup) + result = 0 if parsed_args.state: try: @@ -336,21 +444,119 @@ class SetVolumeBackup(command.Command): result += 1 kwargs = {} + if parsed_args.name: + if volume_client.api_version < api_versions.APIVersion('3.9'): + msg = _( + '--os-volume-api-version 3.9 or greater is required to ' + 'support the --name option' + ) + raise exceptions.CommandError(msg) + kwargs['name'] = parsed_args.name + if parsed_args.description: + if volume_client.api_version < api_versions.APIVersion('3.9'): + msg = _( + '--os-volume-api-version 3.9 or greater is required to ' + 'support the --description option' + ) + raise exceptions.CommandError(msg) + kwargs['description'] = parsed_args.description + + if parsed_args.no_property: + if volume_client.api_version < api_versions.APIVersion('3.43'): + msg = _( + '--os-volume-api-version 3.43 or greater is required to ' + 'support the --no-property option' + ) + raise exceptions.CommandError(msg) + + if parsed_args.properties: + if volume_client.api_version < api_versions.APIVersion('3.43'): + msg = _( + '--os-volume-api-version 3.43 or greater is required to ' + 'support the --property option' + ) + raise exceptions.CommandError(msg) + + if volume_client.api_version >= api_versions.APIVersion('3.43'): + metadata = copy.deepcopy(backup.metadata) + + if parsed_args.no_property: + metadata = {} + + metadata.update(parsed_args.properties) + kwargs['metadata'] = metadata + if kwargs: try: volume_client.backups.update(backup.id, **kwargs) except Exception as e: - LOG.error(_("Failed to update backup name " - "or description: %s"), e) + LOG.error("Failed to update backup: %s", e) result += 1 if result > 0: - raise exceptions.CommandError(_("One or more of the " - "set operations failed")) + msg = _("One or more of the set operations failed") + raise exceptions.CommandError(msg) + + +class UnsetVolumeBackup(command.Command): + """Unset volume backup properties. + + This command requires ``--os-volume-api-version`` 3.43 or greater. + """ + + def get_parser(self, prog_name): + parser = super().get_parser(prog_name) + parser.add_argument( + 'backup', + metavar='<backup>', + help=_('Backup to modify (name or ID)') + ) + parser.add_argument( + '--property', + metavar='<key>', + action='append', + dest='properties', + help=_( + 'Property to remove from this backup ' + '(repeat option to unset multiple values) ' + ), + ) + return parser + + def take_action(self, parsed_args): + volume_client = self.app.client_manager.volume + + if volume_client.api_version < api_versions.APIVersion('3.43'): + msg = _( + '--os-volume-api-version 3.43 or greater is required to ' + 'support the --property option' + ) + raise exceptions.CommandError(msg) + + backup = utils.find_resource( + volume_client.backups, parsed_args.backup) + metadata = copy.deepcopy(backup.metadata) + + for key in parsed_args.properties: + if key not in metadata: + # ignore invalid properties but continue + LOG.warning( + "'%s' is not a valid property for backup '%s'", + key, parsed_args.backup, + ) + continue + + del metadata[key] + + kwargs = { + 'metadata': metadata, + } + + volume_client.backups.update(backup.id, **kwargs) class ShowVolumeBackup(command.ShowOne): diff --git a/openstackclient/volume/v2/volume_transfer_request.py b/openstackclient/volume/v2/volume_transfer_request.py index 2a1ace1f..89199336 100644 --- a/openstackclient/volume/v2/volume_transfer_request.py +++ b/openstackclient/volume/v2/volume_transfer_request.py @@ -16,6 +16,7 @@ import logging +from cinderclient import api_versions from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils @@ -77,6 +78,25 @@ class CreateTransferRequest(command.ShowOne): help=_('New transfer request name (default to None)'), ) parser.add_argument( + '--snapshots', + action='store_true', + dest='snapshots', + help=_( + 'Allow transfer volumes without snapshots (default) ' + '(supported by --os-volume-api-version 3.55 or later)' + ), + default=None, + ) + parser.add_argument( + '--no-snapshots', + action='store_false', + dest='snapshots', + help=_( + 'Disallow transfer volumes without snapshots ' + '(supported by --os-volume-api-version 3.55 or later)' + ), + ) + parser.add_argument( 'volume', metavar="<volume>", help=_('Volume to transfer (name or ID)'), @@ -85,6 +105,21 @@ class CreateTransferRequest(command.ShowOne): def take_action(self, parsed_args): volume_client = self.app.client_manager.volume + + kwargs = {} + + if parsed_args.snapshots is not None: + if volume_client.api_version < api_versions.APIVersion('3.55'): + msg = _( + "--os-volume-api-version 3.55 or greater is required to " + "support the '--(no-)snapshots' option" + ) + raise exceptions.CommandError(msg) + + # unfortunately this option is negative so we have to reverse + # things + kwargs['no_snapshots'] = not parsed_args.snapshots + volume_id = utils.find_resource( volume_client.volumes, parsed_args.volume, @@ -92,6 +127,7 @@ class CreateTransferRequest(command.ShowOne): volume_transfer_request = volume_client.transfers.create( volume_id, parsed_args.name, + **kwargs, ) volume_transfer_request._info.pop("links", None) diff --git a/openstackclient/volume/v3/volume_attachment.py b/openstackclient/volume/v3/volume_attachment.py new file mode 100644 index 00000000..39a9c37f --- /dev/null +++ b/openstackclient/volume/v3/volume_attachment.py @@ -0,0 +1,511 @@ +# 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 logging + +from cinderclient import api_versions +from osc_lib.cli import format_columns +from osc_lib.command import command +from osc_lib import exceptions +from osc_lib import utils + +from openstackclient.i18n import _ +from openstackclient.identity import common as identity_common + +LOG = logging.getLogger(__name__) + +_FILTER_DEPRECATED = _( + "This option is deprecated. Consider using the '--filters' option which " + "was introduced in microversion 3.33 instead." +) + + +def _format_attachment(attachment): + columns = ( + 'id', + 'volume_id', + 'instance', + 'status', + 'attach_mode', + 'attached_at', + 'detached_at', + 'connection_info', + ) + column_headers = ( + 'ID', + 'Volume ID', + 'Instance ID', + 'Status', + 'Attach Mode', + 'Attached At', + 'Detached At', + 'Properties', + ) + + # TODO(stephenfin): Improve output with the nested connection_info + # field - cinderclient printed two things but that's equally ugly + return ( + column_headers, + utils.get_item_properties( + attachment, + columns, + formatters={ + 'connection_info': format_columns.DictColumn, + }, + ), + ) + + +class CreateVolumeAttachment(command.ShowOne): + """Create an attachment for a volume. + + This command will only create a volume attachment in the Volume service. It + will not invoke the necessary Compute service actions to actually attach + the volume to the server at the hypervisor level. As a result, it should + typically only be used for troubleshooting issues with an existing server + in combination with other tooling. For all other use cases, the 'server + volume add' command should be preferred. + """ + + def get_parser(self, prog_name): + parser = super().get_parser(prog_name) + parser.add_argument( + 'volume', + metavar='<volume>', + help=_('Name or ID of volume to attach to server.'), + ) + parser.add_argument( + 'server', + metavar='<server>', + help=_('Name or ID of server to attach volume to.'), + ) + parser.add_argument( + '--connect', + action='store_true', + dest='connect', + default=False, + help=_('Make an active connection using provided connector info'), + ) + parser.add_argument( + '--no-connect', + action='store_false', + dest='connect', + help=_( + 'Do not make an active connection using provided connector ' + 'info' + ), + ) + parser.add_argument( + '--initiator', + metavar='<initiator>', + help=_('IQN of the initiator attaching to'), + ) + parser.add_argument( + '--ip', + metavar='<ip>', + help=_('IP of the system attaching to'), + ) + parser.add_argument( + '--host', + metavar='<host>', + help=_('Name of the host attaching to'), + ) + parser.add_argument( + '--platform', + metavar='<platform>', + help=_('Platform type'), + ) + parser.add_argument( + '--os-type', + metavar='<ostype>', + help=_('OS type'), + ) + parser.add_argument( + '--multipath', + action='store_true', + dest='multipath', + default=False, + help=_('Use multipath'), + ) + parser.add_argument( + '--no-multipath', + action='store_false', + dest='multipath', + help=_('Use multipath'), + ) + parser.add_argument( + '--mountpoint', + metavar='<mountpoint>', + help=_('Mountpoint volume will be attached at'), + ) + parser.add_argument( + '--mode', + metavar='<mode>', + help=_( + 'Mode of volume attachment, rw, ro and null, where null ' + 'indicates we want to honor any existing admin-metadata ' + 'settings ' + '(supported by --os-volume-api-version 3.54 or later)' + ), + ) + return parser + + def take_action(self, parsed_args): + volume_client = self.app.client_manager.volume + compute_client = self.app.client_manager.compute + + if volume_client.api_version < api_versions.APIVersion('3.27'): + msg = _( + "--os-volume-api-version 3.27 or greater is required to " + "support the 'volume attachment create' command" + ) + raise exceptions.CommandError(msg) + + if parsed_args.mode: + if volume_client.api_version < api_versions.APIVersion('3.54'): + msg = _( + "--os-volume-api-version 3.54 or greater is required to " + "support the '--mode' option" + ) + raise exceptions.CommandError(msg) + + connector = {} + if parsed_args.connect: + connector = { + 'initiator': parsed_args.initiator, + 'ip': parsed_args.ip, + 'platform': parsed_args.platform, + 'host': parsed_args.host, + 'os_type': parsed_args.os_type, + 'multipath': parsed_args.multipath, + 'mountpoint': parsed_args.mountpoint, + } + else: + if any({ + parsed_args.initiator, + parsed_args.ip, + parsed_args.platform, + parsed_args.host, + parsed_args.host, + parsed_args.multipath, + parsed_args.mountpoint, + }): + msg = _( + 'You must specify the --connect option for any of the ' + 'connection-specific options such as --initiator to be ' + 'valid' + ) + raise exceptions.CommandError(msg) + + volume = utils.find_resource( + volume_client.volumes, + parsed_args.volume, + ) + server = utils.find_resource( + compute_client.servers, + parsed_args.server, + ) + + attachment = volume_client.attachments.create( + volume.id, connector, server.id, parsed_args.mode) + + return _format_attachment(attachment) + + +class DeleteVolumeAttachment(command.Command): + """Delete an attachment for a volume. + + Similarly to the 'volume attachment create' command, this command will only + delete the volume attachment record in the Volume service. It will not + invoke the necessary Compute service actions to actually attach the volume + to the server at the hypervisor level. As a result, it should typically + only be used for troubleshooting issues with an existing server in + combination with other tooling. For all other use cases, the 'server volume + remove' command should be preferred. + """ + + def get_parser(self, prog_name): + parser = super().get_parser(prog_name) + parser.add_argument( + 'attachment', + metavar='<attachment>', + help=_('ID of volume attachment to delete'), + ) + return parser + + def take_action(self, parsed_args): + volume_client = self.app.client_manager.volume + + if volume_client.api_version < api_versions.APIVersion('3.27'): + msg = _( + "--os-volume-api-version 3.27 or greater is required to " + "support the 'volume attachment delete' command" + ) + raise exceptions.CommandError(msg) + + volume_client.attachments.delete(parsed_args.attachment) + + +class SetVolumeAttachment(command.ShowOne): + """Update an attachment for a volume. + + This call is designed to be more of an volume attachment completion than + anything else. It expects the value of a connector object to notify the + driver that the volume is going to be connected and where it's being + connected to. + """ + + def get_parser(self, prog_name): + parser = super().get_parser(prog_name) + parser.add_argument( + 'attachment', + metavar='<attachment>', + help=_('ID of volume attachment.'), + ) + parser.add_argument( + '--initiator', + metavar='<initiator>', + help=_('IQN of the initiator attaching to'), + ) + parser.add_argument( + '--ip', + metavar='<ip>', + help=_('IP of the system attaching to'), + ) + parser.add_argument( + '--host', + metavar='<host>', + help=_('Name of the host attaching to'), + ) + parser.add_argument( + '--platform', + metavar='<platform>', + help=_('Platform type'), + ) + parser.add_argument( + '--os-type', + metavar='<ostype>', + help=_('OS type'), + ) + parser.add_argument( + '--multipath', + action='store_true', + dest='multipath', + default=False, + help=_('Use multipath'), + ) + parser.add_argument( + '--no-multipath', + action='store_false', + dest='multipath', + help=_('Use multipath'), + ) + parser.add_argument( + '--mountpoint', + metavar='<mountpoint>', + help=_('Mountpoint volume will be attached at'), + ) + return parser + + def take_action(self, parsed_args): + volume_client = self.app.client_manager.volume + + if volume_client.api_version < api_versions.APIVersion('3.27'): + msg = _( + "--os-volume-api-version 3.27 or greater is required to " + "support the 'volume attachment set' command" + ) + raise exceptions.CommandError(msg) + + connector = { + 'initiator': parsed_args.initiator, + 'ip': parsed_args.ip, + 'platform': parsed_args.platform, + 'host': parsed_args.host, + 'os_type': parsed_args.os_type, + 'multipath': parsed_args.multipath, + 'mountpoint': parsed_args.mountpoint, + } + + attachment = volume_client.attachments.update( + parsed_args.attachment, connector) + + return _format_attachment(attachment) + + +class CompleteVolumeAttachment(command.Command): + """Complete an attachment for a volume.""" + + def get_parser(self, prog_name): + parser = super().get_parser(prog_name) + parser.add_argument( + 'attachment', + metavar='<attachment>', + help=_('ID of volume attachment to mark as completed'), + ) + return parser + + def take_action(self, parsed_args): + volume_client = self.app.client_manager.volume + + if volume_client.api_version < api_versions.APIVersion('3.44'): + msg = _( + "--os-volume-api-version 3.44 or greater is required to " + "support the 'volume attachment complete' command" + ) + raise exceptions.CommandError(msg) + + volume_client.attachments.complete(parsed_args.attachment) + + +class ListVolumeAttachment(command.Lister): + """Lists all volume attachments.""" + + def get_parser(self, prog_name): + parser = super().get_parser(prog_name) + parser.add_argument( + '--project', + dest='project', + metavar='<project>', + help=_('Filter results by project (name or ID) (admin only)'), + ) + identity_common.add_project_domain_option_to_parser(parser) + parser.add_argument( + '--all-projects', + dest='all_projects', + action='store_true', + default=utils.env('ALL_PROJECTS', default=False), + help=_('Shows details for all projects (admin only).'), + ) + parser.add_argument( + '--volume-id', + metavar='<volume-id>', + default=None, + help=_('Filters results by a volume ID. ') + _FILTER_DEPRECATED, + ) + parser.add_argument( + '--status', + metavar='<status>', + help=_('Filters results by a status. ') + _FILTER_DEPRECATED, + ) + parser.add_argument( + '--marker', + metavar='<marker>', + help=_( + 'Begin returning volume attachments that appear later in ' + 'volume attachment list than that represented by this ID.' + ), + ) + parser.add_argument( + '--limit', + type=int, + metavar='<limit>', + help=_('Maximum number of volume attachments to return.'), + ) + # TODO(stephenfin): Add once we have an equivalent command for + # 'cinder list-filters' + # parser.add_argument( + # '--filter', + # metavar='<key=value>', + # action=parseractions.KeyValueAction, + # dest='filters', + # help=_( + # "Filter key and value pairs. Use 'foo' to " + # "check enabled filters from server. Use 'key~=value' for " + # "inexact filtering if the key supports " + # "(supported by --os-volume-api-version 3.33 or above)" + # ), + # ) + return parser + + def take_action(self, parsed_args): + volume_client = self.app.client_manager.volume + identity_client = self.app.client_manager.identity + + if volume_client.api_version < api_versions.APIVersion('3.27'): + msg = _( + "--os-volume-api-version 3.27 or greater is required to " + "support the 'volume attachment list' command" + ) + raise exceptions.CommandError(msg) + + project_id = None + if parsed_args.project: + project_id = identity_common.find_project( + identity_client, + parsed_args.project, + parsed_args.project_domain, + ).id + + search_opts = { + 'all_tenants': True if project_id else parsed_args.all_projects, + 'project_id': project_id, + 'status': parsed_args.status, + 'volume_id': parsed_args.volume_id, + } + # Update search option with `filters` + # if AppendFilters.filters: + # search_opts.update(shell_utils.extract_filters(AppendFilters.filters)) + + # TODO(stephenfin): Implement sorting + attachments = volume_client.attachments.list( + search_opts=search_opts, + marker=parsed_args.marker, + limit=parsed_args.limit) + + column_headers = ( + 'ID', + 'Volume ID', + 'Server ID', + 'Status', + ) + columns = ( + 'id', + 'volume_id', + 'instance', + 'status', + ) + + return ( + column_headers, + ( + utils.get_item_properties(a, columns) + for a in attachments + ), + ) + + +class ShowVolumeAttachment(command.ShowOne): + """Show detailed information for a volume attachment.""" + + def get_parser(self, prog_name): + parser = super().get_parser(prog_name) + parser.add_argument( + 'attachment', + metavar='<attachment>', + help=_('ID of volume attachment.'), + ) + return parser + + def take_action(self, parsed_args): + volume_client = self.app.client_manager.volume + + if volume_client.api_version < api_versions.APIVersion('3.27'): + msg = _( + "--os-volume-api-version 3.27 or greater is required to " + "support the 'volume attachment show' command" + ) + raise exceptions.CommandError(msg) + + attachment = volume_client.attachments.show(parsed_args.attachment) + + return _format_attachment(attachment) diff --git a/openstackclient/volume/v3/volume_group.py b/openstackclient/volume/v3/volume_group.py new file mode 100644 index 00000000..db4e9a94 --- /dev/null +++ b/openstackclient/volume/v3/volume_group.py @@ -0,0 +1,506 @@ +# 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 logging + +from cinderclient import api_versions +from osc_lib.command import command +from osc_lib import exceptions +from osc_lib import utils + +from openstackclient.i18n import _ + +LOG = logging.getLogger(__name__) + + +def _format_group(group): + columns = ( + 'id', + 'status', + 'name', + 'description', + 'group_type', + 'volume_types', + 'availability_zone', + 'created_at', + 'volumes', + 'group_snapshot_id', + 'source_group_id', + ) + column_headers = ( + 'ID', + 'Status', + 'Name', + 'Description', + 'Group Type', + 'Volume Types', + 'Availability Zone', + 'Created At', + 'Volumes', + 'Group Snapshot ID', + 'Source Group ID', + ) + + # TODO(stephenfin): Consider using a formatter for volume_types since it's + # a list + return ( + column_headers, + utils.get_item_properties( + group, + columns, + ), + ) + + +class CreateVolumeGroup(command.ShowOne): + """Create a volume group. + + Generic volume groups enable you to create a group of volumes and manage + them together. + + Generic volume groups are more flexible than consistency groups. Currently + volume consistency groups only support consistent group snapshot. It + cannot be extended easily to serve other purposes. A project may want to + put volumes used in the same application together in a group so that it is + easier to manage them together, and this group of volumes may or may not + support consistent group snapshot. Generic volume group solve this problem. + By decoupling the tight relationship between the group construct and the + consistency concept, generic volume groups can be extended to support other + features in the future. + + This command requires ``--os-volume-api-version`` 3.13 or greater. + """ + + def get_parser(self, prog_name): + parser = super().get_parser(prog_name) + parser.add_argument( + 'volume_group_type', + metavar='<volume_group_type>', + help=_('Name or ID of volume group type to use.'), + ) + parser.add_argument( + 'volume_types', + metavar='<volume_type>', + nargs='+', + default=[], + help=_('Name or ID of volume type(s) to use.'), + ) + parser.add_argument( + '--name', + metavar='<name>', + help=_('Name of the volume group.'), + ) + parser.add_argument( + '--description', + metavar='<description>', + help=_('Description of a volume group.') + ) + parser.add_argument( + '--availability-zone', + metavar='<availability-zone>', + help=_('Availability zone for volume group.'), + ) + return parser + + def take_action(self, parsed_args): + volume_client = self.app.client_manager.volume + + if volume_client.api_version < api_versions.APIVersion('3.13'): + msg = _( + "--os-volume-api-version 3.13 or greater is required to " + "support the 'volume group create' command" + ) + raise exceptions.CommandError(msg) + + volume_group_type = utils.find_resource( + volume_client.group_types, + parsed_args.volume_group_type, + ) + + volume_types = [] + for volume_type in parsed_args.volume_types: + volume_types.append( + utils.find_resource( + volume_client.volume_types, + volume_type, + ) + ) + + group = volume_client.groups.create( + volume_group_type.id, + ','.join(x.id for x in volume_types), + parsed_args.name, + parsed_args.description, + availability_zone=parsed_args.availability_zone) + + group = volume_client.groups.get(group.id) + + return _format_group(group) + + +class DeleteVolumeGroup(command.Command): + """Delete a volume group. + + This command requires ``--os-volume-api-version`` 3.13 or greater. + """ + + def get_parser(self, prog_name): + parser = super().get_parser(prog_name) + parser.add_argument( + 'group', + metavar='<group>', + help=_('Name or ID of volume group to delete'), + ) + parser.add_argument( + '--force', + action='store_true', + default=False, + help=_( + 'Delete the volume group even if it contains volumes. ' + 'This will delete any remaining volumes in the group.', + ) + ) + return parser + + def take_action(self, parsed_args): + volume_client = self.app.client_manager.volume + + if volume_client.api_version < api_versions.APIVersion('3.13'): + msg = _( + "--os-volume-api-version 3.13 or greater is required to " + "support the 'volume group delete' command" + ) + raise exceptions.CommandError(msg) + + group = utils.find_resource( + volume_client.groups, + parsed_args.group, + ) + + volume_client.groups.delete( + group.id, delete_volumes=parsed_args.force) + + +class SetVolumeGroup(command.ShowOne): + """Update a volume group. + + This command requires ``--os-volume-api-version`` 3.13 or greater. + """ + + def get_parser(self, prog_name): + parser = super().get_parser(prog_name) + parser.add_argument( + 'group', + metavar='<group>', + help=_('Name or ID of volume group.'), + ) + parser.add_argument( + '--name', + metavar='<name>', + help=_('New name for group.'), + ) + parser.add_argument( + '--description', + metavar='<description>', + help=_('New description for group.'), + ) + parser.add_argument( + '--enable-replication', + action='store_true', + dest='enable_replication', + default=None, + help=_( + 'Enable replication for group. ' + '(supported by --os-volume-api-version 3.38 or above)' + ), + ) + parser.add_argument( + '--disable-replication', + action='store_false', + dest='enable_replication', + help=_( + 'Disable replication for group. ' + '(supported by --os-volume-api-version 3.38 or above)' + ), + ) + return parser + + def take_action(self, parsed_args): + volume_client = self.app.client_manager.volume + + if volume_client.api_version < api_versions.APIVersion('3.13'): + msg = _( + "--os-volume-api-version 3.13 or greater is required to " + "support the 'volume group set' command" + ) + raise exceptions.CommandError(msg) + + group = utils.find_resource( + volume_client.groups, + parsed_args.group, + ) + + if parsed_args.enable_replication is not None: + if volume_client.api_version < api_versions.APIVersion('3.38'): + msg = _( + "--os-volume-api-version 3.38 or greater is required to " + "support the '--enable-replication' or " + "'--disable-replication' options" + ) + raise exceptions.CommandError(msg) + + if parsed_args.enable_replication: + volume_client.groups.enable_replication(group.id) + else: + volume_client.groups.disable_replication(group.id) + + kwargs = {} + + if parsed_args.name is not None: + kwargs['name'] = parsed_args.name + + if parsed_args.description is not None: + kwargs['description'] = parsed_args.description + + if kwargs: + group = volume_client.groups.update(group.id, **kwargs) + + return _format_group(group) + + +class ListVolumeGroup(command.Lister): + """Lists all volume groups. + + This command requires ``--os-volume-api-version`` 3.13 or greater. + """ + + def get_parser(self, prog_name): + parser = super().get_parser(prog_name) + parser.add_argument( + '--all-projects', + dest='all_projects', + action='store_true', + default=utils.env('ALL_PROJECTS', default=False), + help=_('Shows details for all projects (admin only).'), + ) + # TODO(stephenfin): Add once we have an equivalent command for + # 'cinder list-filters' + # parser.add_argument( + # '--filter', + # metavar='<key=value>', + # action=parseractions.KeyValueAction, + # dest='filters', + # help=_( + # "Filter key and value pairs. Use 'foo' to " + # "check enabled filters from server. Use 'key~=value' for " + # "inexact filtering if the key supports " + # "(supported by --os-volume-api-version 3.33 or above)" + # ), + # ) + return parser + + def take_action(self, parsed_args): + volume_client = self.app.client_manager.volume + + if volume_client.api_version < api_versions.APIVersion('3.13'): + msg = _( + "--os-volume-api-version 3.13 or greater is required to " + "support the 'volume group list' command" + ) + raise exceptions.CommandError(msg) + + search_opts = { + 'all_tenants': parsed_args.all_projects, + } + + groups = volume_client.groups.list( + search_opts=search_opts) + + column_headers = ( + 'ID', + 'Status', + 'Name', + ) + columns = ( + 'id', + 'status', + 'name', + ) + + return ( + column_headers, + ( + utils.get_item_properties(a, columns) + for a in groups + ), + ) + + +class ShowVolumeGroup(command.ShowOne): + """Show detailed information for a volume group. + + This command requires ``--os-volume-api-version`` 3.13 or greater. + """ + + def get_parser(self, prog_name): + parser = super().get_parser(prog_name) + parser.add_argument( + 'group', + metavar='<group>', + help=_('Name or ID of volume group.'), + ) + parser.add_argument( + '--volumes', + action='store_true', + dest='show_volumes', + default=None, + help=_( + 'Show volumes included in the group. ' + '(supported by --os-volume-api-version 3.25 or above)' + ), + ) + parser.add_argument( + '--no-volumes', + action='store_false', + dest='show_volumes', + help=_( + 'Do not show volumes included in the group. ' + '(supported by --os-volume-api-version 3.25 or above)' + ), + ) + parser.add_argument( + '--replication-targets', + action='store_true', + dest='show_replication_targets', + default=None, + help=_( + 'Show replication targets for the group. ' + '(supported by --os-volume-api-version 3.38 or above)' + ), + ) + parser.add_argument( + '--no-replication-targets', + action='store_false', + dest='show_replication_targets', + help=_( + 'Do not show replication targets for the group. ' + '(supported by --os-volume-api-version 3.38 or above)' + ), + ) + + return parser + + def take_action(self, parsed_args): + volume_client = self.app.client_manager.volume + + if volume_client.api_version < api_versions.APIVersion('3.13'): + msg = _( + "--os-volume-api-version 3.13 or greater is required to " + "support the 'volume group show' command" + ) + raise exceptions.CommandError(msg) + + kwargs = {} + + if parsed_args.show_volumes is not None: + if volume_client.api_version < api_versions.APIVersion('3.25'): + msg = _( + "--os-volume-api-version 3.25 or greater is required to " + "support the '--(no-)volumes' option" + ) + raise exceptions.CommandError(msg) + + kwargs['list_volume'] = parsed_args.show_volumes + + if parsed_args.show_replication_targets is not None: + if volume_client.api_version < api_versions.APIVersion('3.38'): + msg = _( + "--os-volume-api-version 3.38 or greater is required to " + "support the '--(no-)replication-targets' option" + ) + raise exceptions.CommandError(msg) + + group = utils.find_resource( + volume_client.groups, + parsed_args.group, + ) + + group = volume_client.groups.show(group.id, **kwargs) + + if parsed_args.show_replication_targets: + replication_targets = \ + volume_client.groups.list_replication_targets(group.id) + + group.replication_targets = replication_targets + + # TODO(stephenfin): Show replication targets + return _format_group(group) + + +class FailoverVolumeGroup(command.Command): + """Failover replication for a volume group. + + This command requires ``--os-volume-api-version`` 3.38 or greater. + """ + + def get_parser(self, prog_name): + parser = super().get_parser(prog_name) + parser.add_argument( + 'group', + metavar='<group>', + help=_('Name or ID of volume group to failover replication for.'), + ) + parser.add_argument( + '--allow-attached-volume', + action='store_true', + dest='allow_attached_volume', + default=False, + help=_( + 'Allow group with attached volumes to be failed over.', + ) + ) + parser.add_argument( + '--disallow-attached-volume', + action='store_false', + dest='allow_attached_volume', + default=False, + help=_( + 'Disallow group with attached volumes to be failed over.', + ) + ) + parser.add_argument( + '--secondary-backend-id', + metavar='<backend_id>', + help=_('Secondary backend ID.'), + ) + return parser + + def take_action(self, parsed_args): + volume_client = self.app.client_manager.volume + + if volume_client.api_version < api_versions.APIVersion('3.38'): + msg = _( + "--os-volume-api-version 3.38 or greater is required to " + "support the 'volume group failover' command" + ) + raise exceptions.CommandError(msg) + + group = utils.find_resource( + volume_client.groups, + parsed_args.group, + ) + + volume_client.groups.failover_replication( + group.id, + allow_attached_volume=parsed_args.allow_attached_volume, + secondary_backend_id=parsed_args.secondary_backend_id, + ) diff --git a/openstackclient/volume/v3/volume_group_snapshot.py b/openstackclient/volume/v3/volume_group_snapshot.py new file mode 100644 index 00000000..229cbd71 --- /dev/null +++ b/openstackclient/volume/v3/volume_group_snapshot.py @@ -0,0 +1,234 @@ +# 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 logging + +from cinderclient import api_versions +from osc_lib.command import command +from osc_lib import exceptions +from osc_lib import utils + +from openstackclient.i18n import _ + +LOG = logging.getLogger(__name__) + + +def _format_group_snapshot(snapshot): + columns = ( + 'id', + 'status', + 'name', + 'description', + 'group_id', + 'group_type_id', + ) + column_headers = ( + 'ID', + 'Status', + 'Name', + 'Description', + 'Group', + 'Group Type', + ) + + return ( + column_headers, + utils.get_item_properties( + snapshot, + columns, + ), + ) + + +class CreateVolumeGroupSnapshot(command.ShowOne): + """Create a volume group snapshot. + + This command requires ``--os-volume-api-version`` 3.13 or greater. + """ + + def get_parser(self, prog_name): + parser = super().get_parser(prog_name) + parser.add_argument( + 'volume_group', + metavar='<volume_group>', + help=_('Name or ID of volume group to create a snapshot of.'), + ) + parser.add_argument( + '--name', + metavar='<name>', + help=_('Name of the volume group snapshot.'), + ) + parser.add_argument( + '--description', + metavar='<description>', + help=_('Description of a volume group snapshot.') + ) + return parser + + def take_action(self, parsed_args): + volume_client = self.app.client_manager.volume + + if volume_client.api_version < api_versions.APIVersion('3.14'): + msg = _( + "--os-volume-api-version 3.14 or greater is required to " + "support the 'volume group snapshot create' command" + ) + raise exceptions.CommandError(msg) + + volume_group = utils.find_resource( + volume_client.groups, + parsed_args.volume_group, + ) + + snapshot = volume_client.group_snapshots.create( + volume_group.id, + parsed_args.name, + parsed_args.description) + + return _format_group_snapshot(snapshot) + + +class DeleteVolumeGroupSnapshot(command.Command): + """Delete a volume group snapshot. + + This command requires ``--os-volume-api-version`` 3.14 or greater. + """ + + def get_parser(self, prog_name): + parser = super().get_parser(prog_name) + parser.add_argument( + 'snapshot', + metavar='<snapshot>', + help=_('Name or ID of volume group snapshot to delete'), + ) + return parser + + def take_action(self, parsed_args): + volume_client = self.app.client_manager.volume + + if volume_client.api_version < api_versions.APIVersion('3.14'): + msg = _( + "--os-volume-api-version 3.14 or greater is required to " + "support the 'volume group snapshot delete' command" + ) + raise exceptions.CommandError(msg) + + snapshot = utils.find_resource( + volume_client.group_snapshots, + parsed_args.snapshot, + ) + + volume_client.group_snapshots.delete(snapshot.id) + + +class ListVolumeGroupSnapshot(command.Lister): + """Lists all volume group snapshot. + + This command requires ``--os-volume-api-version`` 3.14 or greater. + """ + + def get_parser(self, prog_name): + parser = super().get_parser(prog_name) + parser.add_argument( + '--all-projects', + dest='all_projects', + action='store_true', + default=utils.env('ALL_PROJECTS', default=False), + help=_('Shows details for all projects (admin only).'), + ) + # TODO(stephenfin): Add once we have an equivalent command for + # 'cinder list-filters' + # parser.add_argument( + # '--filter', + # metavar='<key=value>', + # action=parseractions.KeyValueAction, + # dest='filters', + # help=_( + # "Filter key and value pairs. Use 'foo' to " + # "check enabled filters from server. Use 'key~=value' for " + # "inexact filtering if the key supports " + # "(supported by --os-volume-api-version 3.33 or above)" + # ), + # ) + return parser + + def take_action(self, parsed_args): + volume_client = self.app.client_manager.volume + + if volume_client.api_version < api_versions.APIVersion('3.14'): + msg = _( + "--os-volume-api-version 3.14 or greater is required to " + "support the 'volume group snapshot list' command" + ) + raise exceptions.CommandError(msg) + + search_opts = { + 'all_tenants': parsed_args.all_projects, + } + + groups = volume_client.group_snapshots.list( + search_opts=search_opts) + + column_headers = ( + 'ID', + 'Status', + 'Name', + ) + columns = ( + 'id', + 'status', + 'name', + ) + + return ( + column_headers, + ( + utils.get_item_properties(a, columns) + for a in groups + ), + ) + + +class ShowVolumeGroupSnapshot(command.ShowOne): + """Show detailed information for a volume group snapshot. + + This command requires ``--os-volume-api-version`` 3.14 or greater. + """ + + def get_parser(self, prog_name): + parser = super().get_parser(prog_name) + parser.add_argument( + 'snapshot', + metavar='<snapshot>', + help=_('Name or ID of volume group snapshot.'), + ) + return parser + + def take_action(self, parsed_args): + volume_client = self.app.client_manager.volume + + if volume_client.api_version < api_versions.APIVersion('3.14'): + msg = _( + "--os-volume-api-version 3.14 or greater is required to " + "support the 'volume group snapshot show' command" + ) + raise exceptions.CommandError(msg) + + snapshot = utils.find_resource( + volume_client.group_snapshots, + parsed_args.snapshot, + ) + + # TODO(stephenfin): Do we need this? + snapshot = volume_client.groups.show(snapshot.id) + + return _format_group_snapshot(snapshot) diff --git a/openstackclient/volume/v3/volume_group_type.py b/openstackclient/volume/v3/volume_group_type.py new file mode 100644 index 00000000..860fa544 --- /dev/null +++ b/openstackclient/volume/v3/volume_group_type.py @@ -0,0 +1,410 @@ +# 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 logging + +from cinderclient import api_versions +from osc_lib.cli import format_columns +from osc_lib.cli import parseractions +from osc_lib.command import command +from osc_lib import exceptions +from osc_lib import utils + +from openstackclient.i18n import _ + +LOG = logging.getLogger(__name__) + + +def _format_group_type(group): + columns = ( + 'id', + 'name', + 'description', + 'is_public', + 'group_specs', + ) + column_headers = ( + 'ID', + 'Name', + 'Description', + 'Is Public', + 'Properties', + ) + + # TODO(stephenfin): Consider using a formatter for volume_types since it's + # a list + return ( + column_headers, + utils.get_item_properties( + group, + columns, + formatters={ + 'group_specs': format_columns.DictColumn, + }, + ), + ) + + +class CreateVolumeGroupType(command.ShowOne): + """Create a volume group type. + + This command requires ``--os-volume-api-version`` 3.11 or greater. + """ + + def get_parser(self, prog_name): + parser = super().get_parser(prog_name) + parser.add_argument( + 'name', + metavar='<name>', + help=_('Name of new volume group type.'), + ) + parser.add_argument( + '--description', + metavar='<description>', + help=_('Description of the volume group type.') + ) + type_group = parser.add_mutually_exclusive_group() + type_group.add_argument( + '--public', + dest='is_public', + action='store_true', + default=True, + help=_( + 'Volume group type is available to other projects (default)' + ), + ) + type_group.add_argument( + '--private', + dest='is_public', + action='store_false', + help=_('Volume group type is not available to other projects') + ) + return parser + + def take_action(self, parsed_args): + volume_client = self.app.client_manager.volume + + if volume_client.api_version < api_versions.APIVersion('3.11'): + msg = _( + "--os-volume-api-version 3.11 or greater is required to " + "support the 'volume group type create' command" + ) + raise exceptions.CommandError(msg) + + group_type = volume_client.group_types.create( + parsed_args.name, + parsed_args.description, + parsed_args.is_public) + + return _format_group_type(group_type) + + +class DeleteVolumeGroupType(command.Command): + """Delete a volume group type. + + This command requires ``--os-volume-api-version`` 3.11 or greater. + """ + + def get_parser(self, prog_name): + parser = super().get_parser(prog_name) + parser.add_argument( + 'group_type', + metavar='<group_type>', + help=_('Name or ID of volume group type to delete'), + ) + return parser + + def take_action(self, parsed_args): + volume_client = self.app.client_manager.volume + + if volume_client.api_version < api_versions.APIVersion('3.11'): + msg = _( + "--os-volume-api-version 3.11 or greater is required to " + "support the 'volume group type delete' command" + ) + raise exceptions.CommandError(msg) + + group_type = utils.find_resource( + volume_client.group_types, + parsed_args.group_type, + ) + + volume_client.group_types.delete(group_type.id) + + +class SetVolumeGroupType(command.ShowOne): + """Update a volume group type. + + This command requires ``--os-volume-api-version`` 3.11 or greater. + """ + + def get_parser(self, prog_name): + parser = super().get_parser(prog_name) + parser.add_argument( + 'group_type', + metavar='<group_type>', + help=_('Name or ID of volume group type.'), + ) + parser.add_argument( + '--name', + metavar='<name>', + help=_('New name for volume group type.'), + ) + parser.add_argument( + '--description', + metavar='<description>', + help=_('New description for volume group type.'), + ) + type_group = parser.add_mutually_exclusive_group() + type_group.add_argument( + '--public', + dest='is_public', + action='store_true', + default=None, + help=_('Make volume group type available to other projects.'), + ) + type_group.add_argument( + '--private', + dest='is_public', + action='store_false', + help=_('Make volume group type unavailable to other projects.') + ) + parser.add_argument( + '--no-property', + action='store_true', + help=_( + 'Remove all properties from this volume group type ' + '(specify both --no-property and --property ' + 'to remove the current properties before setting ' + 'new properties)' + ), + ) + parser.add_argument( + '--property', + metavar='<key=value>', + action=parseractions.KeyValueAction, + dest='properties', + help=_( + 'Property to add or modify for this volume group type ' + '(repeat option to set multiple properties)' + ), + ) + return parser + + def take_action(self, parsed_args): + volume_client = self.app.client_manager.volume + + if volume_client.api_version < api_versions.APIVersion('3.11'): + msg = _( + "--os-volume-api-version 3.11 or greater is required to " + "support the 'volume group type set' command" + ) + raise exceptions.CommandError(msg) + + group_type = utils.find_resource( + volume_client.group_types, + parsed_args.group_type, + ) + + kwargs = {} + errors = 0 + + if parsed_args.name is not None: + kwargs['name'] = parsed_args.name + + if parsed_args.description is not None: + kwargs['description'] = parsed_args.description + + if parsed_args.is_public is not None: + kwargs['is_public'] = parsed_args.is_public + + if kwargs: + try: + group_type = volume_client.group_types.update( + group_type.id, **kwargs) + except Exception as e: + LOG.error(_("Failed to update group type: %s"), e) + errors += 1 + + if parsed_args.no_property: + try: + keys = group_type.get_keys().keys() + group_type.unset_keys(keys) + except Exception as e: + LOG.error(_("Failed to clear group type properties: %s"), e) + errors += 1 + + if parsed_args.properties: + try: + group_type.set_keys(parsed_args.properties) + except Exception as e: + LOG.error(_("Failed to set group type properties: %s"), e) + errors += 1 + + if errors > 0: + msg = _( + "Command Failed: One or more of the operations failed" + ) + raise exceptions.CommandError() + + return _format_group_type(group_type) + + +class UnsetVolumeGroupType(command.ShowOne): + """Unset properties of a volume group type. + + This command requires ``--os-volume-api-version`` 3.11 or greater. + """ + + def get_parser(self, prog_name): + parser = super().get_parser(prog_name) + parser.add_argument( + 'group_type', + metavar='<group_type>', + help=_('Name or ID of volume group type.'), + ) + parser.add_argument( + '--property', + metavar='<key>', + action='append', + dest='properties', + help=_( + 'Property to remove from this volume group type ' + '(repeat option to unset multiple properties)' + ), + ) + return parser + + def take_action(self, parsed_args): + volume_client = self.app.client_manager.volume + + if volume_client.api_version < api_versions.APIVersion('3.11'): + msg = _( + "--os-volume-api-version 3.11 or greater is required to " + "support the 'volume group type unset' command" + ) + raise exceptions.CommandError(msg) + + group_type = utils.find_resource( + volume_client.group_types, + parsed_args.group_type, + ) + + group_type.unset_keys(parsed_args.properties) + + group_type = utils.find_resource( + volume_client.group_types, + parsed_args.group_type, + ) + + return _format_group_type(group_type) + + +class ListVolumeGroupType(command.Lister): + """Lists all volume group types. + + This command requires ``--os-volume-api-version`` 3.11 or greater. + """ + + def get_parser(self, prog_name): + parser = super().get_parser(prog_name) + parser.add_argument( + '--default', + action='store_true', + dest='show_default', + default=False, + help=_('List the default volume group type.'), + ) + # TODO(stephenfin): Add once we have an equivalent command for + # 'cinder list-filters' + # parser.add_argument( + # '--filter', + # metavar='<key=value>', + # action=parseractions.KeyValueAction, + # dest='filters', + # help=_( + # "Filter key and value pairs. Use 'foo' to " + # "check enabled filters from server. Use 'key~=value' for " + # "inexact filtering if the key supports " + # "(supported by --os-volume-api-version 3.33 or above)" + # ), + # ) + return parser + + def take_action(self, parsed_args): + volume_client = self.app.client_manager.volume + + if volume_client.api_version < api_versions.APIVersion('3.11'): + msg = _( + "--os-volume-api-version 3.11 or greater is required to " + "support the 'volume group type list' command" + ) + raise exceptions.CommandError(msg) + + if parsed_args.show_default: + group_types = [volume_client.group_types.default()] + else: + group_types = volume_client.group_types.list() + + column_headers = ( + 'ID', + 'Name', + 'Is Public', + 'Properties', + ) + columns = ( + 'id', + 'name', + 'is_public', + 'group_specs', + ) + + return ( + column_headers, + ( + utils.get_item_properties(a, columns) + for a in group_types + ), + ) + + +class ShowVolumeGroupType(command.ShowOne): + """Show detailed information for a volume group type. + + This command requires ``--os-volume-api-version`` 3.11 or greater. + """ + + def get_parser(self, prog_name): + parser = super().get_parser(prog_name) + parser.add_argument( + 'group_type', + metavar='<group_type>', + help=_('Name or ID of volume group type.'), + ) + return parser + + def take_action(self, parsed_args): + volume_client = self.app.client_manager.volume + + if volume_client.api_version < api_versions.APIVersion('3.11'): + msg = _( + "--os-volume-api-version 3.11 or greater is required to " + "support the 'volume group type show' command" + ) + raise exceptions.CommandError(msg) + + group_type = utils.find_resource( + volume_client.group_types, + parsed_args.group, + ) + + return _format_group_type(group_type) diff --git a/openstackclient/volume/v3/volume_message.py b/openstackclient/volume/v3/volume_message.py new file mode 100644 index 00000000..4fe5ae92 --- /dev/null +++ b/openstackclient/volume/v3/volume_message.py @@ -0,0 +1,165 @@ +# +# 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 V3 Messages implementations""" + +import logging as LOG + +from cinderclient import api_versions +from osc_lib.command import command +from osc_lib import exceptions +from osc_lib import utils + +from openstackclient.i18n import _ +from openstackclient.identity import common as identity_common + + +class DeleteMessage(command.Command): + _description = _('Delete a volume failure message') + + def get_parser(self, prog_name): + parser = super().get_parser(prog_name) + parser.add_argument( + 'message_ids', + metavar='<message-id>', + nargs='+', + help=_('Message(s) to delete (ID)') + ) + + return parser + + def take_action(self, parsed_args): + volume_client = self.app.client_manager.volume + + if volume_client.api_version < api_versions.APIVersion('3.3'): + msg = _( + "--os-volume-api-version 3.3 or greater is required to " + "support the 'volume message delete' command" + ) + raise exceptions.CommandError(msg) + + errors = 0 + for message_id in parsed_args.message_ids: + try: + volume_client.messages.delete(message_id) + except Exception: + LOG.error(_('Failed to delete message: %s'), message_id) + errors += 1 + + if errors > 0: + total = len(parsed_args.message_ids) + msg = _('Failed to delete %(errors)s of %(total)s messages.') % { + 'errors': errors, 'total': total, + } + raise exceptions.CommandError(msg) + + +class ListMessages(command.Lister): + _description = _('List volume failure messages') + + def get_parser(self, prog_name): + parser = super().get_parser(prog_name) + + parser.add_argument( + '--project', + metavar='<project>', + help=_('Filter results by project (name or ID) (admin only)'), + ) + identity_common.add_project_domain_option_to_parser(parser) + parser.add_argument( + '--marker', + metavar='<message-id>', + help=_('The last message ID of the previous page'), + default=None, + ) + parser.add_argument( + '--limit', + type=int, + metavar='<limit>', + help=_('Maximum number of messages to display'), + default=None, + ) + + return parser + + def take_action(self, parsed_args): + volume_client = self.app.client_manager.volume + identity_client = self.app.client_manager.identity + + if volume_client.api_version < api_versions.APIVersion('3.3'): + msg = _( + "--os-volume-api-version 3.3 or greater is required to " + "support the 'volume message list' command" + ) + raise exceptions.CommandError(msg) + + column_headers = ( + 'ID', + 'Event ID', + 'Resource Type', + 'Resource UUID', + 'Message Level', + 'User Message', + 'Request ID', + 'Created At', + 'Guaranteed Until', + ) + + project_id = None + if parsed_args.project: + project_id = identity_common.find_project( + identity_client, + parsed_args.project, + parsed_args.project_domain).id + + search_opts = { + 'project_id': project_id, + } + data = volume_client.messages.list( + search_opts=search_opts, + marker=parsed_args.marker, + limit=parsed_args.limit) + + return ( + column_headers, + (utils.get_item_properties(s, column_headers) for s in data) + ) + + +class ShowMessage(command.ShowOne): + _description = _('Show a volume failure message') + + def get_parser(self, prog_name): + parser = super(ShowMessage, self).get_parser(prog_name) + parser.add_argument( + 'message_id', + metavar='<message-id>', + help=_('Message to show (ID).') + ) + + return parser + + def take_action(self, parsed_args): + volume_client = self.app.client_manager.volume + + if volume_client.api_version < api_versions.APIVersion('3.3'): + msg = _( + "--os-volume-api-version 3.3 or greater is required to " + "support the 'volume message show' command" + ) + raise exceptions.CommandError(msg) + + message = volume_client.messages.get(parsed_args.message_id) + + return zip(*sorted(message._info.items())) |
