summaryrefslogtreecommitdiff
path: root/openstackclient/volume
diff options
context:
space:
mode:
Diffstat (limited to 'openstackclient/volume')
-rw-r--r--openstackclient/volume/client.py61
-rw-r--r--openstackclient/volume/v1/volume.py10
-rw-r--r--openstackclient/volume/v2/volume_backup.py300
-rw-r--r--openstackclient/volume/v2/volume_transfer_request.py36
-rw-r--r--openstackclient/volume/v3/volume_attachment.py511
-rw-r--r--openstackclient/volume/v3/volume_group.py506
-rw-r--r--openstackclient/volume/v3/volume_group_snapshot.py234
-rw-r--r--openstackclient/volume/v3/volume_group_type.py410
-rw-r--r--openstackclient/volume/v3/volume_message.py165
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()))