Files
python-cinderclient/cinderclient/v3/shell.py
Gorka Eguileor 45eb51eb99 Add service cleanup command
Cinder volume services will perform cleanup on start, but when we have
multiple volume services grouped in a cluster, we may want to trigger
cleanup of services that are down.

This patch adds command `work-cleanup` to trigger server cleanups and
prints service nodes that will be cleaned and those that didn't have an
alternative service in the cluster to do the cleanup.

This command will only work on servers supporting API version 3.24 or
higher.

New command:

cinder work-cleanup [--cluster <cluster-name>] [--host <hostname>]
                    [--binary <binary>]
                    [--is-up <True|true|False|false>]
                    [--disabled <True|true|False|false>]
                    [--resource-id <resource-id>]
                    [--resource-type <Volume|Snapshot>]

Specs: https://specs.openstack.org/openstack/cinder-specs/specs/newton/ha-aa-cleanup.html

Change-Id: I1c33ffbffcb14f34ee2bda9042e706937b1147d7
Depends-On: If336b6569b171846954ed6eb73f5a4314c6c7e2e
Implements: blueprint cinder-volume-active-active-support
2017-10-03 11:03:28 +02:00

2284 lines
84 KiB
Python

# Copyright (c) 2013-2014 OpenStack Foundation
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import print_function
import argparse
import collections
import os
import sys
from oslo_utils import strutils
import six
import cinderclient
from cinderclient import api_versions
from cinderclient import base
from cinderclient import exceptions
from cinderclient import shell_utils
from cinderclient import utils
from cinderclient.v2.shell import * # flake8: noqa
FILTER_DEPRECATED = ("This option is deprecated and will be removed in "
"newer release. Please use '--filters' option which "
"is introduced since 3.33 instead.")
@api_versions.wraps('3.33')
@utils.arg('--resource',
metavar='<resource>',
default=None,
help='Show enabled filters for specified resource. Default=None.')
def do_list_filters(cs, args):
"""List enabled filters.
Symbol '~' after filter key means it supports inexact filtering.
"""
filters = cs.resource_filters.list(resource=args.resource)
shell_utils.print_resource_filter_list(filters)
@utils.arg('--all-tenants',
metavar='<all_tenants>',
nargs='?',
type=int,
const=1,
default=0,
help='Shows details for all tenants. Admin only.')
@utils.arg('--all_tenants',
nargs='?',
type=int,
const=1,
help=argparse.SUPPRESS)
@utils.arg('--name',
metavar='<name>',
default=None,
help="Filters results by a name. Default=None. "
"%s" % FILTER_DEPRECATED)
@utils.arg('--status',
metavar='<status>',
default=None,
help="Filters results by a status. Default=None. "
"%s" % FILTER_DEPRECATED)
@utils.arg('--volume-id',
metavar='<volume-id>',
default=None,
help="Filters results by a volume ID. Default=None. "
"%s" % FILTER_DEPRECATED)
@utils.arg('--volume_id',
help=argparse.SUPPRESS)
@utils.arg('--marker',
metavar='<marker>',
default=None,
help='Begin returning backups that appear later in the backup '
'list than that represented by this id. '
'Default=None.')
@utils.arg('--limit',
metavar='<limit>',
default=None,
help='Maximum number of backups to return. Default=None.')
@utils.arg('--sort',
metavar='<key>[:<direction>]',
default=None,
help=(('Comma-separated list of sort keys and directions in the '
'form of <key>[:<asc|desc>]. '
'Valid keys: %s. '
'Default=None.') % ', '.join(base.SORT_KEY_VALUES)))
@utils.arg('--filters',
type=six.text_type,
nargs='*',
start_version='3.33',
metavar='<key=value>',
default=None,
help="Filter key and value pairs. Please use 'cinder list-filters' "
"to check enabled filters from server. Use 'key~=value' for "
"inexact filtering if the key supports. Default=None.")
def do_backup_list(cs, args):
"""Lists all backups."""
# pylint: disable=function-redefined
search_opts = {
'all_tenants': args.all_tenants,
'name': args.name,
'status': args.status,
'volume_id': args.volume_id,
}
# Update search option with `filters`
if hasattr(args, 'filters') and args.filters is not None:
search_opts.update(shell_utils.extract_filters(args.filters))
backups = cs.backups.list(search_opts=search_opts,
marker=args.marker,
limit=args.limit,
sort=args.sort)
shell_utils.translate_volume_snapshot_keys(backups)
columns = ['ID', 'Volume ID', 'Status', 'Name', 'Size', 'Object Count',
'Container']
if args.sort:
sortby_index = None
else:
sortby_index = 0
utils.print_list(backups, columns, sortby_index=sortby_index)
@utils.arg('--detail',
action='store_true',
help='Show detailed information about pools.')
@utils.arg('--filters',
type=six.text_type,
nargs='*',
start_version='3.33',
metavar='<key=value>',
default=None,
help="Filter key and value pairs. Please use 'cinder list-filters' "
"to check enabled filters from server, Default=None.")
def do_get_pools(cs, args):
"""Show pool information for backends. Admin only."""
# pylint: disable=function-redefined
search_opts = {}
# Update search option with `filters`
if hasattr(args, 'filters') and args.filters is not None:
search_opts.update(shell_utils.extract_filters(args.filters))
if cs.api_version >= api_versions.APIVersion("3.33"):
pools = cs.volumes.get_pools(args.detail, search_opts)
else:
pools = cs.volumes.get_pools(args.detail)
infos = dict()
infos.update(pools._info)
for info in infos['pools']:
backend = dict()
backend['name'] = info['name']
if args.detail:
backend.update(info['capabilities'])
utils.print_dict(backend)
RESET_STATE_RESOURCES = {'volume': utils.find_volume,
'backup': shell_utils.find_backup,
'snapshot': shell_utils.find_volume_snapshot,
'group': shell_utils.find_group,
'group-snapshot': shell_utils.find_group_snapshot}
@utils.arg('--group_id',
metavar='<group_id>',
default=None,
help="Filters results by a group_id. Default=None."
"%s" % FILTER_DEPRECATED,
start_version='3.10')
@utils.arg('--all-tenants',
dest='all_tenants',
metavar='<0|1>',
nargs='?',
type=int,
const=1,
default=0,
help='Shows details for all tenants. Admin only.')
@utils.arg('--all_tenants',
nargs='?',
type=int,
const=1,
help=argparse.SUPPRESS)
@utils.arg('--name',
metavar='<name>',
default=None,
help="Filters results by a name. Default=None. "
"%s" % FILTER_DEPRECATED)
@utils.arg('--display-name',
help=argparse.SUPPRESS)
@utils.arg('--status',
metavar='<status>',
default=None,
help="Filters results by a status. Default=None. "
"%s" % FILTER_DEPRECATED)
@utils.arg('--bootable',
metavar='<True|true|False|false>',
const=True,
nargs='?',
choices=['True', 'true', 'False', 'false'],
help="Filters results by bootable status. Default=None. "
"%s" % FILTER_DEPRECATED)
@utils.arg('--migration_status',
metavar='<migration_status>',
default=None,
help="Filters results by a migration status. Default=None. "
"Admin only. "
"%s" % FILTER_DEPRECATED)
@utils.arg('--metadata',
nargs='*',
metavar='<key=value>',
default=None,
help="Filters results by a metadata key and value pair. "
"Default=None. "
"%s" % FILTER_DEPRECATED)
@utils.arg('--image_metadata',
nargs='*',
metavar='<key=value>',
default=None,
start_version='3.4',
help="Filters results by a image metadata key and value pair. "
"Require volume api version >=3.4. Default=None."
"%s" % FILTER_DEPRECATED)
@utils.arg('--marker',
metavar='<marker>',
default=None,
help='Begin returning volumes that appear later in the volume '
'list than that represented by this volume id. '
'Default=None.')
@utils.arg('--limit',
metavar='<limit>',
default=None,
help='Maximum number of volumes to return. Default=None.')
@utils.arg('--fields',
default=None,
metavar='<fields>',
help='Comma-separated list of fields to display. '
'Use the show command to see which fields are available. '
'Unavailable/non-existent fields will be ignored. '
'Default=None.')
@utils.arg('--sort_key',
metavar='<sort_key>',
default=None,
help=argparse.SUPPRESS)
@utils.arg('--sort_dir',
metavar='<sort_dir>',
default=None,
help=argparse.SUPPRESS)
@utils.arg('--sort',
metavar='<key>[:<direction>]',
default=None,
help=(('Comma-separated list of sort keys and directions in the '
'form of <key>[:<asc|desc>]. '
'Valid keys: %s. '
'Default=None.') % ', '.join(base.SORT_KEY_VALUES)))
@utils.arg('--tenant',
type=str,
dest='tenant',
nargs='?',
metavar='<tenant>',
help='Display information from single tenant (Admin only).')
@utils.arg('--filters',
type=six.text_type,
nargs='*',
start_version='3.33',
metavar='<key=value>',
default=None,
help="Filter key and value pairs. Please use 'cinder list-filters' "
"to check enabled filters from server. Use 'key~=value' "
"for inexact filtering if the key supports. Default=None.")
def do_list(cs, args):
"""Lists all volumes."""
# pylint: disable=function-redefined
# NOTE(thingee): Backwards-compatibility with v1 args
if args.display_name is not None:
args.name = args.display_name
all_tenants = 1 if args.tenant else \
int(os.environ.get("ALL_TENANTS", args.all_tenants))
search_opts = {
'all_tenants': all_tenants,
'project_id': args.tenant,
'name': args.name,
'status': args.status,
'bootable': args.bootable,
'migration_status': args.migration_status,
'metadata': shell_utils.extract_metadata(args)
if args.metadata else None,
'glance_metadata': shell_utils.extract_metadata(args,
type='image_metadata')
if hasattr(args, 'image_metadata') and args.image_metadata else None,
'group_id': getattr(args, 'group_id', None),
}
# Update search option with `filters`
if hasattr(args, 'filters') and args.filters is not None:
search_opts.update(shell_utils.extract_filters(args.filters))
# If unavailable/non-existent fields are specified, these fields will
# be removed from key_list at the print_list() during key validation.
field_titles = []
if args.fields:
for field_title in args.fields.split(','):
field_titles.append(field_title)
# --sort_key and --sort_dir deprecated in kilo and is not supported
# with --sort
if args.sort and (args.sort_key or args.sort_dir):
raise exceptions.CommandError(
'The --sort_key and --sort_dir arguments are deprecated and are '
'not supported with --sort.')
volumes = cs.volumes.list(search_opts=search_opts, marker=args.marker,
limit=args.limit, sort_key=args.sort_key,
sort_dir=args.sort_dir, sort=args.sort)
shell_utils.translate_volume_keys(volumes)
# Create a list of servers to which the volume is attached
for vol in volumes:
servers = [s.get('server_id') for s in vol.attachments]
setattr(vol, 'attached_to', ','.join(map(str, servers)))
if field_titles:
# Remove duplicate fields
key_list = ['ID']
unique_titles = [k for k in collections.OrderedDict.fromkeys(
[x.title().strip() for x in field_titles]) if k != 'Id']
key_list.extend(unique_titles)
else:
key_list = ['ID', 'Status', 'Name', 'Size', 'Volume Type',
'Bootable', 'Attached to']
# If all_tenants is specified, print
# Tenant ID as well.
if search_opts['all_tenants']:
key_list.insert(1, 'Tenant ID')
if args.sort_key or args.sort_dir or args.sort:
sortby_index = None
else:
sortby_index = 0
utils.print_list(volumes, key_list, exclude_unavailable=True,
sortby_index=sortby_index)
@utils.arg('entity', metavar='<entity>', nargs='+',
help='Name or ID of entity to update.')
@utils.arg('--type', metavar='<type>', default='volume',
choices=RESET_STATE_RESOURCES.keys(),
help="Type of entity to update. Available resources "
"are: 'volume', 'snapshot', 'backup', "
"'group' (since 3.20) and "
"'group-snapshot' (since 3.19), Default=volume.")
@utils.arg('--state', metavar='<state>', default=None,
help=("The state to assign to the entity. "
"NOTE: This command simply changes the state of the "
"entity in the database with no regard to actual status, "
"exercise caution when using. Default=None, that means the "
"state is unchanged."))
@utils.arg('--attach-status', metavar='<attach-status>', default=None,
help=('This is only used for a volume entity. The attach status '
'to assign to the volume in the database, with no regard '
'to the actual status. Valid values are "attached" and '
'"detached". Default=None, that means the status '
'is unchanged.'))
@utils.arg('--reset-migration-status',
action='store_true',
help=('This is only used for a volume entity. Clears the migration '
'status of the volume in the DataBase that indicates the '
'volume is source or destination of volume migration, '
'with no regard to the actual status.'))
def do_reset_state(cs, args):
"""Explicitly updates the entity state in the Cinder database.
Being a database change only, this has no impact on the true state of the
entity and may not match the actual state. This can render a entity
unusable in the case of changing to the 'available' state.
"""
# pylint: disable=function-redefined
failure_count = 0
single = (len(args.entity) == 1)
migration_status = 'none' if args.reset_migration_status else None
collector = RESET_STATE_RESOURCES[args.type]
argument = (args.state,)
if args.type == 'volume':
argument += (args.attach_status, migration_status)
for entity in args.entity:
try:
collector(cs, entity).reset_state(*argument)
except Exception as e:
print(e)
failure_count += 1
msg = "Reset state for entity %s failed: %s" % (entity, e)
if not single:
print(msg)
if failure_count == len(args.entity):
msg = "Unable to reset the state for the specified entity(s)."
raise exceptions.CommandError(msg)
@utils.arg('size',
metavar='<size>',
nargs='?',
type=int,
action=CheckSizeArgForCreate,
help='Size of volume, in GiBs. (Required unless '
'snapshot-id/source-volid is specified).')
@utils.arg('--consisgroup-id',
metavar='<consistencygroup-id>',
default=None,
help='ID of a consistency group where the new volume belongs to. '
'Default=None.')
@utils.arg('--group-id',
metavar='<group-id>',
default=None,
help='ID of a group where the new volume belongs to. '
'Default=None.',
start_version='3.13')
@utils.arg('--snapshot-id',
metavar='<snapshot-id>',
default=None,
help='Creates volume from snapshot ID. Default=None.')
@utils.arg('--snapshot_id',
help=argparse.SUPPRESS)
@utils.arg('--source-volid',
metavar='<source-volid>',
default=None,
help='Creates volume from volume ID. Default=None.')
@utils.arg('--source_volid',
help=argparse.SUPPRESS)
@utils.arg('--source-replica',
metavar='<source-replica>',
default=None,
help='Creates volume from replicated volume ID. Default=None.')
@utils.arg('--image-id',
metavar='<image-id>',
default=None,
help='Creates volume from image ID. Default=None.')
@utils.arg('--image_id',
help=argparse.SUPPRESS)
@utils.arg('--image',
metavar='<image>',
default=None,
help='Creates a volume from image (ID or name). Default=None.')
@utils.arg('--image_ref',
help=argparse.SUPPRESS)
@utils.arg('--name',
metavar='<name>',
default=None,
help='Volume name. Default=None.')
@utils.arg('--display-name',
help=argparse.SUPPRESS)
@utils.arg('--display_name',
help=argparse.SUPPRESS)
@utils.arg('--description',
metavar='<description>',
default=None,
help='Volume description. Default=None.')
@utils.arg('--display-description',
help=argparse.SUPPRESS)
@utils.arg('--display_description',
help=argparse.SUPPRESS)
@utils.arg('--volume-type',
metavar='<volume-type>',
default=None,
help='Volume type. Default=None.')
@utils.arg('--volume_type',
help=argparse.SUPPRESS)
@utils.arg('--availability-zone',
metavar='<availability-zone>',
default=None,
help='Availability zone for volume. Default=None.')
@utils.arg('--availability_zone',
help=argparse.SUPPRESS)
@utils.arg('--metadata',
nargs='*',
metavar='<key=value>',
default=None,
help='Metadata key and value pairs. Default=None.')
@utils.arg('--hint',
metavar='<key=value>',
dest='scheduler_hints',
action='append',
default=[],
help='Scheduler hint, like in nova.')
@utils.arg('--allow-multiattach',
dest='multiattach',
action="store_true",
help=('Allow volume to be attached more than once.'
' Default=False'),
default=False)
@utils.arg('--poll',
action="store_true",
help=('Wait for volume creation until it completes.'))
def do_create(cs, args):
"""Creates a volume."""
# NOTE(thingee): Backwards-compatibility with v1 args
if args.display_name is not None:
args.name = args.display_name
if args.display_description is not None:
args.description = args.display_description
volume_metadata = None
if args.metadata is not None:
volume_metadata = shell_utils.extract_metadata(args)
# NOTE(N.S.): take this piece from novaclient
hints = {}
if args.scheduler_hints:
for hint in args.scheduler_hints:
key, _sep, value = hint.partition('=')
# NOTE(vish): multiple copies of same hint will
# result in a list of values
if key in hints:
if isinstance(hints[key], six.string_types):
hints[key] = [hints[key]]
hints[key] += [value]
else:
hints[key] = value
# NOTE(N.S.): end of taken piece
# Keep backward compatibility with image_id, favoring explicit ID
image_ref = args.image_id or args.image or args.image_ref
try:
group_id = args.group_id
except AttributeError:
group_id = None
volume = cs.volumes.create(args.size,
args.consisgroup_id,
group_id,
args.snapshot_id,
args.source_volid,
args.name,
args.description,
args.volume_type,
availability_zone=args.availability_zone,
imageRef=image_ref,
metadata=volume_metadata,
scheduler_hints=hints,
source_replica=args.source_replica,
multiattach=args.multiattach)
info = dict()
volume = cs.volumes.get(volume.id)
info.update(volume._info)
if 'readonly' in info['metadata']:
info['readonly'] = info['metadata']['readonly']
info.pop('links', None)
if args.poll:
timeout_period = os.environ.get("POLL_TIMEOUT_PERIOD", 3600)
shell_utils._poll_for_status(cs.volumes.get, volume.id, info, 'creating', ['available'],
timeout_period, cs.client.global_request_id, cs.messages)
utils.print_dict(info)
@utils.arg('volume',
metavar='<volume>',
help='Name or ID of volume for which to update metadata.')
@utils.arg('action',
metavar='<action>',
choices=['set', 'unset'],
help='The action. Valid values are "set" or "unset."')
@utils.arg('metadata',
metavar='<key=value>',
nargs='+',
default=[],
end_version='3.14',
help='Metadata key and value pair to set or unset. '
'For unset, specify only the key.')
@utils.arg('metadata',
metavar='<key=value>',
nargs='+',
default=[],
start_version='3.15',
help='Metadata key and value pair to set or unset. '
'For unset, specify only the key(s): <key key>')
def do_metadata(cs, args):
"""Sets or deletes volume metadata."""
volume = utils.find_volume(cs, args.volume)
metadata = shell_utils.extract_metadata(args)
if args.action == 'set':
cs.volumes.set_metadata(volume, metadata)
elif args.action == 'unset':
# NOTE(zul): Make sure py2/py3 sorting is the same
cs.volumes.delete_metadata(volume, sorted(metadata.keys(),
reverse=True))
@api_versions.wraps('3.12')
@utils.arg('--all-tenants',
dest='all_tenants',
metavar='<0|1>',
nargs='?',
type=int,
const=1,
default=utils.env('ALL_TENANTS', default=0),
help='Shows details for all tenants. Admin only.')
def do_summary(cs, args):
"""Get volumes summary."""
all_tenants = args.all_tenants
info = cs.volumes.summary(all_tenants)
formatters = ['total_size', 'total_count']
if cs.api_version >= api_versions.APIVersion("3.36"):
formatters.append('metadata')
utils.print_dict(info['volume-summary'], formatters=formatters)
@api_versions.wraps('3.11')
def do_group_type_list(cs, args):
"""Lists available 'group types'. (Admin only will see private types)"""
gtypes = cs.group_types.list()
shell_utils.print_group_type_list(gtypes)
@api_versions.wraps('3.11')
def do_group_type_default(cs, args):
"""List the default group type."""
gtype = cs.group_types.default()
shell_utils.print_group_type_list([gtype])
@api_versions.wraps('3.11')
@utils.arg('group_type',
metavar='<group_type>',
help='Name or ID of the group type.')
def do_group_type_show(cs, args):
"""Show group type details."""
gtype = shell_utils.find_gtype(cs, args.group_type)
info = dict()
info.update(gtype._info)
info.pop('links', None)
utils.print_dict(info, formatters=['group_specs'])
@api_versions.wraps('3.11')
@utils.arg('id',
metavar='<id>',
help='ID of the group type.')
@utils.arg('--name',
metavar='<name>',
help='Name of the group type.')
@utils.arg('--description',
metavar='<description>',
help='Description of the group type.')
@utils.arg('--is-public',
metavar='<is-public>',
help='Make type accessible to the public or not.')
def do_group_type_update(cs, args):
"""Updates group type name, description, and/or is_public."""
is_public = strutils.bool_from_string(args.is_public)
gtype = cs.group_types.update(args.id, args.name, args.description,
is_public)
shell_utils.print_group_type_list([gtype])
@api_versions.wraps('3.11')
def do_group_specs_list(cs, args):
"""Lists current group types and specs."""
gtypes = cs.group_types.list()
utils.print_list(gtypes, ['ID', 'Name', 'group_specs'])
@api_versions.wraps('3.11')
@utils.arg('name',
metavar='<name>',
help='Name of new group type.')
@utils.arg('--description',
metavar='<description>',
help='Description of new group type.')
@utils.arg('--is-public',
metavar='<is-public>',
default=True,
help='Make type accessible to the public (default true).')
def do_group_type_create(cs, args):
"""Creates a group type."""
is_public = strutils.bool_from_string(args.is_public)
gtype = cs.group_types.create(args.name, args.description, is_public)
shell_utils.print_group_type_list([gtype])
@api_versions.wraps('3.11')
@utils.arg('group_type',
metavar='<group_type>', nargs='+',
help='Name or ID of group type or types to delete.')
def do_group_type_delete(cs, args):
"""Deletes group type or types."""
failure_count = 0
for group_type in args.group_type:
try:
gtype = shell_utils.find_group_type(cs, group_type)
cs.group_types.delete(gtype)
print("Request to delete group type %s has been accepted."
% group_type)
except Exception as e:
failure_count += 1
print("Delete for group type %s failed: %s" % (group_type, e))
if failure_count == len(args.group_type):
raise exceptions.CommandError("Unable to delete any of the "
"specified types.")
@api_versions.wraps('3.11')
@utils.arg('gtype',
metavar='<gtype>',
help='Name or ID of group type.')
@utils.arg('action',
metavar='<action>',
choices=['set', 'unset'],
help='The action. Valid values are "set" or "unset."')
@utils.arg('metadata',
metavar='<key=value>',
nargs='+',
default=[],
help='The group specs key and value pair to set or unset. '
'For unset, specify only the key.')
def do_group_type_key(cs, args):
"""Sets or unsets group_spec for a group type."""
gtype = shell_utils.find_group_type(cs, args.gtype)
keypair = shell_utils.extract_metadata(args)
if args.action == 'set':
gtype.set_keys(keypair)
elif args.action == 'unset':
gtype.unset_keys(list(keypair))
@utils.arg('tenant',
metavar='<tenant_id>',
help='ID of tenant for which to set quotas.')
@utils.arg('--volumes',
metavar='<volumes>',
type=int, default=None,
help='The new "volumes" quota value. Default=None.')
@utils.arg('--snapshots',
metavar='<snapshots>',
type=int, default=None,
help='The new "snapshots" quota value. Default=None.')
@utils.arg('--gigabytes',
metavar='<gigabytes>',
type=int, default=None,
help='The new "gigabytes" quota value. Default=None.')
@utils.arg('--backups',
metavar='<backups>',
type=int, default=None,
help='The new "backups" quota value. Default=None.')
@utils.arg('--backup-gigabytes',
metavar='<backup_gigabytes>',
type=int, default=None,
help='The new "backup_gigabytes" quota value. Default=None.')
@utils.arg('--groups',
metavar='<groups>',
type=int, default=None,
help='The new "groups" quota value. Default=None.',
start_version='3.13')
@utils.arg('--volume-type',
metavar='<volume_type_name>',
default=None,
help='Volume type. Default=None.')
@utils.arg('--per-volume-gigabytes',
metavar='<per_volume_gigabytes>',
type=int, default=None,
help='Set max volume size limit. Default=None.')
@utils.arg('--skip-validation',
metavar='<skip_validation>',
default=False,
help='Skip validate the existing resource quota. Default=False.')
def do_quota_update(cs, args):
"""Updates quotas for a tenant."""
shell_utils.quota_update(cs.quotas, args.tenant, args)
@utils.arg('volume',
metavar='<volume>',
help='Name or ID of volume to snapshot.')
@utils.arg('--force',
metavar='<True|False>',
const=True,
nargs='?',
default=False,
help='Enables or disables upload of '
'a volume that is attached to an instance. '
'Default=False. '
'This option may not be supported by your cloud.')
@utils.arg('--container-format',
metavar='<container-format>',
default='bare',
help='Container format type. '
'Default is bare.')
@utils.arg('--container_format',
help=argparse.SUPPRESS)
@utils.arg('--disk-format',
metavar='<disk-format>',
default='raw',
help='Disk format type. '
'Default is raw.')
@utils.arg('--disk_format',
help=argparse.SUPPRESS)
@utils.arg('image_name',
metavar='<image-name>',
help='The new image name.')
@utils.arg('--image_name',
help=argparse.SUPPRESS)
@utils.arg('--visibility',
metavar='<public|private|community|shared>',
help='Set image visibility to public, private, community or '
'shared. Default=private.',
default='private',
start_version='3.1')
@utils.arg('--protected',
metavar='<True|False>',
help='Prevents image from being deleted. Default=False.',
default=False,
start_version='3.1')
def do_upload_to_image(cs, args):
"""Uploads volume to Image Service as an image."""
volume = utils.find_volume(cs, args.volume)
if cs.api_version >= api_versions.APIVersion("3.1"):
shell_utils.print_volume_image(
volume.upload_to_image(args.force,
args.image_name,
args.container_format,
args.disk_format,
args.visibility,
args.protected))
else:
shell_utils.print_volume_image(
volume.upload_to_image(args.force,
args.image_name,
args.container_format,
args.disk_format))
@utils.arg('volume', metavar='<volume>', help='ID of volume to migrate.')
# NOTE(geguileo): host is positional but optional in order to maintain backward
# compatibility even with mutually exclusive arguments. If version is < 3.16
# then only host positional argument will be possible, and since the
# exclusive_arg group has required=True it will be required even if it's
# optional.
@utils.exclusive_arg('destination', 'host', required=True, nargs='?',
metavar='<host>', help='Destination host. Takes the '
'form: host@backend-name#pool')
@utils.exclusive_arg('destination', '--cluster', required=True,
help='Destination cluster. Takes the form: '
'cluster@backend-name#pool',
start_version='3.16')
@utils.arg('--force-host-copy', metavar='<True|False>',
choices=['True', 'False'],
required=False,
const=True,
nargs='?',
default=False,
help='Enables or disables generic host-based '
'force-migration, which bypasses driver '
'optimizations. Default=False.')
@utils.arg('--lock-volume', metavar='<True|False>',
choices=['True', 'False'],
required=False,
const=True,
nargs='?',
default=False,
help='Enables or disables the termination of volume migration '
'caused by other commands. This option applies to the '
'available volume. True means it locks the volume '
'state and does not allow the migration to be aborted. The '
'volume status will be in maintenance during the '
'migration. False means it allows the volume migration '
'to be aborted. The volume status is still in the original '
'status. Default=False.')
def do_migrate(cs, args):
"""Migrates volume to a new host."""
volume = utils.find_volume(cs, args.volume)
try:
volume.migrate_volume(args.host, args.force_host_copy,
args.lock_volume, getattr(args, 'cluster', None))
print("Request to migrate volume %s has been accepted." % (volume.id))
except Exception as e:
print("Migration for volume %s failed: %s." % (volume.id,
six.text_type(e)))
@api_versions.wraps('3.9', '3.43')
@utils.arg('backup', metavar='<backup>',
help='Name or ID of backup to rename.')
@utils.arg('--name', nargs='?', metavar='<name>',
help='New name for backup.')
@utils.arg('--description', metavar='<description>',
help='Backup description. Default=None.')
@utils.arg('--metadata',
nargs='*',
metavar='<key=value>',
default=None,
help='Metadata key and value pairs. Default=None.',
start_version='3.43')
def do_backup_update(cs, args):
"""Renames a backup."""
kwargs = {}
if args.name is not None:
kwargs['name'] = args.name
if args.description is not None:
kwargs['description'] = args.description
if cs.api_version >= api_versions.APIVersion("3.43"):
if args.metadata is not None:
kwargs['metadata'] = args.metadata
if not kwargs:
msg = 'Must supply either name or description.'
raise exceptions.ClientException(code=1, message=msg)
shell_utils.find_backup(cs, args.backup).update(**kwargs)
print("Request to update backup '%s' has been accepted." % args.backup)
@api_versions.wraps('3.7')
@utils.arg('--name', metavar='<name>', default=None,
help='Filter by cluster name, without backend will list all '
'clustered services from the same cluster. Default=None.')
@utils.arg('--binary', metavar='<binary>', default=None,
help='Cluster binary. Default=None.')
@utils.arg('--is-up', metavar='<True|true|False|false>', default=None,
choices=('True', 'true', 'False', 'false'),
help='Filter by up/dow status. Default=None.')
@utils.arg('--disabled', metavar='<True|true|False|false>', default=None,
choices=('True', 'true', 'False', 'false'),
help='Filter by disabled status. Default=None.')
@utils.arg('--num-hosts', metavar='<num-hosts>', default=None,
help='Filter by number of hosts in the cluster.')
@utils.arg('--num-down-hosts', metavar='<num-down-hosts>', default=None,
help='Filter by number of hosts that are down.')
@utils.arg('--detailed', dest='detailed', default=False,
help='Get detailed clustered service information (Default=False).',
action='store_true')
def do_cluster_list(cs, args):
"""Lists clustered services with optional filtering."""
clusters = cs.clusters.list(name=args.name, binary=args.binary,
is_up=args.is_up, disabled=args.disabled,
num_hosts=args.num_hosts,
num_down_hosts=args.num_down_hosts,
detailed=args.detailed)
columns = ['Name', 'Binary', 'State', 'Status']
if args.detailed:
columns.extend(('Num Hosts', 'Num Down Hosts', 'Last Heartbeat',
'Disabled Reason', 'Created At', 'Updated at'))
utils.print_list(clusters, columns)
@api_versions.wraps('3.7')
@utils.arg('binary', metavar='<binary>', nargs='?', default='cinder-volume',
help='Binary to filter by. Default: cinder-volume.')
@utils.arg('name', metavar='<cluster-name>',
help='Name of the clustered service to show.')
def do_cluster_show(cs, args):
"""Show detailed information on a clustered service."""
cluster = cs.clusters.show(args.name, args.binary)
utils.print_dict(cluster.to_dict())
@api_versions.wraps('3.7')
@utils.arg('binary', metavar='<binary>', nargs='?', default='cinder-volume',
help='Binary to filter by. Default: cinder-volume.')
@utils.arg('name', metavar='<cluster-name>',
help='Name of the clustered services to update.')
def do_cluster_enable(cs, args):
"""Enables clustered services."""
cluster = cs.clusters.update(args.name, args.binary, disabled=False)
utils.print_dict(cluster.to_dict())
@api_versions.wraps('3.7')
@utils.arg('binary', metavar='<binary>', nargs='?', default='cinder-volume',
help='Binary to filter by. Default: cinder-volume.')
@utils.arg('name', metavar='<cluster-name>',
help='Name of the clustered services to update.')
@utils.arg('--reason', metavar='<reason>', default=None,
help='Reason for disabling clustered service.')
def do_cluster_disable(cs, args):
"""Disables clustered services."""
cluster = cs.clusters.update(args.name, args.binary, disabled=True,
disabled_reason=args.reason)
utils.print_dict(cluster.to_dict())
@api_versions.wraps('3.24')
@utils.arg('--cluster', metavar='<cluster-name>', default=None,
help='Cluster name. Default=None.')
@utils.arg('--host', metavar='<hostname>', default=None,
help='Service host name. Default=None.')
@utils.arg('--binary', metavar='<binary>', default=None,
help='Service binary. Default=None.')
@utils.arg('--is-up', metavar='<True|true|False|false>', dest='is_up',
default=None, choices=('True', 'true', 'False', 'false'),
help='Filter by up/down status, if set to true services need to be'
' up, if set to false services need to be down. Default is '
'None, which means up/down status is ignored.')
@utils.arg('--disabled', metavar='<True|true|False|false>', default=None,
choices=('True', 'true', 'False', 'false'),
help='Filter by disabled status. Default=None.')
@utils.arg('--resource-id', metavar='<resource-id>', default=None,
help='UUID of a resource to cleanup. Default=None.')
@utils.arg('--resource-type', metavar='<Volume|Snapshot>', default=None,
choices=('Volume', 'Snapshot'),
help='Type of resource to cleanup.')
def do_work_cleanup(cs, args):
"""Request cleanup of services with optional filtering."""
filters = dict(cluster_name=args.cluster, host=args.host,
binary=args.binary, is_up=args.is_up,
disabled=args.disabled, resource_id=args.resource_id,
resource_type=args.resource_type)
filters = {k: v for k, v in filters.items() if v is not None}
cleaning, unavailable = cs.workers.clean(**filters)
columns = ('ID', 'Cluster Name', 'Host', 'Binary')
if cleaning:
print('Following services will be cleaned:')
utils.print_list(cleaning, columns)
if unavailable:
print('There are no alternative nodes to do cleanup for the following '
'services:')
utils.print_list(unavailable, columns)
if not (cleaning or unavailable):
print('No cleanable services matched cleanup criteria.')
@utils.arg('host',
metavar='<host>',
help='Cinder host on which the existing volume resides; '
'takes the form: host@backend-name#pool')
@utils.arg('--cluster',
help='Cinder cluster on which the existing volume resides; '
'takes the form: cluster@backend-name#pool',
start_version='3.16')
@utils.arg('identifier',
metavar='<identifier>',
help='Name or other Identifier for existing volume')
@utils.arg('--id-type',
metavar='<id-type>',
default='source-name',
help='Type of backend device identifier provided, '
'typically source-name or source-id (Default=source-name)')
@utils.arg('--name',
metavar='<name>',
help='Volume name (Default=None)')
@utils.arg('--description',
metavar='<description>',
help='Volume description (Default=None)')
@utils.arg('--volume-type',
metavar='<volume-type>',
help='Volume type (Default=None)')
@utils.arg('--availability-zone',
metavar='<availability-zone>',
help='Availability zone for volume (Default=None)')
@utils.arg('--metadata',
type=str,
nargs='*',
metavar='<key=value>',
help='Metadata key=value pairs (Default=None)')
@utils.arg('--bootable',
action='store_true',
help='Specifies that the newly created volume should be'
' marked as bootable')
def do_manage(cs, args):
"""Manage an existing volume."""
volume_metadata = None
if args.metadata is not None:
volume_metadata = shell_utils.extract_metadata(args)
# Build a dictionary of key/value pairs to pass to the API.
ref_dict = {args.id_type: args.identifier}
# The recommended way to specify an existing volume is by ID or name, and
# have the Cinder driver look for 'source-name' or 'source-id' elements in
# the ref structure. To make things easier for the user, we have special
# --source-name and --source-id CLI options that add the appropriate
# element to the ref structure.
#
# Note how argparse converts hyphens to underscores. We use hyphens in the
# dictionary so that it is consistent with what the user specified on the
# CLI.
if hasattr(args, 'source_name') and args.source_name is not None:
ref_dict['source-name'] = args.source_name
if hasattr(args, 'source_id') and args.source_id is not None:
ref_dict['source-id'] = args.source_id
volume = cs.volumes.manage(host=args.host,
ref=ref_dict,
name=args.name,
description=args.description,
volume_type=args.volume_type,
availability_zone=args.availability_zone,
metadata=volume_metadata,
bootable=args.bootable,
cluster=getattr(args, 'cluster', None))
info = {}
volume = cs.volumes.get(volume.id)
info.update(volume._info)
info.pop('links', None)
utils.print_dict(info)
@api_versions.wraps('3.8')
# NOTE(geguileo): host is positional but optional in order to maintain backward
# compatibility even with mutually exclusive arguments. If version is < 3.16
# then only host positional argument will be possible, and since the
# exclusive_arg group has required=True it will be required even if it's
# optional.
@utils.exclusive_arg('source', 'host', required=True, nargs='?',
metavar='<host>',
help='Cinder host on which to list manageable volumes; '
'takes the form: host@backend-name#pool')
@utils.exclusive_arg('source', '--cluster', required=True,
metavar='CLUSTER',
help='Cinder cluster on which to list manageable '
'volumes; takes the form: cluster@backend-name#pool',
start_version='3.17')
@utils.arg('--detailed',
metavar='<detailed>',
default=True,
help='Returned detailed information (default true).')
@utils.arg('--marker',
metavar='<marker>',
default=None,
help='Begin returning volumes that appear later in the volume '
'list than that represented by this reference. This reference '
'should be json like. '
'Default=None.')
@utils.arg('--limit',
metavar='<limit>',
default=None,
help='Maximum number of volumes to return. Default=None.')
@utils.arg('--offset',
metavar='<offset>',
default=None,
help='Number of volumes to skip after marker. Default=None.')
@utils.arg('--sort',
metavar='<key>[:<direction>]',
default=None,
help=(('Comma-separated list of sort keys and directions in the '
'form of <key>[:<asc|desc>]. '
'Valid keys: %s. '
'Default=None.'
) % ', '.join(base.SORT_MANAGEABLE_KEY_VALUES)))
def do_manageable_list(cs, args):
"""Lists all manageable volumes."""
# pylint: disable=function-redefined
detailed = strutils.bool_from_string(args.detailed)
cluster = getattr(args, 'cluster', None)
volumes = cs.volumes.list_manageable(host=args.host, detailed=detailed,
marker=args.marker, limit=args.limit,
offset=args.offset, sort=args.sort,
cluster=cluster)
columns = ['reference', 'size', 'safe_to_manage']
if detailed:
columns.extend(['reason_not_safe', 'cinder_id', 'extra_info'])
utils.print_list(volumes, columns, sortby_index=None)
@api_versions.wraps('3.13')
@utils.arg('--all-tenants',
dest='all_tenants',
metavar='<0|1>',
nargs='?',
type=int,
const=1,
default=utils.env('ALL_TENANTS', default=0),
help='Shows details for all tenants. Admin only.')
@utils.arg('--filters',
type=six.text_type,
nargs='*',
start_version='3.33',
metavar='<key=value>',
default=None,
help="Filter key and value pairs. Please use 'cinder list-filters' "
"to check enabled filters from server. Use 'key~=value' "
"for inexact filtering if the key supports. Default=None.")
def do_group_list(cs, args):
"""Lists all groups."""
search_opts = {'all_tenants': args.all_tenants}
# Update search option with `filters`
if hasattr(args, 'filters') and args.filters is not None:
search_opts.update(shell_utils.extract_filters(args.filters))
groups = cs.groups.list(search_opts=search_opts)
columns = ['ID', 'Status', 'Name']
utils.print_list(groups, columns)
@api_versions.wraps('3.13')
@utils.arg('--list-volume',
dest='list_volume',
metavar='<False|True>',
nargs='?',
type=bool,
const=True,
default=False,
help='Shows volumes included in the group.',
start_version='3.25')
@utils.arg('group',
metavar='<group>',
help='Name or ID of a group.')
def do_group_show(cs, args):
"""Shows details of a group."""
info = dict()
if getattr(args, 'list_volume', None):
group = shell_utils.find_group(cs, args.group,
list_volume=args.list_volume)
else:
group = shell_utils.find_group(cs, args.group)
info.update(group._info)
info.pop('links', None)
utils.print_dict(info)
@api_versions.wraps('3.13')
@utils.arg('grouptype',
metavar='<group-type>',
help='Group type.')
@utils.arg('volumetypes',
metavar='<volume-types>',
help='Comma-separated list of volume types.')
@utils.arg('--name',
metavar='<name>',
help='Name of a group.')
@utils.arg('--description',
metavar='<description>',
default=None,
help='Description of a group. Default=None.')
@utils.arg('--availability-zone',
metavar='<availability-zone>',
default=None,
help='Availability zone for group. Default=None.')
def do_group_create(cs, args):
"""Creates a group."""
group = cs.groups.create(
args.grouptype,
args.volumetypes,
args.name,
args.description,
availability_zone=args.availability_zone)
info = dict()
group = cs.groups.get(group.id)
info.update(group._info)
info.pop('links', None)
utils.print_dict(info)
@api_versions.wraps('3.14')
@utils.arg('--group-snapshot',
metavar='<group-snapshot>',
help='Name or ID of a group snapshot. Default=None.')
@utils.arg('--source-group',
metavar='<source-group>',
help='Name or ID of a source group. Default=None.')
@utils.arg('--name',
metavar='<name>',
help='Name of a group. Default=None.')
@utils.arg('--description',
metavar='<description>',
help='Description of a group. Default=None.')
def do_group_create_from_src(cs, args):
"""Creates a group from a group snapshot or a source group."""
if not args.group_snapshot and not args.source_group:
msg = ('Cannot create group because neither '
'group snapshot nor source group is provided.')
raise exceptions.ClientException(code=1, message=msg)
if args.group_snapshot and args.source_group:
msg = ('Cannot create group because both '
'group snapshot and source group are provided.')
raise exceptions.ClientException(code=1, message=msg)
group_snapshot = None
if args.group_snapshot:
group_snapshot = shell_utils.find_group_snapshot(cs,
args.group_snapshot)
source_group = None
if args.source_group:
source_group = shell_utils.find_group(cs, args.source_group)
info = cs.groups.create_from_src(
group_snapshot.id if group_snapshot else None,
source_group.id if source_group else None,
args.name,
args.description)
info.pop('links', None)
utils.print_dict(info)
@api_versions.wraps('3.13')
@utils.arg('group',
metavar='<group>', nargs='+',
help='Name or ID of one or more groups '
'to be deleted.')
@utils.arg('--delete-volumes',
action='store_true',
default=False,
help='Allows or disallows groups to be deleted '
'if they are not empty. If the group is empty, '
'it can be deleted without the delete-volumes flag. '
'If the group is not empty, the delete-volumes '
'flag is required for it to be deleted. If True, '
'all volumes in the group will also be deleted.')
def do_group_delete(cs, args):
"""Removes one or more groups."""
failure_count = 0
for group in args.group:
try:
shell_utils.find_group(cs, group).delete(args.delete_volumes)
except Exception as e:
failure_count += 1
print("Delete for group %s failed: %s" %
(group, e))
if failure_count == len(args.group):
raise exceptions.CommandError("Unable to delete any of the specified "
"groups.")
@api_versions.wraps('3.13')
@utils.arg('group',
metavar='<group>',
help='Name or ID of a group.')
@utils.arg('--name', metavar='<name>',
help='New name for group. Default=None.')
@utils.arg('--description', metavar='<description>',
help='New description for group. Default=None.')
@utils.arg('--add-volumes',
metavar='<uuid1,uuid2,......>',
help='UUID of one or more volumes '
'to be added to the group, '
'separated by commas. Default=None.')
@utils.arg('--remove-volumes',
metavar='<uuid3,uuid4,......>',
help='UUID of one or more volumes '
'to be removed from the group, '
'separated by commas. Default=None.')
def do_group_update(cs, args):
"""Updates a group."""
kwargs = {}
if args.name is not None:
kwargs['name'] = args.name
if args.description is not None:
kwargs['description'] = args.description
if args.add_volumes is not None:
kwargs['add_volumes'] = args.add_volumes
if args.remove_volumes is not None:
kwargs['remove_volumes'] = args.remove_volumes
if not kwargs:
msg = ('At least one of the following args must be supplied: '
'name, description, add-volumes, remove-volumes.')
raise exceptions.ClientException(code=1, message=msg)
shell_utils.find_group(cs, args.group).update(**kwargs)
print("Request to update group '%s' has been accepted." % args.group)
@api_versions.wraps('3.38')
@utils.arg('group',
metavar='<group>',
help='Name or ID of the group.')
def do_group_enable_replication(cs, args):
"""Enables replication for group."""
shell_utils.find_group(cs, args.group).enable_replication()
@api_versions.wraps('3.38')
@utils.arg('group',
metavar='<group>',
help='Name or ID of the group.')
def do_group_disable_replication(cs, args):
"""Disables replication for group."""
shell_utils.find_group(cs, args.group).disable_replication()
@api_versions.wraps('3.38')
@utils.arg('group',
metavar='<group>',
help='Name or ID of the group.')
@utils.arg('--allow-attached-volume',
action='store_true',
default=False,
help='Allows or disallows group with '
'attached volumes to be failed over.')
@utils.arg('--secondary-backend-id',
metavar='<secondary_backend_id>',
help='Secondary backend id. Default=None.')
def do_group_failover_replication(cs, args):
"""Fails over replication for group."""
shell_utils.find_group(cs, args.group).failover_replication(
allow_attached_volume=args.allow_attached_volume,
secondary_backend_id=args.secondary_backend_id)
@api_versions.wraps('3.38')
@utils.arg('group',
metavar='<group>',
help='Name or ID of the group.')
def do_group_list_replication_targets(cs, args):
"""Lists replication targets for group.
Example value for replication_targets:
.. code-block: json
{
'replication_targets': [{'backend_id': 'vendor-id-1',
'unique_key': 'val1',
......},
{'backend_id': 'vendor-id-2',
'unique_key': 'val2',
......}]
}
"""
rc, replication_targets = shell_utils.find_group(
cs, args.group).list_replication_targets()
rep_targets = replication_targets.get('replication_targets')
if rep_targets and len(rep_targets) > 0:
utils.print_list(rep_targets, [key for key in rep_targets[0].keys()])
@utils.arg('--all-tenants',
dest='all_tenants',
metavar='<0|1>',
nargs='?',
type=int,
const=1,
default=0,
help='Shows details for all tenants. Admin only.')
@utils.arg('--status',
metavar='<status>',
default=None,
help="Filters results by a status. Default=None. "
"%s" % FILTER_DEPRECATED)
@utils.arg('--group-id',
metavar='<group_id>',
default=None,
help="Filters results by a group ID. Default=None. "
"%s" % FILTER_DEPRECATED)
@utils.arg('--filters',
type=six.text_type,
nargs='*',
start_version='3.33',
metavar='<key=value>',
default=None,
help="Filter key and value pairs. Please use 'cinder list-filters' "
"to check enabled filters from server. Use 'key~=value' "
"for inexact filtering if the key supports. Default=None.")
def do_group_snapshot_list(cs, args):
"""Lists all group snapshots."""
all_tenants = int(os.environ.get("ALL_TENANTS", args.all_tenants))
search_opts = {
'all_tenants': all_tenants,
'status': args.status,
'group_id': args.group_id,
}
# Update search option with `filters`
if hasattr(args, 'filters') and args.filters is not None:
search_opts.update(shell_utils.extract_filters(args.filters))
group_snapshots = cs.group_snapshots.list(search_opts=search_opts)
columns = ['ID', 'Status', 'Name']
utils.print_list(group_snapshots, columns)
@api_versions.wraps('3.14')
@utils.arg('group_snapshot',
metavar='<group_snapshot>',
help='Name or ID of group snapshot.')
def do_group_snapshot_show(cs, args):
"""Shows group snapshot details."""
info = dict()
group_snapshot = shell_utils.find_group_snapshot(cs, args.group_snapshot)
info.update(group_snapshot._info)
info.pop('links', None)
utils.print_dict(info)
@api_versions.wraps('3.14')
@utils.arg('group',
metavar='<group>',
help='Name or ID of a group.')
@utils.arg('--name',
metavar='<name>',
default=None,
help='Group snapshot name. Default=None.')
@utils.arg('--description',
metavar='<description>',
default=None,
help='Group snapshot description. Default=None.')
def do_group_snapshot_create(cs, args):
"""Creates a group snapshot."""
group = shell_utils.find_group(cs, args.group)
group_snapshot = cs.group_snapshots.create(
group.id,
args.name,
args.description)
info = dict()
group_snapshot = cs.group_snapshots.get(group_snapshot.id)
info.update(group_snapshot._info)
info.pop('links', None)
utils.print_dict(info)
@api_versions.wraps('3.14')
@utils.arg('group_snapshot',
metavar='<group_snapshot>', nargs='+',
help='Name or ID of one or more group snapshots to be deleted.')
def do_group_snapshot_delete(cs, args):
"""Removes one or more group snapshots."""
failure_count = 0
for group_snapshot in args.group_snapshot:
try:
shell_utils.find_group_snapshot(cs, group_snapshot).delete()
except Exception as e:
failure_count += 1
print("Delete for group snapshot %s failed: %s" %
(group_snapshot, e))
if failure_count == len(args.group_snapshot):
raise exceptions.CommandError("Unable to delete any of the specified "
"group snapshots.")
@api_versions.wraps('3.0')
@utils.arg('--host', metavar='<hostname>', default=None,
help='Host name. Default=None.')
@utils.arg('--binary', metavar='<binary>', default=None,
help='Service binary. Default=None.')
@utils.arg('--withreplication',
metavar='<True|False>',
const=True,
nargs='?',
default=False,
start_version='3.7',
help='Enables or disables display of '
'Replication info for c-vol services. Default=False.')
def do_service_list(cs, args):
"""Lists all services. Filter by host and service binary."""
if hasattr(args, 'withreplication'):
replication = strutils.bool_from_string(args.withreplication,
strict=True)
else:
replication = False
result = cs.services.list(host=args.host, binary=args.binary)
columns = ["Binary", "Host", "Zone", "Status", "State", "Updated_at"]
if cs.api_version.matches('3.7'):
columns.append('Cluster')
if replication:
columns.extend(["Replication Status", "Active Backend ID", "Frozen"])
# NOTE(jay-lau-513): we check if the response has disabled_reason
# so as not to add the column when the extended ext is not enabled.
if result and hasattr(result[0], 'disabled_reason'):
columns.append("Disabled Reason")
utils.print_list(result, columns)
@api_versions.wraps('3.8')
# NOTE(geguileo): host is positional but optional in order to maintain backward
# compatibility even with mutually exclusive arguments. If version is < 3.16
# then only host positional argument will be possible, and since the
# exclusive_arg group has required=True it will be required even if it's
# optional.
@utils.exclusive_arg('source', 'host', required=True, nargs='?',
metavar='<host>',
help='Cinder host on which to list manageable snapshots; '
'takes the form: host@backend-name#pool')
@utils.exclusive_arg('source', '--cluster', required=True,
help='Cinder cluster on which to list manageable '
'snapshots; takes the form: cluster@backend-name#pool',
start_version='3.17')
@utils.arg('--detailed',
metavar='<detailed>',
default=True,
help='Returned detailed information (default true).')
@utils.arg('--marker',
metavar='<marker>',
default=None,
help='Begin returning volumes that appear later in the volume '
'list than that represented by this reference. This reference '
'should be json like. '
'Default=None.')
@utils.arg('--limit',
metavar='<limit>',
default=None,
help='Maximum number of volumes to return. Default=None.')
@utils.arg('--offset',
metavar='<offset>',
default=None,
help='Number of volumes to skip after marker. Default=None.')
@utils.arg('--sort',
metavar='<key>[:<direction>]',
default=None,
help=(('Comma-separated list of sort keys and directions in the '
'form of <key>[:<asc|desc>]. '
'Valid keys: %s. '
'Default=None.'
) % ', '.join(base.SORT_MANAGEABLE_KEY_VALUES)))
def do_snapshot_manageable_list(cs, args):
"""Lists all manageable snapshots."""
# pylint: disable=function-redefined
detailed = strutils.bool_from_string(args.detailed)
cluster = getattr(args, 'cluster', None)
snapshots = cs.volume_snapshots.list_manageable(host=args.host,
detailed=detailed,
marker=args.marker,
limit=args.limit,
offset=args.offset,
sort=args.sort,
cluster=cluster)
columns = ['reference', 'size', 'safe_to_manage', 'source_reference']
if detailed:
columns.extend(['reason_not_safe', 'cinder_id', 'extra_info'])
utils.print_list(snapshots, columns, sortby_index=None)
@api_versions.wraps("3.0")
def do_api_version(cs, args):
"""Display the server API version information."""
columns = ['ID', 'Status', 'Version', 'Min_version']
response = cs.services.server_api_version()
utils.print_list(response, columns)
@api_versions.wraps("3.40")
@utils.arg(
'snapshot',
metavar='<snapshot>',
help='Name or ID of the snapshot to restore. The snapshot must be the '
'most recent one known to cinder.')
def do_revert_to_snapshot(cs, args):
"""Revert a volume to the specified snapshot."""
snapshot = shell_utils.find_volume_snapshot(cs, args.snapshot)
volume = utils.find_volume(cs, snapshot.volume_id)
volume.revert_to_snapshot(snapshot)
@api_versions.wraps("3.3")
@utils.arg('--marker',
metavar='<marker>',
default=None,
start_version='3.5',
help='Begin returning message that appear later in the message '
'list than that represented by this id. '
'Default=None.')
@utils.arg('--limit',
metavar='<limit>',
default=None,
start_version='3.5',
help='Maximum number of messages to return. Default=None.')
@utils.arg('--sort',
metavar='<key>[:<direction>]',
default=None,
start_version='3.5',
help=(('Comma-separated list of sort keys and directions in the '
'form of <key>[:<asc|desc>]. '
'Valid keys: %s. '
'Default=None.') % ', '.join(base.SORT_KEY_VALUES)))
@utils.arg('--resource_uuid',
metavar='<resource_uuid>',
default=None,
help="Filters results by a resource uuid. Default=None. "
"%s" % FILTER_DEPRECATED)
@utils.arg('--resource_type',
metavar='<type>',
default=None,
help="Filters results by a resource type. Default=None. "
"%s" % FILTER_DEPRECATED)
@utils.arg('--event_id',
metavar='<id>',
default=None,
help="Filters results by event id. Default=None. "
"%s" % FILTER_DEPRECATED)
@utils.arg('--request_id',
metavar='<request_id>',
default=None,
help="Filters results by request id. Default=None. "
"%s" % FILTER_DEPRECATED)
@utils.arg('--level',
metavar='<level>',
default=None,
help="Filters results by the message level. Default=None. "
"%s" % FILTER_DEPRECATED)
@utils.arg('--filters',
type=six.text_type,
nargs='*',
start_version='3.33',
metavar='<key=value>',
default=None,
help="Filter key and value pairs. Please use 'cinder list-filters' "
"to check enabled filters from server. Use 'key~=value' "
"for inexact filtering if the key supports. Default=None.")
def do_message_list(cs, args):
"""Lists all messages."""
search_opts = {
'resource_uuid': args.resource_uuid,
'event_id': args.event_id,
'request_id': args.request_id,
}
# Update search option with `filters`
if hasattr(args, 'filters') and args.filters is not None:
search_opts.update(shell_utils.extract_filters(args.filters))
if args.resource_type:
search_opts['resource_type'] = args.resource_type.upper()
if args.level:
search_opts['message_level'] = args.level.upper()
marker = args.marker if hasattr(args, 'marker') else None
limit = args.limit if hasattr(args, 'limit') else None
sort = args.sort if hasattr(args, 'sort') else None
messages = cs.messages.list(search_opts=search_opts,
marker=marker,
limit=limit,
sort=sort)
columns = ['ID', 'Resource Type', 'Resource UUID', 'Event ID',
'User Message']
if sort:
sortby_index = None
else:
sortby_index = 0
utils.print_list(messages, columns, sortby_index=sortby_index)
@api_versions.wraps("3.3")
@utils.arg('message',
metavar='<message>',
help='ID of message.')
def do_message_show(cs, args):
"""Shows message details."""
info = dict()
message = shell_utils.find_message(cs, args.message)
info.update(message._info)
info.pop('links', None)
utils.print_dict(info)
@api_versions.wraps("3.3")
@utils.arg('message',
metavar='<message>', nargs='+',
help='ID of one or more message to be deleted.')
def do_message_delete(cs, args):
"""Removes one or more messages."""
failure_count = 0
for message in args.message:
try:
shell_utils.find_message(cs, message).delete()
except Exception as e:
failure_count += 1
print("Delete for message %s failed: %s" % (message, e))
if failure_count == len(args.message):
raise exceptions.CommandError("Unable to delete any of the specified "
"messages.")
@utils.arg('--all-tenants',
dest='all_tenants',
metavar='<0|1>',
nargs='?',
type=int,
const=1,
default=0,
help='Shows details for all tenants. Admin only.')
@utils.arg('--all_tenants',
nargs='?',
type=int,
const=1,
help=argparse.SUPPRESS)
@utils.arg('--name',
metavar='<name>',
default=None,
help="Filters results by a name. Default=None. "
"%s" % FILTER_DEPRECATED)
@utils.arg('--display-name',
help=argparse.SUPPRESS)
@utils.arg('--display_name',
help=argparse.SUPPRESS)
@utils.arg('--status',
metavar='<status>',
default=None,
help="Filters results by a status. Default=None. "
"%s" % FILTER_DEPRECATED)
@utils.arg('--volume-id',
metavar='<volume-id>',
default=None,
help="Filters results by a volume ID. Default=None. "
"%s" % FILTER_DEPRECATED)
@utils.arg('--volume_id',
help=argparse.SUPPRESS)
@utils.arg('--marker',
metavar='<marker>',
default=None,
help='Begin returning snapshots that appear later in the snapshot '
'list than that represented by this id. '
'Default=None.')
@utils.arg('--limit',
metavar='<limit>',
default=None,
help='Maximum number of snapshots to return. Default=None.')
@utils.arg('--sort',
metavar='<key>[:<direction>]',
default=None,
help=(('Comma-separated list of sort keys and directions in the '
'form of <key>[:<asc|desc>]. '
'Valid keys: %s. '
'Default=None.') % ', '.join(base.SORT_KEY_VALUES)))
@utils.arg('--tenant',
type=str,
dest='tenant',
nargs='?',
metavar='<tenant>',
help='Display information from single tenant (Admin only).')
@utils.arg('--metadata',
nargs='*',
metavar='<key=value>',
default=None,
start_version='3.22',
help="Filters results by a metadata key and value pair. Require "
"volume api version >=3.22. Default=None. "
"%s" % FILTER_DEPRECATED)
@utils.arg('--filters',
type=six.text_type,
nargs='*',
start_version='3.33',
metavar='<key=value>',
default=None,
help="Filter key and value pairs. Please use 'cinder list-filters' "
"to check enabled filters from server. Use 'key~=value' "
"for inexact filtering if the key supports. Default=None.")
def do_snapshot_list(cs, args):
"""Lists all snapshots."""
# pylint: disable=function-redefined
all_tenants = (1 if args.tenant else
int(os.environ.get("ALL_TENANTS", args.all_tenants)))
if args.display_name is not None:
args.name = args.display_name
metadata = None
try:
if args.metadata:
metadata = shell_utils.extract_metadata(args)
except AttributeError:
pass
search_opts = {
'all_tenants': all_tenants,
'name': args.name,
'status': args.status,
'volume_id': args.volume_id,
'project_id': args.tenant,
'metadata': metadata
}
# Update search option with `filters`
if hasattr(args, 'filters') and args.filters is not None:
search_opts.update(shell_utils.extract_filters(args.filters))
snapshots = cs.volume_snapshots.list(search_opts=search_opts,
marker=args.marker,
limit=args.limit,
sort=args.sort)
shell_utils.translate_volume_snapshot_keys(snapshots)
sortby_index = None if args.sort else 0
if cs.api_version >= api_versions.APIVersion("3.41"):
utils.print_list(snapshots,
['ID', 'Volume ID', 'Status',
'Name', 'Size', 'User ID'],
sortby_index=sortby_index)
else:
utils.print_list(snapshots,
['ID', 'Volume ID', 'Status', 'Name', 'Size'],
sortby_index=sortby_index)
@api_versions.wraps('3.27')
@utils.arg('--all-tenants',
dest='all_tenants',
metavar='<0|1>',
nargs='?',
type=int,
const=1,
default=utils.env('ALL_TENANTS', default=0),
help='Shows details for all tenants. Admin only.')
@utils.arg('--volume-id',
metavar='<volume-id>',
default=None,
help="Filters results by a volume ID. Default=None. "
"%s" % FILTER_DEPRECATED)
@utils.arg('--status',
metavar='<status>',
default=None,
help="Filters results by a status. Default=None. "
"%s" % FILTER_DEPRECATED)
@utils.arg('--marker',
metavar='<marker>',
default=None,
help='Begin returning attachments that appear later in '
'attachment list than that represented by this id. '
'Default=None.')
@utils.arg('--limit',
metavar='<limit>',
default=None,
help='Maximum number of attachments to return. Default=None.')
@utils.arg('--sort',
metavar='<key>[:<direction>]',
default=None,
help=(('Comma-separated list of sort keys and directions in the '
'form of <key>[:<asc|desc>]. '
'Valid keys: %s. '
'Default=None.') % ', '.join(base.SORT_KEY_VALUES)))
@utils.arg('--tenant',
type=str,
dest='tenant',
nargs='?',
metavar='<tenant>',
help='Display information from single tenant (Admin only).')
@utils.arg('--filters',
type=six.text_type,
nargs='*',
start_version='3.33',
metavar='<key=value>',
default=None,
help="Filter key and value pairs. Please use 'cinder list-filters' "
"to check enabled filters from server. Use 'key~=value' "
"for inexact filtering if the key supports. Default=None.")
def do_attachment_list(cs, args):
"""Lists all attachments."""
search_opts = {
'all_tenants': 1 if args.tenant else args.all_tenants,
'project_id': args.tenant,
'status': args.status,
'volume_id': args.volume_id,
}
# Update search option with `filters`
if hasattr(args, 'filters') and args.filters is not None:
search_opts.update(shell_utils.extract_filters(args.filters))
attachments = cs.attachments.list(search_opts=search_opts,
marker=args.marker,
limit=args.limit,
sort=args.sort)
columns = ['ID', 'Volume ID', 'Status', 'Server ID']
if args.sort:
sortby_index = None
else:
sortby_index = 0
utils.print_list(attachments, columns, sortby_index=sortby_index)
@api_versions.wraps('3.27')
@utils.arg('attachment',
metavar='<attachment>',
help='ID of attachment.')
def do_attachment_show(cs, args):
"""Show detailed information for attachment."""
attachment = cs.attachments.show(args.attachment)
attachment_dict = attachment.to_dict()
connection_dict = attachment_dict.pop('connection_info', {})
utils.print_dict(attachment_dict)
# TODO(jdg): Need to add checks here like admin/policy for displaying the
# connection_info, this is still experimental so we'll leave it enabled for
# now
if connection_dict:
utils.print_dict(connection_dict)
@api_versions.wraps('3.27')
@utils.arg('volume',
metavar='<volume>',
help='Name or ID of volume or volumes to attach.')
@utils.arg('server_id',
metavar='<server_id>',
help='ID of server attaching to.')
@utils.arg('--connect',
metavar='<connect>',
default=False,
help='Make an active connection using provided connector info '
'(True or False).')
@utils.arg('--initiator',
metavar='<initiator>',
default=None,
help='iqn of the initiator attaching to. Default=None.')
@utils.arg('--ip',
metavar='<ip>',
default=None,
help='ip of the system attaching to. Default=None.')
@utils.arg('--host',
metavar='<host>',
default=None,
help='Name of the host attaching to. Default=None.')
@utils.arg('--platform',
metavar='<platform>',
default='x86_64',
help='Platform type. Default=x86_64.')
@utils.arg('--ostype',
metavar='<ostype>',
default='linux2',
help='OS type. Default=linux2.')
@utils.arg('--multipath',
metavar='<multipath>',
default=False,
help='Use multipath. Default=False.')
@utils.arg('--mountpoint',
metavar='<mountpoint>',
default=None,
help='Mountpoint volume will be attached at. Default=None.')
def do_attachment_create(cs, args):
"""Create an attachment for a cinder volume."""
connector = {}
if strutils.bool_from_string(args.connect, strict=True):
# FIXME(jdg): Add in all the options when they're finalized
connector = {'initiator': args.initiator,
'ip': args.ip,
'platform': args.platform,
'host': args.host,
'os_type': args.ostype,
'multipath': args.multipath,
'mountpoint': args.mountpoint}
volume = utils.find_volume(cs, args.volume)
attachment = cs.attachments.create(volume.id,
connector,
args.server_id)
connector_dict = attachment.pop('connection_info', None)
utils.print_dict(attachment)
if connector_dict:
utils.print_dict(connector_dict)
@api_versions.wraps('3.27')
@utils.arg('attachment',
metavar='<attachment>',
help='ID of attachment.')
@utils.arg('--initiator',
metavar='<initiator>',
default=None,
help='iqn of the initiator attaching to. Default=None.')
@utils.arg('--ip',
metavar='<ip>',
default=None,
help='ip of the system attaching to. Default=None.')
@utils.arg('--host',
metavar='<host>',
default=None,
help='Name of the host attaching to. Default=None.')
@utils.arg('--platform',
metavar='<platform>',
default='x86_64',
help='Platform type. Default=x86_64.')
@utils.arg('--ostype',
metavar='<ostype>',
default='linux2',
help='OS type. Default=linux2.')
@utils.arg('--multipath',
metavar='<multipath>',
default=False,
help='Use multipath. Default=False.')
@utils.arg('--mountpoint',
metavar='<mountpoint>',
default=None,
help='Mountpoint volume will be attached at. Default=None.')
def do_attachment_update(cs, args):
"""Update an attachment for a cinder volume.
This call is designed to be more of an 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.
"""
connector = {'initiator': args.initiator,
'ip': args.ip,
'platform': args.platform,
'host': args.host,
'os_type': args.ostype,
'multipath': args.multipath,
'mountpoint': args.mountpoint}
attachment = cs.attachments.update(args.attachment,
connector)
attachment_dict = attachment.to_dict()
connector_dict = attachment_dict.pop('connection_info', None)
utils.print_dict(attachment_dict)
if connector_dict:
utils.print_dict(connector_dict)
@api_versions.wraps('3.27')
@utils.arg('attachment',
metavar='<attachment>', nargs='+',
help='ID of attachment or attachments to delete.')
def do_attachment_delete(cs, args):
"""Delete an attachment for a cinder volume."""
for attachment in args.attachment:
cs.attachments.delete(attachment)
@api_versions.wraps('3.44')
@utils.arg('attachment',
metavar='<attachment>', nargs='+',
help='ID of attachment or attachments to delete.')
def do_attachment_complete(cs, args):
"""Complete an attachment for a cinder volume."""
for attachment in args.attachment:
cs.attachments.complete(attachment)
@api_versions.wraps('3.0')
def do_version_list(cs, args):
"""List all API versions."""
result = cs.services.server_api_version()
if 'min_version' in dir(result[0]):
columns = ["Id", "Status", "Updated", "Min Version", "Version"]
else:
columns = ["Id", "Status", "Updated"]
print("Client supported API versions:")
print("Minimum version %(v)s" %
{'v': api_versions.MIN_VERSION})
print("Maximum version %(v)s" %
{'v': api_versions.MAX_VERSION})
print("\nServer supported API versions:")
utils.print_list(result, columns)
@api_versions.wraps('3.32')
@utils.arg('level',
metavar='<log-level>',
choices=('INFO', 'WARNING', 'ERROR', 'DEBUG',
'info', 'warning', 'error', 'debug'),
help='Desired log level.')
@utils.arg('--binary',
choices=('', '*', 'cinder-api', 'cinder-volume', 'cinder-scheduler',
'cinder-backup'),
default='',
help='Binary to change.')
@utils.arg('--server',
default='',
help='Host or cluster value for service.')
@utils.arg('--prefix',
default='',
help='Prefix for the log. ie: "cinder.volume.drivers.".')
def do_service_set_log(cs, args):
cs.services.set_log_levels(args.level, args.binary, args.server,
args.prefix)
@api_versions.wraps('3.32')
@utils.arg('--binary',
choices=('', '*', 'cinder-api', 'cinder-volume', 'cinder-scheduler',
'cinder-backup'),
default='',
help='Binary to query.')
@utils.arg('--server',
default='',
help='Host or cluster value for service.')
@utils.arg('--prefix',
default='',
help='Prefix for the log. ie: "sqlalchemy.".')
def do_service_get_log(cs, args):
log_levels = cs.services.get_log_levels(args.binary, args.server,
args.prefix)
columns = ('Binary', 'Host', 'Prefix', 'Level')
utils.print_list(log_levels, columns)
@api_versions.wraps('3.43')
@utils.arg('volume', metavar='<volume>',
help='Name or ID of volume to backup.')
@utils.arg('--container', metavar='<container>',
default=None,
help='Backup container name. Default=None.')
@utils.arg('--display-name',
help=argparse.SUPPRESS)
@utils.arg('--name', metavar='<name>',
default=None,
help='Backup name. Default=None.')
@utils.arg('--display-description',
help=argparse.SUPPRESS)
@utils.arg('--description',
metavar='<description>',
default=None,
help='Backup description. Default=None.')
@utils.arg('--incremental',
action='store_true',
help='Incremental backup. Default=False.',
default=False)
@utils.arg('--force',
action='store_true',
help='Allows or disallows backup of a volume '
'when the volume is attached to an instance. '
'If set to True, backs up the volume whether '
'its status is "available" or "in-use". The backup '
'of an "in-use" volume means your data is crash '
'consistent. Default=False.',
default=False)
@utils.arg('--snapshot-id',
metavar='<snapshot-id>',
default=None,
help='ID of snapshot to backup. Default=None.')
@utils.arg('--metadata',
nargs='*',
metavar='<key=value>',
default=None,
help='Metadata key and value pairs. Default=None.')
def do_backup_create(cs, args):
"""Creates a volume backup."""
if args.display_name is not None:
args.name = args.display_name
if args.display_description is not None:
args.description = args.display_description
volume = utils.find_volume(cs, args.volume)
backup = cs.backups.create(volume.id,
args.container,
args.name,
args.description,
args.incremental,
args.force,
args.snapshot_id,
args.metadata)
info = {"volume_id": volume.id}
info.update(backup._info)
if 'links' in info:
info.pop('links')
utils.print_dict(info)