diff options
Diffstat (limited to 'openstackclient')
40 files changed, 2299 insertions, 166 deletions
diff --git a/openstackclient/image/client.py b/openstackclient/image/client.py index 1be6c765..b67c291f 100644 --- a/openstackclient/image/client.py +++ b/openstackclient/image/client.py @@ -22,7 +22,7 @@ from openstackclient.i18n import _ LOG = logging.getLogger(__name__) -DEFAULT_API_VERSION = '1' +DEFAULT_API_VERSION = '2' API_VERSION_OPTION = 'os_image_api_version' API_NAME = "image" API_VERSIONS = { diff --git a/openstackclient/image/v1/image.py b/openstackclient/image/v1/image.py index cf389d17..eb79cd2f 100644 --- a/openstackclient/image/v1/image.py +++ b/openstackclient/image/v1/image.py @@ -38,6 +38,8 @@ from openstackclient.i18n import _ DEFAULT_CONTAINER_FORMAT = 'bare' DEFAULT_DISK_FORMAT = 'raw' +DISK_CHOICES = ["ami", "ari", "aki", "vhd", "vmdk", "raw", "qcow2", "vhdx", + "vdi", "iso"] LOG = logging.getLogger(__name__) @@ -89,8 +91,9 @@ class CreateImage(command.ShowOne): "--disk-format", default=DEFAULT_DISK_FORMAT, metavar="<disk-format>", - help=_("Image disk format " - "(default: %s)") % DEFAULT_DISK_FORMAT, + choices=DISK_CHOICES, + help=_("Image disk format. The supported options are: %s. " + "The default format is: raw") % ', '.join(DISK_CHOICES) ) parser.add_argument( "--size", @@ -350,8 +353,9 @@ class ListImage(command.Lister): parser.add_argument( '--sort', metavar="<key>[:<direction>]", + default='name:asc', help=_("Sort output by selected keys and directions(asc or desc) " - "(default: asc), multiple keys and directions can be " + "(default: name:asc), multiple keys and directions can be " "specified separated by comma"), ) return parser @@ -502,14 +506,12 @@ class SetImage(command.Command): container_choices, choices=container_choices ) - disk_choices = ["ami", "ari", "aki", "vhd", "vmdk", "raw", "qcow2", - "vhdx", "vdi", "iso"] parser.add_argument( "--disk-format", metavar="<disk-format>", - help=_("Disk format of image. Acceptable formats: %s") % - disk_choices, - choices=disk_choices + choices=DISK_CHOICES, + help=_("Image disk format. The supported options are: %s.") % + ', '.join(DISK_CHOICES) ) parser.add_argument( "--size", diff --git a/openstackclient/image/v2/image.py b/openstackclient/image/v2/image.py index 657277c0..1d167605 100644 --- a/openstackclient/image/v2/image.py +++ b/openstackclient/image/v2/image.py @@ -32,6 +32,8 @@ from openstackclient.identity import common DEFAULT_CONTAINER_FORMAT = 'bare' DEFAULT_DISK_FORMAT = 'raw' +DISK_CHOICES = ["ami", "ari", "aki", "vhd", "vmdk", "raw", "qcow2", "vhdx", + "vdi", "iso"] LOG = logging.getLogger(__name__) @@ -140,9 +142,10 @@ class CreateImage(command.ShowOne): parser.add_argument( "--disk-format", default=DEFAULT_DISK_FORMAT, + choices=DISK_CHOICES, metavar="<disk-format>", - help=_("Image disk format " - "(default: %s)") % DEFAULT_DISK_FORMAT, + help=_("Image disk format. The supported options are: %s. " + "The default format is: raw") % ', '.join(DISK_CHOICES) ) parser.add_argument( "--min-disk", @@ -447,8 +450,9 @@ class ListImage(command.Lister): parser.add_argument( '--sort', metavar="<key>[:<direction>]", + default='name:asc', help=_("Sort output by selected keys and directions(asc or desc) " - "(default: asc), multiple keys and directions can be " + "(default: name:asc), multiple keys and directions can be " "specified separated by comma"), ) parser.add_argument( @@ -482,7 +486,6 @@ class ListImage(command.Lister): if parsed_args.marker: kwargs['marker'] = utils.find_resource(image_client.images, parsed_args.marker).id - if parsed_args.long: columns = ( 'ID', @@ -515,7 +518,19 @@ class ListImage(command.Lister): column_headers = columns # List of image data received - data = image_client.api.image_list(**kwargs) + data = [] + if 'marker' in kwargs: + data = image_client.api.image_list(**kwargs) + else: + # No pages received yet, so start the page marker at None. + marker = None + while True: + page = image_client.api.image_list(marker=marker, **kwargs) + if not page: + break + data.extend(page) + # Set the marker to the id of the last item we received + marker = page[-1]['id'] if parsed_args.property: # NOTE(dtroyer): coerce to a list to subscript it in py3 @@ -650,8 +665,9 @@ class SetImage(command.Command): parser.add_argument( "--disk-format", metavar="<disk-format>", - help=_("Image disk format " - "(default: %s)") % DEFAULT_DISK_FORMAT, + choices=DISK_CHOICES, + help=_("Image disk format. The supported options are: %s") % + ', '.join(DISK_CHOICES) ) protected_group = parser.add_mutually_exclusive_group() protected_group.add_argument( diff --git a/openstackclient/network/v2/floating_ip.py b/openstackclient/network/v2/floating_ip.py index c787cd2f..7b8374e2 100644 --- a/openstackclient/network/v2/floating_ip.py +++ b/openstackclient/network/v2/floating_ip.py @@ -18,6 +18,7 @@ import logging from osc_lib import utils from openstackclient.i18n import _ +from openstackclient.identity import common as identity_common from openstackclient.network import common from openstackclient.network import sdk_utils @@ -66,6 +67,15 @@ def _get_attrs(client_manager, parsed_args): if parsed_args.description is not None: attrs['description'] = parsed_args.description + if parsed_args.project: + identity_client = client_manager.identity + project_id = identity_common.find_project( + identity_client, + parsed_args.project, + parsed_args.project_domain, + ).id + attrs['tenant_id'] = project_id + return attrs @@ -113,6 +123,12 @@ class CreateFloatingIP(common.NetworkAndComputeShowOne): metavar='<description>', help=_('Set floating IP description') ) + parser.add_argument( + '--project', + metavar='<project>', + help=_("Owner's project (name or ID)") + ) + identity_common.add_project_domain_option_to_parser(parser) return parser def take_action_network(self, client, parsed_args): diff --git a/openstackclient/network/v2/network_qos_policy.py b/openstackclient/network/v2/network_qos_policy.py index 75cb1d91..5ccbe36b 100644 --- a/openstackclient/network/v2/network_qos_policy.py +++ b/openstackclient/network/v2/network_qos_policy.py @@ -21,17 +21,18 @@ from osc_lib import utils from openstackclient.i18n import _ from openstackclient.identity import common as identity_common +from openstackclient.network import sdk_utils LOG = logging.getLogger(__name__) def _get_columns(item): - columns = list(item.keys()) - if 'tenant_id' in columns: - columns.remove('tenant_id') - columns.append('project_id') - return tuple(sorted(columns)) + column_map = { + 'is_shared': 'shared', + 'tenant_id': 'project_id', + } + return sdk_utils.get_osc_show_columns_for_sdk_resource(item, column_map) def _get_attrs(client_manager, parsed_args): @@ -56,6 +57,8 @@ def _get_attrs(client_manager, parsed_args): return attrs +# TODO(abhiraut): Use the SDK resource mapped attribute names once the +# OSC minimum requirements include SDK 1.0. class CreateNetworkQosPolicy(command.ShowOne): _description = _("Create a QoS policy") @@ -96,9 +99,9 @@ class CreateNetworkQosPolicy(command.ShowOne): client = self.app.client_manager.network attrs = _get_attrs(self.app.client_manager, parsed_args) obj = client.create_qos_policy(**attrs) - columns = _get_columns(obj) + display_columns, columns = _get_columns(obj) data = utils.get_item_properties(obj, columns, formatters={}) - return columns, data + return (display_columns, data) class DeleteNetworkQosPolicy(command.Command): @@ -135,6 +138,8 @@ class DeleteNetworkQosPolicy(command.Command): raise exceptions.CommandError(msg) +# TODO(abhiraut): Use only the SDK resource mapped attribute names once the +# OSC minimum requirements include SDK 1.0. class ListNetworkQosPolicy(command.Lister): _description = _("List QoS policies") @@ -143,8 +148,8 @@ class ListNetworkQosPolicy(command.Lister): columns = ( 'id', 'name', - 'shared', - 'tenant_id', + 'is_shared', + 'project_id', ) column_headers = ( 'ID', @@ -160,6 +165,8 @@ class ListNetworkQosPolicy(command.Lister): ) for s in data)) +# TODO(abhiraut): Use the SDK resource mapped attribute names once the +# OSC minimum requirements include SDK 1.0. class SetNetworkQosPolicy(command.Command): _description = _("Set QoS policy properties") @@ -226,6 +233,6 @@ class ShowNetworkQosPolicy(command.ShowOne): client = self.app.client_manager.network obj = client.find_qos_policy(parsed_args.policy, ignore_missing=False) - columns = _get_columns(obj) + display_columns, columns = _get_columns(obj) data = utils.get_item_properties(obj, columns) - return columns, data + return (display_columns, data) diff --git a/openstackclient/network/v2/network_rbac.py b/openstackclient/network/v2/network_rbac.py index 2bcdd811..e837af3a 100644 --- a/openstackclient/network/v2/network_rbac.py +++ b/openstackclient/network/v2/network_rbac.py @@ -21,20 +21,18 @@ from osc_lib import utils from openstackclient.i18n import _ from openstackclient.identity import common as identity_common +from openstackclient.network import sdk_utils LOG = logging.getLogger(__name__) def _get_columns(item): - columns = list(item.keys()) - if 'tenant_id' in columns: - columns.remove('tenant_id') - columns.append('project_id') - if 'target_tenant' in columns: - columns.remove('target_tenant') - columns.append('target_project_id') - return tuple(sorted(columns)) + column_map = { + 'target_tenant': 'target_project_id', + 'tenant_id': 'project_id', + } + return sdk_utils.get_osc_show_columns_for_sdk_resource(item, column_map) def _get_attrs(client_manager, parsed_args): @@ -70,6 +68,8 @@ def _get_attrs(client_manager, parsed_args): return attrs +# TODO(abhiraut): Use the SDK resource mapped attribute names once the +# OSC minimum requirements include SDK 1.0. class CreateNetworkRBAC(command.ShowOne): _description = _("Create network RBAC policy") @@ -122,9 +122,9 @@ class CreateNetworkRBAC(command.ShowOne): client = self.app.client_manager.network attrs = _get_attrs(self.app.client_manager, parsed_args) obj = client.create_rbac_policy(**attrs) - columns = _get_columns(obj) + display_columns, columns = _get_columns(obj) data = utils.get_item_properties(obj, columns) - return columns, data + return display_columns, data class DeleteNetworkRBAC(command.Command): @@ -185,6 +185,8 @@ class ListNetworkRBAC(command.Lister): ) for s in data)) +# TODO(abhiraut): Use the SDK resource mapped attribute names once the +# OSC minimum requirements include SDK 1.0. class SetNetworkRBAC(command.Command): _description = _("Set network RBAC policy properties") @@ -242,6 +244,6 @@ class ShowNetworkRBAC(command.ShowOne): client = self.app.client_manager.network obj = client.find_rbac_policy(parsed_args.rbac_policy, ignore_missing=False) - columns = _get_columns(obj) + display_columns, columns = _get_columns(obj) data = utils.get_item_properties(obj, columns) - return columns, data + return display_columns, data diff --git a/openstackclient/network/v2/port.py b/openstackclient/network/v2/port.py index c960ba25..bce3e2d3 100644 --- a/openstackclient/network/v2/port.py +++ b/openstackclient/network/v2/port.py @@ -50,7 +50,8 @@ def _get_columns(item): columns = list(item.keys()) if 'tenant_id' in columns: columns.remove('tenant_id') - columns.append('project_id') + if 'project_id' not in columns: + columns.append('project_id') binding_columns = [ 'binding:host_id', 'binding:profile', diff --git a/openstackclient/network/v2/router.py b/openstackclient/network/v2/router.py index cbd412b5..cdd634d0 100644 --- a/openstackclient/network/v2/router.py +++ b/openstackclient/network/v2/router.py @@ -25,6 +25,7 @@ from osc_lib import utils from openstackclient.i18n import _ from openstackclient.identity import common as identity_common +from openstackclient.network import sdk_utils LOG = logging.getLogger(__name__) @@ -59,11 +60,10 @@ _formatters = { def _get_columns(item): - columns = list(item.keys()) - if 'tenant_id' in columns: - columns.remove('tenant_id') - columns.append('project_id') - return tuple(sorted(columns)) + column_map = { + 'tenant_id': 'project_id', + } + return sdk_utils.get_osc_show_columns_for_sdk_resource(item, column_map) def _get_attrs(client_manager, parsed_args): @@ -215,10 +215,10 @@ class CreateRouter(command.ShowOne): attrs['ha'] = parsed_args.ha obj = client.create_router(**attrs) - columns = _get_columns(obj) + display_columns, columns = _get_columns(obj) data = utils.get_item_properties(obj, columns, formatters=_formatters) - return (columns, data) + return (display_columns, data) class DeleteRouter(command.Command): @@ -282,11 +282,17 @@ class ListRouter(command.Lister): default=False, help=_("List additional fields in output") ) + parser.add_argument( + '--project', + metavar='<project>', + help=_("List routers according to their project (name or ID)") + ) + identity_common.add_project_domain_option_to_parser(parser) return parser def take_action(self, parsed_args): + identity_client = self.app.client_manager.identity client = self.app.client_manager.network - columns = ( 'id', 'name', @@ -316,6 +322,13 @@ class ListRouter(command.Lister): elif parsed_args.disable: args['admin_state_up'] = False + if parsed_args.project: + project_id = identity_common.find_project( + identity_client, + parsed_args.project, + parsed_args.project_domain, + ).id + args['tenant_id'] = project_id if parsed_args.long: columns = columns + ( 'routes', @@ -523,9 +536,10 @@ class ShowRouter(command.ShowOne): def take_action(self, parsed_args): client = self.app.client_manager.network obj = client.find_router(parsed_args.router, ignore_missing=False) - columns = _get_columns(obj) + display_columns, columns = _get_columns(obj) data = utils.get_item_properties(obj, columns, formatters=_formatters) - return (columns, data) + + return (display_columns, data) class UnsetRouter(command.Command): diff --git a/openstackclient/network/v2/security_group.py b/openstackclient/network/v2/security_group.py index 554dd61d..5420bc8b 100644 --- a/openstackclient/network/v2/security_group.py +++ b/openstackclient/network/v2/security_group.py @@ -81,8 +81,9 @@ def _get_columns(item): columns.remove('security_group_rules') property_column_mappings.append(('rules', 'security_group_rules')) if 'tenant_id' in columns: - columns.append('project_id') columns.remove('tenant_id') + if 'project_id' not in columns: + columns.append('project_id') property_column_mappings.append(('project_id', 'tenant_id')) display_columns = sorted(columns) @@ -201,6 +202,13 @@ class ListSecurityGroup(common.NetworkAndComputeLister): default=False, help=argparse.SUPPRESS, ) + parser.add_argument( + '--project', + metavar='<project>', + help=_("List security groups according to the project " + "(name or ID)") + ) + identity_common.add_project_domain_option_to_parser(parser) return parser def update_parser_compute(self, parser): @@ -228,7 +236,16 @@ class ListSecurityGroup(common.NetworkAndComputeLister): ) for s in data)) def take_action_network(self, client, parsed_args): - return self._get_return_data(client.security_groups()) + filters = {} + if parsed_args.project: + identity_client = self.app.client_manager.identity + project_id = identity_common.find_project( + identity_client, + parsed_args.project, + parsed_args.project_domain, + ).id + filters['tenant_id'] = project_id + return self._get_return_data(client.security_groups(**filters)) def take_action_compute(self, client, parsed_args): search = {'all_tenants': parsed_args.all_projects} diff --git a/openstackclient/network/v2/security_group_rule.py b/openstackclient/network/v2/security_group_rule.py index 05fafc0d..b878d875 100644 --- a/openstackclient/network/v2/security_group_rule.py +++ b/openstackclient/network/v2/security_group_rule.py @@ -29,6 +29,7 @@ import six from openstackclient.i18n import _ from openstackclient.identity import common as identity_common from openstackclient.network import common +from openstackclient.network import sdk_utils from openstackclient.network import utils as network_utils @@ -67,11 +68,10 @@ def _format_network_port_range(rule): def _get_columns(item): - columns = list(item.keys()) - if 'tenant_id' in columns: - columns.remove('tenant_id') - columns.append('project_id') - return tuple(sorted(columns)) + column_map = { + 'tenant_id': 'project_id', + } + return sdk_utils.get_osc_show_columns_for_sdk_resource(item, column_map) def _convert_to_lowercase(string): @@ -89,6 +89,8 @@ def _is_icmp_protocol(protocol): return False +# TODO(abhiraut): Use the SDK resource mapped attribute names once the +# OSC minimum requirements include SDK 1.0. class CreateSecurityGroupRule(common.NetworkAndComputeShowOne): _description = _("Create a new security group rule") @@ -341,9 +343,9 @@ class CreateSecurityGroupRule(common.NetworkAndComputeShowOne): # Create and show the security group rule. obj = client.create_security_group_rule(**attrs) - columns = _get_columns(obj) + display_columns, columns = _get_columns(obj) data = utils.get_item_properties(obj, columns) - return (columns, data) + return (display_columns, data) def take_action_compute(self, client, parsed_args): group = utils.find_resource( @@ -597,9 +599,9 @@ class ShowSecurityGroupRule(common.NetworkAndComputeShowOne): def take_action_network(self, client, parsed_args): obj = client.find_security_group_rule(parsed_args.rule, ignore_missing=False) - columns = _get_columns(obj) + display_columns, columns = _get_columns(obj) data = utils.get_item_properties(obj, columns) - return (columns, data) + return (display_columns, data) def take_action_compute(self, client, parsed_args): # NOTE(rtheis): Unfortunately, compute does not have an API diff --git a/openstackclient/network/v2/subnet.py b/openstackclient/network/v2/subnet.py index 5366dff6..292b7c06 100644 --- a/openstackclient/network/v2/subnet.py +++ b/openstackclient/network/v2/subnet.py @@ -23,6 +23,7 @@ from osc_lib import utils from openstackclient.i18n import _ from openstackclient.identity import common as identity_common +from openstackclient.network import sdk_utils LOG = logging.getLogger(__name__) @@ -126,11 +127,12 @@ def _get_common_parse_arguments(parser, is_create=True): def _get_columns(item): - columns = list(item.keys()) - if 'tenant_id' in columns: - columns.remove('tenant_id') - columns.append('project_id') - return tuple(sorted(columns)) + column_map = { + 'is_dhcp_enabled': 'enable_dhcp', + 'subnet_pool_id': 'subnetpool_id', + 'tenant_id': 'project_id', + } + return sdk_utils.get_osc_show_columns_for_sdk_resource(item, column_map) def convert_entries_to_nexthop(entries): @@ -226,6 +228,8 @@ def _get_attrs(client_manager, parsed_args, is_create=True): return attrs +# TODO(abhiraut): Use the SDK resource mapped attribute names once the +# OSC minimum requirements include SDK 1.0. class CreateSubnet(command.ShowOne): _description = _("Create a subnet") @@ -332,9 +336,9 @@ class CreateSubnet(command.ShowOne): client = self.app.client_manager.network attrs = _get_attrs(self.app.client_manager, parsed_args) obj = client.create_subnet(**attrs) - columns = _get_columns(obj) + display_columns, columns = _get_columns(obj) data = utils.get_item_properties(obj, columns, formatters=_formatters) - return (columns, data) + return (display_columns, data) class DeleteSubnet(command.Command): @@ -371,6 +375,8 @@ class DeleteSubnet(command.Command): raise exceptions.CommandError(msg) +# TODO(abhiraut): Use only the SDK resource mapped attribute names once the +# OSC minimum requirements include SDK 1.0. class ListSubnet(command.Lister): _description = _("List subnets") @@ -452,8 +458,10 @@ class ListSubnet(command.Lister): filters['ip_version'] = parsed_args.ip_version if parsed_args.dhcp: filters['enable_dhcp'] = True + filters['is_dhcp_enabled'] = True elif parsed_args.no_dhcp: filters['enable_dhcp'] = False + filters['is_dhcp_enabled'] = False if parsed_args.service_types: filters['service_types'] = parsed_args.service_types if parsed_args.project: @@ -463,6 +471,7 @@ class ListSubnet(command.Lister): parsed_args.project_domain, ).id filters['tenant_id'] = project_id + filters['project_id'] = project_id if parsed_args.network: network_id = network_client.find_network(parsed_args.network, ignore_missing=False).id @@ -481,7 +490,7 @@ class ListSubnet(command.Lister): headers += ('Project', 'DHCP', 'Name Servers', 'Allocation Pools', 'Host Routes', 'IP Version', 'Gateway', 'Service Types') - columns += ('tenant_id', 'enable_dhcp', 'dns_nameservers', + columns += ('project_id', 'is_dhcp_enabled', 'dns_nameservers', 'allocation_pools', 'host_routes', 'ip_version', 'gateway_ip', 'service_types') @@ -492,6 +501,8 @@ class ListSubnet(command.Lister): ) for s in data)) +# TODO(abhiraut): Use the SDK resource mapped attribute names once the +# OSC minimum requirements include SDK 1.0. class SetSubnet(command.Command): _description = _("Set subnet properties") @@ -576,9 +587,9 @@ class ShowSubnet(command.ShowOne): def take_action(self, parsed_args): obj = self.app.client_manager.network.find_subnet(parsed_args.subnet, ignore_missing=False) - columns = _get_columns(obj) + display_columns, columns = _get_columns(obj) data = utils.get_item_properties(obj, columns, formatters=_formatters) - return (columns, data) + return (display_columns, data) class UnsetSubnet(command.Command): diff --git a/openstackclient/shell.py b/openstackclient/shell.py index be4b5283..e08eee61 100644 --- a/openstackclient/shell.py +++ b/openstackclient/shell.py @@ -148,10 +148,10 @@ class OpenStackShell(shell.OpenStackShell): 'auth_type': self._auth_type, }, ) - except (IOError, OSError) as e: + except (IOError, OSError): self.log.critical("Could not read clouds.yaml configuration file") self.print_help_if_requested() - raise e + raise if not self.options.debug: self.options.debug = None diff --git a/openstackclient/tests/functional/volume/v1/test_snapshot.py b/openstackclient/tests/functional/volume/v1/test_snapshot.py index c6d65ccc..1e1c6b21 100644 --- a/openstackclient/tests/functional/volume/v1/test_snapshot.py +++ b/openstackclient/tests/functional/volume/v1/test_snapshot.py @@ -16,8 +16,8 @@ import uuid from openstackclient.tests.functional.volume.v1 import common -class SnapshotTests(common.BaseVolumeTests): - """Functional tests for snapshot. """ +class VolumeSnapshotTests(common.BaseVolumeTests): + """Functional tests for volume snapshot. """ VOLLY = uuid.uuid4().hex NAME = uuid.uuid4().hex @@ -36,24 +36,25 @@ class SnapshotTests(common.BaseVolumeTests): @classmethod def setUpClass(cls): - super(SnapshotTests, cls).setUpClass() + super(VolumeSnapshotTests, cls).setUpClass() cls.openstack('volume create --size 1 ' + cls.VOLLY) cls.wait_for_status('volume show ' + cls.VOLLY, 'available\n', 3) opts = cls.get_opts(['status']) - raw_output = cls.openstack('snapshot create --name ' + cls.NAME + - ' ' + cls.VOLLY + opts) + raw_output = cls.openstack('volume snapshot create --volume ' + + cls.VOLLY + ' ' + cls.NAME + opts) cls.assertOutput('creating\n', raw_output) - cls.wait_for_status('snapshot show ' + cls.NAME, 'available\n', 3) + cls.wait_for_status( + 'volume snapshot show ' + cls.NAME, 'available\n', 3) @classmethod def tearDownClass(cls): # Rename test raw_output = cls.openstack( - 'snapshot set --name ' + cls.OTHER_NAME + ' ' + cls.NAME) + 'volume snapshot set --name ' + cls.OTHER_NAME + ' ' + cls.NAME) cls.assertOutput('', raw_output) # Delete test raw_output_snapshot = cls.openstack( - 'snapshot delete ' + cls.OTHER_NAME) + 'volume snapshot delete ' + cls.OTHER_NAME) cls.wait_for_status('volume show ' + cls.VOLLY, 'available\n', 6) raw_output_volume = cls.openstack('volume delete --force ' + cls.VOLLY) cls.assertOutput('', raw_output_snapshot) @@ -61,26 +62,27 @@ class SnapshotTests(common.BaseVolumeTests): def test_snapshot_list(self): opts = self.get_opts(self.HEADERS) - raw_output = self.openstack('snapshot list' + opts) + raw_output = self.openstack('volume snapshot list' + opts) self.assertIn(self.NAME, raw_output) def test_snapshot_set_unset_properties(self): raw_output = self.openstack( - 'snapshot set --property a=b --property c=d ' + self.NAME) + 'volume snapshot set --property a=b --property c=d ' + self.NAME) self.assertEqual("", raw_output) opts = self.get_opts(["properties"]) - raw_output = self.openstack('snapshot show ' + self.NAME + opts) + raw_output = self.openstack('volume snapshot show ' + self.NAME + opts) self.assertEqual("a='b', c='d'\n", raw_output) - raw_output = self.openstack('snapshot unset --property a ' + self.NAME) + raw_output = self.openstack( + 'volume snapshot unset --property a ' + self.NAME) self.assertEqual("", raw_output) - raw_output = self.openstack('snapshot show ' + self.NAME + opts) + raw_output = self.openstack('volume snapshot show ' + self.NAME + opts) self.assertEqual("c='d'\n", raw_output) def test_snapshot_set_description(self): raw_output = self.openstack( - 'snapshot set --description backup ' + self.NAME) + 'volume snapshot set --description backup ' + self.NAME) self.assertEqual("", raw_output) opts = self.get_opts(["display_description", "display_name"]) - raw_output = self.openstack('snapshot show ' + self.NAME + opts) + raw_output = self.openstack('volume snapshot show ' + self.NAME + opts) self.assertEqual("backup\n" + self.NAME + "\n", raw_output) diff --git a/openstackclient/tests/functional/volume/v2/test_snapshot.py b/openstackclient/tests/functional/volume/v2/test_snapshot.py index fcbc31cb..4eb69e9d 100644 --- a/openstackclient/tests/functional/volume/v2/test_snapshot.py +++ b/openstackclient/tests/functional/volume/v2/test_snapshot.py @@ -16,8 +16,8 @@ import uuid from openstackclient.tests.functional.volume.v2 import common -class SnapshotTests(common.BaseVolumeTests): - """Functional tests for snapshot. """ +class VolumeSnapshotTests(common.BaseVolumeTests): + """Functional tests for volume snapshot. """ VOLLY = uuid.uuid4().hex NAME = uuid.uuid4().hex @@ -36,24 +36,25 @@ class SnapshotTests(common.BaseVolumeTests): @classmethod def setUpClass(cls): - super(SnapshotTests, cls).setUpClass() + super(VolumeSnapshotTests, cls).setUpClass() cls.openstack('volume create --size 1 ' + cls.VOLLY) cls.wait_for_status('volume show ' + cls.VOLLY, 'available\n', 3) opts = cls.get_opts(['status']) - raw_output = cls.openstack('snapshot create --name ' + cls.NAME + - ' ' + cls.VOLLY + opts) + raw_output = cls.openstack('volume snapshot create --volume ' + + cls.VOLLY + ' ' + cls.NAME + opts) cls.assertOutput('creating\n', raw_output) - cls.wait_for_status('snapshot show ' + cls.NAME, 'available\n', 3) + cls.wait_for_status( + 'volume snapshot show ' + cls.NAME, 'available\n', 3) @classmethod def tearDownClass(cls): # Rename test raw_output = cls.openstack( - 'snapshot set --name ' + cls.OTHER_NAME + ' ' + cls.NAME) + 'volume snapshot set --name ' + cls.OTHER_NAME + ' ' + cls.NAME) cls.assertOutput('', raw_output) # Delete test raw_output_snapshot = cls.openstack( - 'snapshot delete ' + cls.OTHER_NAME) + 'volume snapshot delete ' + cls.OTHER_NAME) cls.wait_for_status('volume show ' + cls.VOLLY, 'available\n', 6) raw_output_volume = cls.openstack('volume delete --force ' + cls.VOLLY) cls.assertOutput('', raw_output_snapshot) @@ -61,26 +62,27 @@ class SnapshotTests(common.BaseVolumeTests): def test_snapshot_list(self): opts = self.get_opts(self.HEADERS) - raw_output = self.openstack('snapshot list' + opts) + raw_output = self.openstack('volume snapshot list' + opts) self.assertIn(self.NAME, raw_output) def test_snapshot_properties(self): raw_output = self.openstack( - 'snapshot set --property a=b --property c=d ' + self.NAME) + 'volume snapshot set --property a=b --property c=d ' + self.NAME) self.assertEqual("", raw_output) opts = self.get_opts(["properties"]) - raw_output = self.openstack('snapshot show ' + self.NAME + opts) + raw_output = self.openstack('volume snapshot show ' + self.NAME + opts) self.assertEqual("a='b', c='d'\n", raw_output) - raw_output = self.openstack('snapshot unset --property a ' + self.NAME) + raw_output = self.openstack( + 'volume snapshot unset --property a ' + self.NAME) self.assertEqual("", raw_output) - raw_output = self.openstack('snapshot show ' + self.NAME + opts) + raw_output = self.openstack('volume snapshot show ' + self.NAME + opts) self.assertEqual("c='d'\n", raw_output) def test_snapshot_set(self): raw_output = self.openstack( - 'snapshot set --description backup ' + self.NAME) + 'volume snapshot set --description backup ' + self.NAME) self.assertEqual("", raw_output) opts = self.get_opts(["description", "name"]) - raw_output = self.openstack('snapshot show ' + self.NAME + opts) + raw_output = self.openstack('volume snapshot show ' + self.NAME + opts) self.assertEqual("backup\n" + self.NAME + "\n", raw_output) diff --git a/openstackclient/tests/functional/volume/v2/test_volume.py b/openstackclient/tests/functional/volume/v2/test_volume.py index fb880578..ea891cba 100644 --- a/openstackclient/tests/functional/volume/v2/test_volume.py +++ b/openstackclient/tests/functional/volume/v2/test_volume.py @@ -106,11 +106,12 @@ class VolumeTests(common.BaseVolumeTests): opts = self.get_opts(self.FIELDS) # Create snapshot from test volume - raw_output = self.openstack('snapshot create ' + self.NAME + - ' --name ' + self.SNAPSHOT_NAME + opts) + raw_output = self.openstack('volume snapshot create ' + + self.SNAPSHOT_NAME + + ' --volume ' + self.NAME + opts) expected = self.SNAPSHOT_NAME + '\n' self.assertOutput(expected, raw_output) - self.wait_for("snapshot", self.SNAPSHOT_NAME, "available") + self.wait_for("volume snapshot", self.SNAPSHOT_NAME, "available") # Create volume from snapshot raw_output = self.openstack('volume create --size 2 --snapshot ' + @@ -126,7 +127,8 @@ class VolumeTests(common.BaseVolumeTests): self.assertOutput('', raw_output) # Delete test snapshot - raw_output = self.openstack('snapshot delete ' + self.SNAPSHOT_NAME) + raw_output = self.openstack( + 'volume snapshot delete ' + self.SNAPSHOT_NAME) self.assertOutput('', raw_output) self.wait_for("volume", self.NAME, "available") diff --git a/openstackclient/tests/unit/image/v1/test_image.py b/openstackclient/tests/unit/image/v1/test_image.py index aef74f04..036c8336 100644 --- a/openstackclient/tests/unit/image/v1/test_image.py +++ b/openstackclient/tests/unit/image/v1/test_image.py @@ -116,7 +116,7 @@ class TestImageCreate(TestImage): self.images_mock.configure_mock(**mock_exception) arglist = [ '--container-format', 'ovf', - '--disk-format', 'fs', + '--disk-format', 'ami', '--min-disk', '10', '--min-ram', '4', '--protected', @@ -126,7 +126,7 @@ class TestImageCreate(TestImage): ] verifylist = [ ('container_format', 'ovf'), - ('disk_format', 'fs'), + ('disk_format', 'ami'), ('min_disk', 10), ('min_ram', 4), ('protected', True), @@ -147,7 +147,7 @@ class TestImageCreate(TestImage): self.images_mock.create.assert_called_with( name=self.new_image.name, container_format='ovf', - disk_format='fs', + disk_format='ami', min_disk=10, min_ram=4, protected=True, diff --git a/openstackclient/tests/unit/image/v2/test_image.py b/openstackclient/tests/unit/image/v2/test_image.py index ebc9c3a7..a054e513 100644 --- a/openstackclient/tests/unit/image/v2/test_image.py +++ b/openstackclient/tests/unit/image/v2/test_image.py @@ -130,7 +130,7 @@ class TestImageCreate(TestImage): self.images_mock.configure_mock(**mock_exception) arglist = [ '--container-format', 'ovf', - '--disk-format', 'fs', + '--disk-format', 'ami', '--min-disk', '10', '--min-ram', '4', ('--protected' @@ -143,7 +143,7 @@ class TestImageCreate(TestImage): ] verifylist = [ ('container_format', 'ovf'), - ('disk_format', 'fs'), + ('disk_format', 'ami'), ('min_disk', 10), ('min_ram', 4), ('protected', self.new_image.protected), @@ -165,7 +165,7 @@ class TestImageCreate(TestImage): self.images_mock.create.assert_called_with( name=self.new_image.name, container_format='ovf', - disk_format='fs', + disk_format='ami', min_disk=10, min_ram=4, owner=self.project.id, @@ -193,7 +193,7 @@ class TestImageCreate(TestImage): arglist = [ '--container-format', 'ovf', - '--disk-format', 'fs', + '--disk-format', 'ami', '--min-disk', '10', '--min-ram', '4', '--owner', 'unexist_owner', @@ -203,7 +203,7 @@ class TestImageCreate(TestImage): ] verifylist = [ ('container_format', 'ovf'), - ('disk_format', 'fs'), + ('disk_format', 'ami'), ('min_disk', 10), ('min_ram', 4), ('owner', 'unexist_owner'), @@ -227,7 +227,7 @@ class TestImageCreate(TestImage): arglist = [ '--container-format', 'ovf', - '--disk-format', 'fs', + '--disk-format', 'ami', '--min-disk', '10', '--min-ram', '4', '--protected', @@ -237,7 +237,7 @@ class TestImageCreate(TestImage): ] verifylist = [ ('container_format', 'ovf'), - ('disk_format', 'fs'), + ('disk_format', 'ami'), ('min_disk', 10), ('min_ram', 4), ('protected', True), @@ -535,7 +535,9 @@ class TestImageList(TestImage): # returns a tuple containing the column names and an iterable # containing the data to be listed. columns, data = self.cmd.take_action(parsed_args) - self.api_mock.image_list.assert_called_with() + self.api_mock.image_list.assert_called_with( + marker=self._image.id, + ) self.assertEqual(self.columns, columns) self.assertEqual(self.datalist, tuple(data)) @@ -558,6 +560,7 @@ class TestImageList(TestImage): columns, data = self.cmd.take_action(parsed_args) self.api_mock.image_list.assert_called_with( public=True, + marker=self._image.id, ) self.assertEqual(self.columns, columns) @@ -581,6 +584,7 @@ class TestImageList(TestImage): columns, data = self.cmd.take_action(parsed_args) self.api_mock.image_list.assert_called_with( private=True, + marker=self._image.id, ) self.assertEqual(self.columns, columns) @@ -604,6 +608,7 @@ class TestImageList(TestImage): columns, data = self.cmd.take_action(parsed_args) self.api_mock.image_list.assert_called_with( shared=True, + marker=self._image.id, ) self.assertEqual(self.columns, columns) @@ -622,7 +627,9 @@ class TestImageList(TestImage): # returns a tuple containing the column names and an iterable # containing the data to be listed. columns, data = self.cmd.take_action(parsed_args) - self.api_mock.image_list.assert_called_with() + self.api_mock.image_list.assert_called_with( + marker=self._image.id, + ) collist = ( 'ID', @@ -670,7 +677,9 @@ class TestImageList(TestImage): # returns a tuple containing the column names and an iterable # containing the data to be listed. columns, data = self.cmd.take_action(parsed_args) - self.api_mock.image_list.assert_called_with() + self.api_mock.image_list.assert_called_with( + marker=self._image.id, + ) sf_mock.assert_called_with( [self._image], attr='a', @@ -693,7 +702,9 @@ class TestImageList(TestImage): # returns a tuple containing the column names and an iterable # containing the data to be listed. columns, data = self.cmd.take_action(parsed_args) - self.api_mock.image_list.assert_called_with() + self.api_mock.image_list.assert_called_with( + marker=self._image.id, + ) si_mock.assert_called_with( [self._image], 'name:asc' @@ -712,7 +723,7 @@ class TestImageList(TestImage): columns, data = self.cmd.take_action(parsed_args) self.api_mock.image_list.assert_called_with( - limit=1, + limit=1, marker=self._image.id ) self.assertEqual(self.columns, columns) diff --git a/openstackclient/tests/unit/network/v2/fakes.py b/openstackclient/tests/unit/network/v2/fakes.py index 37f10147..c18511f7 100644 --- a/openstackclient/tests/unit/network/v2/fakes.py +++ b/openstackclient/tests/unit/network/v2/fakes.py @@ -746,6 +746,7 @@ class FakeNetworkQosPolicy(object): loaded=True) # Set attributes with special mapping in OpenStack SDK. + qos_policy.is_shared = qos_policy_attrs['shared'] qos_policy.project_id = qos_policy_attrs['tenant_id'] return qos_policy @@ -1064,6 +1065,8 @@ class FakeSubnet(object): loaded=True) # Set attributes with special mappings in OpenStack SDK. + subnet.is_dhcp_enabled = subnet_attrs['enable_dhcp'] + subnet.subnet_pool_id = subnet_attrs['subnetpool_id'] subnet.project_id = subnet_attrs['tenant_id'] return subnet diff --git a/openstackclient/tests/unit/network/v2/test_floating_ip.py b/openstackclient/tests/unit/network/v2/test_floating_ip.py index 10f3067d..b3d211ba 100644 --- a/openstackclient/tests/unit/network/v2/test_floating_ip.py +++ b/openstackclient/tests/unit/network/v2/test_floating_ip.py @@ -18,6 +18,7 @@ from osc_lib import exceptions from openstackclient.network.v2 import floating_ip from openstackclient.tests.unit.compute.v2 import fakes as compute_fakes +from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes_v3 from openstackclient.tests.unit.network.v2 import fakes as network_fakes from openstackclient.tests.unit import utils as tests_utils @@ -31,6 +32,7 @@ class TestFloatingIPNetwork(network_fakes.TestNetworkV2): # Get a shortcut to the network client self.network = self.app.client_manager.network + self.projects_mock = self.app.client_manager.identity.projects class TestCreateFloatingIPNetwork(TestFloatingIPNetwork): @@ -145,6 +147,54 @@ class TestCreateFloatingIPNetwork(TestFloatingIPNetwork): self.assertEqual(self.columns, columns) self.assertEqual(self.data, data) + def test_floating_ip_create_project(self): + project = identity_fakes_v3.FakeProject.create_one_project() + self.projects_mock.get.return_value = project + arglist = [ + '--project', project.id, + self.floating_ip.floating_network_id, + ] + verifylist = [ + ('network', self.floating_ip.floating_network_id), + ('project', project.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.network.create_ip.assert_called_once_with(**{ + 'floating_network_id': self.floating_ip.floating_network_id, + 'tenant_id': project.id, + }) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + def test_floating_ip_create_project_domain(self): + project = identity_fakes_v3.FakeProject.create_one_project() + domain = identity_fakes_v3.FakeDomain.create_one_domain() + self.projects_mock.get.return_value = project + arglist = [ + "--project", project.name, + "--project-domain", domain.name, + self.floating_ip.floating_network_id, + ] + verifylist = [ + ('network', self.floating_ip.floating_network_id), + ('project', project.name), + ('project_domain', domain.name), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.network.create_ip.assert_called_once_with(**{ + 'floating_network_id': self.floating_ip.floating_network_id, + 'tenant_id': project.id, + }) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + class TestDeleteFloatingIPNetwork(TestFloatingIPNetwork): diff --git a/openstackclient/tests/unit/network/v2/test_router.py b/openstackclient/tests/unit/network/v2/test_router.py index 24984e47..b0409447 100644 --- a/openstackclient/tests/unit/network/v2/test_router.py +++ b/openstackclient/tests/unit/network/v2/test_router.py @@ -18,6 +18,7 @@ from osc_lib import exceptions from osc_lib import utils as osc_utils from openstackclient.network.v2 import router +from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes_v3 from openstackclient.tests.unit.network.v2 import fakes as network_fakes from openstackclient.tests.unit import utils as tests_utils @@ -29,6 +30,7 @@ class TestRouter(network_fakes.TestNetworkV2): # Get a shortcut to the network client self.network = self.app.client_manager.network + self.projects_mock = self.app.client_manager.identity.projects class TestAddPortToRouter(TestRouter): @@ -476,6 +478,45 @@ class TestListRouter(TestRouter): self.network.routers.assert_called_once_with( **{'admin_state_up': False} ) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, list(data)) + + def test_router_list_project(self): + project = identity_fakes_v3.FakeProject.create_one_project() + self.projects_mock.get.return_value = project + arglist = [ + '--project', project.id, + ] + verifylist = [ + ('project', project.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + filters = {'tenant_id': project.id} + + self.network.routers.assert_called_once_with(**filters) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, list(data)) + + def test_router_list_project_domain(self): + project = identity_fakes_v3.FakeProject.create_one_project() + self.projects_mock.get.return_value = project + arglist = [ + '--project', project.id, + '--project-domain', project.domain_id, + ] + verifylist = [ + ('project', project.id), + ('project_domain', project.domain_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + filters = {'tenant_id': project.id} + + self.network.routers.assert_called_once_with(**filters) self.assertEqual(self.columns, columns) self.assertEqual(self.data, list(data)) diff --git a/openstackclient/tests/unit/network/v2/test_security_group.py b/openstackclient/tests/unit/network/v2/test_security_group.py index 2615b77a..43aa07cc 100644 --- a/openstackclient/tests/unit/network/v2/test_security_group.py +++ b/openstackclient/tests/unit/network/v2/test_security_group.py @@ -444,6 +444,44 @@ class TestListSecurityGroupNetwork(TestSecurityGroupNetwork): self.assertEqual(self.columns, columns) self.assertEqual(self.data, list(data)) + def test_security_group_list_project(self): + project = identity_fakes.FakeProject.create_one_project() + self.projects_mock.get.return_value = project + arglist = [ + '--project', project.id, + ] + verifylist = [ + ('project', project.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + filters = {'tenant_id': project.id} + + self.network.security_groups.assert_called_once_with(**filters) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, list(data)) + + def test_security_group_list_project_domain(self): + project = identity_fakes.FakeProject.create_one_project() + self.projects_mock.get.return_value = project + arglist = [ + '--project', project.id, + '--project-domain', project.domain_id, + ] + verifylist = [ + ('project', project.id), + ('project_domain', project.domain_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + filters = {'tenant_id': project.id} + + self.network.security_groups.assert_called_once_with(**filters) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, list(data)) + class TestListSecurityGroupCompute(TestSecurityGroupCompute): diff --git a/openstackclient/tests/unit/network/v2/test_subnet.py b/openstackclient/tests/unit/network/v2/test_subnet.py index e7406575..47de5616 100644 --- a/openstackclient/tests/unit/network/v2/test_subnet.py +++ b/openstackclient/tests/unit/network/v2/test_subnet.py @@ -636,7 +636,7 @@ class TestListSubnet(TestSubnet): parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - filters = {'enable_dhcp': True} + filters = {'enable_dhcp': True, 'is_dhcp_enabled': True} self.network.subnets.assert_called_once_with(**filters) self.assertEqual(self.columns, columns) @@ -652,7 +652,7 @@ class TestListSubnet(TestSubnet): parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - filters = {'enable_dhcp': False} + filters = {'enable_dhcp': False, 'is_dhcp_enabled': False} self.network.subnets.assert_called_once_with(**filters) self.assertEqual(self.columns, columns) @@ -685,7 +685,7 @@ class TestListSubnet(TestSubnet): parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - filters = {'tenant_id': project.id} + filters = {'tenant_id': project.id, 'project_id': project.id} self.network.subnets.assert_called_once_with(**filters) self.assertEqual(self.columns, columns) @@ -723,7 +723,7 @@ class TestListSubnet(TestSubnet): parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - filters = {'tenant_id': project.id} + filters = {'tenant_id': project.id, 'project_id': project.id} self.network.subnets.assert_called_once_with(**filters) self.assertEqual(self.columns, columns) diff --git a/openstackclient/tests/unit/volume/v1/test_snapshot.py b/openstackclient/tests/unit/volume/v1/test_snapshot.py index edfbdc19..8e30d6a9 100644 --- a/openstackclient/tests/unit/volume/v1/test_snapshot.py +++ b/openstackclient/tests/unit/volume/v1/test_snapshot.py @@ -19,7 +19,7 @@ from osc_lib import exceptions from osc_lib import utils from openstackclient.tests.unit.volume.v1 import fakes as volume_fakes -from openstackclient.volume.v1 import snapshot +from openstackclient.volume.v1 import volume_snapshot class TestSnapshot(volume_fakes.TestVolumev1): @@ -67,20 +67,20 @@ class TestSnapshotCreate(TestSnapshot): self.volumes_mock.get.return_value = self.volume self.snapshots_mock.create.return_value = self.new_snapshot # Get the command object to test - self.cmd = snapshot.CreateSnapshot(self.app, None) + self.cmd = volume_snapshot.CreateVolumeSnapshot(self.app, None) def test_snapshot_create(self): arglist = [ - "--name", self.new_snapshot.display_name, + "--volume", self.new_snapshot.volume_id, "--description", self.new_snapshot.display_description, "--force", - self.new_snapshot.volume_id, + self.new_snapshot.display_name, ] verifylist = [ - ("name", self.new_snapshot.display_name), + ("volume", self.new_snapshot.volume_id), ("description", self.new_snapshot.display_description), ("force", True), - ("volume", self.new_snapshot.volume_id), + ("snapshot_name", self.new_snapshot.display_name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -97,7 +97,7 @@ class TestSnapshotCreate(TestSnapshot): def test_snapshot_create_without_name(self): arglist = [ - self.new_snapshot.volume_id, + "--volume", self.new_snapshot.volume_id, "--description", self.new_snapshot.display_description, "--force" ] @@ -119,6 +119,32 @@ class TestSnapshotCreate(TestSnapshot): self.assertEqual(self.columns, columns) self.assertEqual(self.data, data) + def test_snapshot_create_without_volume(self): + arglist = [ + "--description", self.new_snapshot.display_description, + "--force", + self.new_snapshot.display_name + ] + verifylist = [ + ("description", self.new_snapshot.display_description), + ("force", True), + ("snapshot_name", self.new_snapshot.display_name) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.volumes_mock.get.assert_called_once_with( + self.new_snapshot.display_name) + self.snapshots_mock.create.assert_called_once_with( + self.new_snapshot.volume_id, + True, + self.new_snapshot.display_name, + self.new_snapshot.display_description, + ) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + class TestSnapshotDelete(TestSnapshot): @@ -132,7 +158,7 @@ class TestSnapshotDelete(TestSnapshot): self.snapshots_mock.delete.return_value = None # Get the command object to mock - self.cmd = snapshot.DeleteSnapshot(self.app, None) + self.cmd = volume_snapshot.DeleteVolumeSnapshot(self.app, None) def test_snapshot_delete(self): arglist = [ @@ -244,7 +270,7 @@ class TestSnapshotList(TestSnapshot): self.volumes_mock.list.return_value = [self.volume] self.snapshots_mock.list.return_value = self.snapshots # Get the command to test - self.cmd = snapshot.ListSnapshot(self.app, None) + self.cmd = volume_snapshot.ListVolumeSnapshot(self.app, None) def test_snapshot_list_without_options(self): arglist = [] @@ -307,7 +333,7 @@ class TestSnapshotSet(TestSnapshot): self.snapshots_mock.get.return_value = self.snapshot self.snapshots_mock.set_metadata.return_value = None # Get the command object to mock - self.cmd = snapshot.SetSnapshot(self.app, None) + self.cmd = volume_snapshot.SetVolumeSnapshot(self.app, None) def test_snapshot_set_all(self): arglist = [ @@ -404,7 +430,7 @@ class TestSnapshotShow(TestSnapshot): self.snapshots_mock.get.return_value = self.snapshot # Get the command object to test - self.cmd = snapshot.ShowSnapshot(self.app, None) + self.cmd = volume_snapshot.ShowVolumeSnapshot(self.app, None) def test_snapshot_show(self): arglist = [ @@ -432,7 +458,7 @@ class TestSnapshotUnset(TestSnapshot): self.snapshots_mock.get.return_value = self.snapshot self.snapshots_mock.delete_metadata.return_value = None # Get the command object to mock - self.cmd = snapshot.UnsetSnapshot(self.app, None) + self.cmd = volume_snapshot.UnsetVolumeSnapshot(self.app, None) def test_snapshot_unset(self): arglist = [ diff --git a/openstackclient/tests/unit/volume/v1/test_type.py b/openstackclient/tests/unit/volume/v1/test_type.py index 23a1186d..81ad8301 100644 --- a/openstackclient/tests/unit/volume/v1/test_type.py +++ b/openstackclient/tests/unit/volume/v1/test_type.py @@ -158,11 +158,13 @@ class TestTypeList(TestType): columns = ( "ID", - "Name" + "Name", + "Is Public", ) columns_long = ( "ID", "Name", + "Is Public", "Properties" ) @@ -171,12 +173,14 @@ class TestTypeList(TestType): data.append(( t.id, t.name, + t.is_public, )) data_long = [] for t in volume_types: data_long.append(( t.id, t.name, + t.is_public, utils.format_dict(t.extra_specs), )) diff --git a/openstackclient/tests/unit/volume/v1/test_volume.py b/openstackclient/tests/unit/volume/v1/test_volume.py index 25707288..7a44dea8 100644 --- a/openstackclient/tests/unit/volume/v1/test_volume.py +++ b/openstackclient/tests/unit/volume/v1/test_volume.py @@ -57,10 +57,6 @@ class TestVolume(volume_fakes.TestVolumev1): return volumes -# TODO(dtroyer): The volume create tests are incomplete, only the minimal -# options and the options that require additional processing -# are implemented at this time. - class TestVolumeCreate(TestVolume): project = identity_fakes.FakeProject.create_one_project() diff --git a/openstackclient/tests/unit/volume/v2/fakes.py b/openstackclient/tests/unit/volume/v2/fakes.py index 5e1d16e1..3137bfb0 100644 --- a/openstackclient/tests/unit/volume/v2/fakes.py +++ b/openstackclient/tests/unit/volume/v2/fakes.py @@ -224,6 +224,8 @@ class FakeVolumeClient(object): self.quota_classes.resource_class = fakes.FakeResource(None, {}) self.consistencygroups = mock.Mock() self.consistencygroups.resource_class = fakes.FakeResource(None, {}) + self.cgsnapshots = mock.Mock() + self.cgsnapshots.resource_class = fakes.FakeResource(None, {}) self.auth_token = kwargs['token'] self.management_url = kwargs['endpoint'] @@ -248,10 +250,7 @@ class TestVolume(utils.TestCommand): class FakeVolume(object): - """Fake one or more volumes. - - TODO(xiexs): Currently, only volume API v2 is supported by this class. - """ + """Fake one or more volumes.""" @staticmethod def create_one_volume(attrs=None): @@ -547,6 +546,106 @@ class FakeConsistencyGroup(object): return consistency_groups + @staticmethod + def get_consistency_groups(consistency_groups=None, count=2): + """Note: + + Get an iterable MagicMock object with a list of faked + consistency_groups. + + If consistency_groups list is provided, then initialize + the Mock object with the list. Otherwise create one. + + :param List consistency_groups: + A list of FakeResource objects faking consistency_groups + :param Integer count: + The number of consistency_groups to be faked + :return + An iterable Mock object with side_effect set to a list of faked + consistency_groups + """ + if consistency_groups is None: + consistency_groups = (FakeConsistencyGroup. + create_consistency_groups(count)) + + return mock.Mock(side_effect=consistency_groups) + + +class FakeConsistencyGroupSnapshot(object): + """Fake one or more consistency group snapshot.""" + + @staticmethod + def create_one_consistency_group_snapshot(attrs=None): + """Create a fake consistency group snapshot. + + :param Dictionary attrs: + A dictionary with all attributes + :return: + A FakeResource object with id, name, description, etc. + """ + attrs = attrs or {} + + # Set default attributes. + consistency_group_snapshot_info = { + "id": 'id-' + uuid.uuid4().hex, + "name": 'backup-name-' + uuid.uuid4().hex, + "description": 'description-' + uuid.uuid4().hex, + "status": "error", + "consistencygroup_id": 'consistency-group-id' + uuid.uuid4().hex, + "created_at": 'time-' + uuid.uuid4().hex, + } + + # Overwrite default attributes. + consistency_group_snapshot_info.update(attrs) + + consistency_group_snapshot = fakes.FakeResource( + info=copy.deepcopy(consistency_group_snapshot_info), + loaded=True) + return consistency_group_snapshot + + @staticmethod + def create_consistency_group_snapshots(attrs=None, count=2): + """Create multiple fake consistency group snapshots. + + :param Dictionary attrs: + A dictionary with all attributes + :param int count: + The number of consistency group snapshots to fake + :return: + A list of FakeResource objects faking the + consistency group snapshots + """ + consistency_group_snapshots = [] + for i in range(0, count): + consistency_group_snapshot = ( + FakeConsistencyGroupSnapshot. + create_one_consistency_group_snapshot(attrs) + ) + consistency_group_snapshots.append(consistency_group_snapshot) + + return consistency_group_snapshots + + @staticmethod + def get_consistency_group_snapshots(snapshots=None, count=2): + """Get an iterable MagicMock object with a list of faked cgsnapshots. + + If consistenct group snapshots list is provided, then initialize + the Mock object with the list. Otherwise create one. + + :param List snapshots: + A list of FakeResource objects faking consistency group snapshots + :param Integer count: + The number of consistency group snapshots to be faked + :return + An iterable Mock object with side_effect set to a list of faked + consistency groups + """ + if snapshots is None: + snapshots = (FakeConsistencyGroupSnapshot. + create_consistency_group_snapshots(count)) + + return mock.Mock(side_effect=snapshots) + class FakeExtension(object): """Fake one or more extension.""" diff --git a/openstackclient/tests/unit/volume/v2/test_consistency_group.py b/openstackclient/tests/unit/volume/v2/test_consistency_group.py index 00e1b60e..5beb6ef2 100644 --- a/openstackclient/tests/unit/volume/v2/test_consistency_group.py +++ b/openstackclient/tests/unit/volume/v2/test_consistency_group.py @@ -12,6 +12,10 @@ # under the License. # +import mock +from mock import call + +from osc_lib import exceptions from osc_lib import utils from openstackclient.tests.unit.volume.v2 import fakes as volume_fakes @@ -28,6 +32,235 @@ class TestConsistencyGroup(volume_fakes.TestVolume): self.app.client_manager.volume.consistencygroups) self.consistencygroups_mock.reset_mock() + self.types_mock = self.app.client_manager.volume.volume_types + self.types_mock.reset_mock() + + +class TestConsistencyGroupCreate(TestConsistencyGroup): + + volume_type = volume_fakes.FakeType.create_one_type() + new_consistency_group = ( + volume_fakes.FakeConsistencyGroup.create_one_consistency_group()) + + columns = ( + 'availability_zone', + 'created_at', + 'description', + 'id', + 'name', + 'status', + 'volume_types', + ) + data = ( + new_consistency_group.availability_zone, + new_consistency_group.created_at, + new_consistency_group.description, + new_consistency_group.id, + new_consistency_group.name, + new_consistency_group.status, + new_consistency_group.volume_types, + ) + + def setUp(self): + super(TestConsistencyGroupCreate, self).setUp() + self.consistencygroups_mock.create.return_value = ( + self.new_consistency_group) + self.consistencygroups_mock.create_from_src.return_value = ( + self.new_consistency_group) + self.consistencygroups_mock.get.return_value = ( + self.new_consistency_group) + self.types_mock.get.return_value = self.volume_type + + # Get the command object to test + self.cmd = consistency_group.CreateConsistencyGroup(self.app, None) + + def test_consistency_group_create(self): + arglist = [ + '--volume-type', self.volume_type.id, + '--description', self.new_consistency_group.description, + '--availability-zone', + self.new_consistency_group.availability_zone, + self.new_consistency_group.name, + ] + verifylist = [ + ('volume_type', self.volume_type.id), + ('description', self.new_consistency_group.description), + ('availability_zone', + self.new_consistency_group.availability_zone), + ('name', self.new_consistency_group.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.types_mock.get.assert_called_once_with( + self.volume_type.id) + self.consistencygroups_mock.get.assert_not_called() + self.consistencygroups_mock.create.assert_called_once_with( + self.volume_type.id, + name=self.new_consistency_group.name, + description=self.new_consistency_group.description, + availability_zone=self.new_consistency_group.availability_zone, + ) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + def test_consistency_group_create_without_name(self): + arglist = [ + '--volume-type', self.volume_type.id, + '--description', self.new_consistency_group.description, + '--availability-zone', + self.new_consistency_group.availability_zone, + ] + verifylist = [ + ('volume_type', self.volume_type.id), + ('description', self.new_consistency_group.description), + ('availability_zone', + self.new_consistency_group.availability_zone), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.types_mock.get.assert_called_once_with( + self.volume_type.id) + self.consistencygroups_mock.get.assert_not_called() + self.consistencygroups_mock.create.assert_called_once_with( + self.volume_type.id, + name=None, + description=self.new_consistency_group.description, + availability_zone=self.new_consistency_group.availability_zone, + ) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + def test_consistency_group_create_from_source(self): + arglist = [ + '--consistency-group-source', self.new_consistency_group.id, + '--description', self.new_consistency_group.description, + self.new_consistency_group.name, + ] + verifylist = [ + ('consistency_group_source', self.new_consistency_group.id), + ('description', self.new_consistency_group.description), + ('name', self.new_consistency_group.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.types_mock.get.assert_not_called() + self.consistencygroups_mock.get.assert_called_once_with( + self.new_consistency_group.id) + self.consistencygroups_mock.create_from_src.assert_called_with( + None, + self.new_consistency_group.id, + name=self.new_consistency_group.name, + description=self.new_consistency_group.description, + ) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + +class TestConsistencyGroupDelete(TestConsistencyGroup): + + consistency_groups =\ + volume_fakes.FakeConsistencyGroup.create_consistency_groups(count=2) + + def setUp(self): + super(TestConsistencyGroupDelete, self).setUp() + + self.consistencygroups_mock.get = volume_fakes.FakeConsistencyGroup.\ + get_consistency_groups(self.consistency_groups) + self.consistencygroups_mock.delete.return_value = None + + # Get the command object to mock + self.cmd = consistency_group.DeleteConsistencyGroup(self.app, None) + + def test_consistency_group_delete(self): + arglist = [ + self.consistency_groups[0].id + ] + verifylist = [ + ("consistency_groups", [self.consistency_groups[0].id]) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.consistencygroups_mock.delete.assert_called_with( + self.consistency_groups[0].id, False) + self.assertIsNone(result) + + def test_consistency_group_delete_with_force(self): + arglist = [ + '--force', + self.consistency_groups[0].id, + ] + verifylist = [ + ('force', True), + ("consistency_groups", [self.consistency_groups[0].id]) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.consistencygroups_mock.delete.assert_called_with( + self.consistency_groups[0].id, True) + self.assertIsNone(result) + + def test_delete_multiple_consistency_groups(self): + arglist = [] + for b in self.consistency_groups: + arglist.append(b.id) + verifylist = [ + ('consistency_groups', arglist), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + calls = [] + for b in self.consistency_groups: + calls.append(call(b.id, False)) + self.consistencygroups_mock.delete.assert_has_calls(calls) + self.assertIsNone(result) + + def test_delete_multiple_consistency_groups_with_exception(self): + arglist = [ + self.consistency_groups[0].id, + 'unexist_consistency_group', + ] + verifylist = [ + ('consistency_groups', arglist), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + find_mock_result = [self.consistency_groups[0], + exceptions.CommandError] + with mock.patch.object(utils, 'find_resource', + side_effect=find_mock_result) as find_mock: + try: + self.cmd.take_action(parsed_args) + self.fail('CommandError should be raised.') + except exceptions.CommandError as e: + self.assertEqual('1 of 2 consistency groups failed to delete.', + str(e)) + + find_mock.assert_any_call(self.consistencygroups_mock, + self.consistency_groups[0].id) + find_mock.assert_any_call(self.consistencygroups_mock, + 'unexist_consistency_group') + + self.assertEqual(2, find_mock.call_count) + self.consistencygroups_mock.delete.assert_called_once_with( + self.consistency_groups[0].id, False + ) + class TestConsistencyGroupList(TestConsistencyGroup): @@ -120,3 +353,46 @@ class TestConsistencyGroupList(TestConsistencyGroup): detailed=True, search_opts={'all_tenants': False}) self.assertEqual(self.columns_long, columns) self.assertEqual(self.data_long, list(data)) + + +class TestConsistencyGroupShow(TestConsistencyGroup): + columns = ( + 'availability_zone', + 'created_at', + 'description', + 'id', + 'name', + 'status', + 'volume_types', + ) + + def setUp(self): + super(TestConsistencyGroupShow, self).setUp() + + self.consistency_group = ( + volume_fakes.FakeConsistencyGroup.create_one_consistency_group()) + self.data = ( + self.consistency_group.availability_zone, + self.consistency_group.created_at, + self.consistency_group.description, + self.consistency_group.id, + self.consistency_group.name, + self.consistency_group.status, + self.consistency_group.volume_types, + ) + self.consistencygroups_mock.get.return_value = self.consistency_group + self.cmd = consistency_group.ShowConsistencyGroup(self.app, None) + + def test_consistency_group_show(self): + arglist = [ + self.consistency_group.id + ] + verifylist = [ + ("consistency_group", self.consistency_group.id) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + self.consistencygroups_mock.get.assert_called_once_with( + self.consistency_group.id) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) diff --git a/openstackclient/tests/unit/volume/v2/test_consistency_group_snapshot.py b/openstackclient/tests/unit/volume/v2/test_consistency_group_snapshot.py new file mode 100644 index 00000000..3bfe93df --- /dev/null +++ b/openstackclient/tests/unit/volume/v2/test_consistency_group_snapshot.py @@ -0,0 +1,351 @@ +# +# 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 mock import call + +from openstackclient.tests.unit.volume.v2 import fakes as volume_fakes +from openstackclient.volume.v2 import consistency_group_snapshot + + +class TestConsistencyGroupSnapshot(volume_fakes.TestVolume): + + def setUp(self): + super(TestConsistencyGroupSnapshot, self).setUp() + + # Get a shortcut to the TransferManager Mock + self.cgsnapshots_mock = ( + self.app.client_manager.volume.cgsnapshots) + self.cgsnapshots_mock.reset_mock() + self.consistencygroups_mock = ( + self.app.client_manager.volume.consistencygroups) + self.consistencygroups_mock.reset_mock() + + +class TestConsistencyGroupSnapshotCreate(TestConsistencyGroupSnapshot): + + _consistency_group_snapshot = ( + volume_fakes. + FakeConsistencyGroupSnapshot. + create_one_consistency_group_snapshot() + ) + consistency_group = ( + volume_fakes.FakeConsistencyGroup.create_one_consistency_group()) + + columns = ( + 'consistencygroup_id', + 'created_at', + 'description', + 'id', + 'name', + 'status', + ) + data = ( + _consistency_group_snapshot.consistencygroup_id, + _consistency_group_snapshot.created_at, + _consistency_group_snapshot.description, + _consistency_group_snapshot.id, + _consistency_group_snapshot.name, + _consistency_group_snapshot.status, + ) + + def setUp(self): + super(TestConsistencyGroupSnapshotCreate, self).setUp() + self.cgsnapshots_mock.create.return_value = ( + self._consistency_group_snapshot) + self.consistencygroups_mock.get.return_value = ( + self.consistency_group) + + # Get the command object to test + self.cmd = (consistency_group_snapshot. + CreateConsistencyGroupSnapshot(self.app, None)) + + def test_consistency_group_snapshot_create(self): + arglist = [ + '--consistency-group', self.consistency_group.id, + '--description', self._consistency_group_snapshot.description, + self._consistency_group_snapshot.name, + ] + verifylist = [ + ('consistency_group', self.consistency_group.id), + ('description', self._consistency_group_snapshot.description), + ('snapshot_name', self._consistency_group_snapshot.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.consistencygroups_mock.get.assert_called_once_with( + self.consistency_group.id) + self.cgsnapshots_mock.create.assert_called_once_with( + self.consistency_group.id, + name=self._consistency_group_snapshot.name, + description=self._consistency_group_snapshot.description, + ) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + def test_consistency_group_snapshot_create_no_consistency_group(self): + arglist = [ + '--description', self._consistency_group_snapshot.description, + self._consistency_group_snapshot.name, + ] + verifylist = [ + ('description', self._consistency_group_snapshot.description), + ('snapshot_name', self._consistency_group_snapshot.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.consistencygroups_mock.get.assert_called_once_with( + self._consistency_group_snapshot.name) + self.cgsnapshots_mock.create.assert_called_once_with( + self.consistency_group.id, + name=self._consistency_group_snapshot.name, + description=self._consistency_group_snapshot.description, + ) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + +class TestConsistencyGroupSnapshotDelete(TestConsistencyGroupSnapshot): + + consistency_group_snapshots = ( + volume_fakes.FakeConsistencyGroupSnapshot. + create_consistency_group_snapshots(count=2) + ) + + def setUp(self): + super(TestConsistencyGroupSnapshotDelete, self).setUp() + + self.cgsnapshots_mock.get = ( + volume_fakes.FakeConsistencyGroupSnapshot. + get_consistency_group_snapshots(self.consistency_group_snapshots) + ) + self.cgsnapshots_mock.delete.return_value = None + + # Get the command object to mock + self.cmd = (consistency_group_snapshot. + DeleteConsistencyGroupSnapshot(self.app, None)) + + def test_consistency_group_snapshot_delete(self): + arglist = [ + self.consistency_group_snapshots[0].id + ] + verifylist = [ + ("consistency_group_snapshot", + [self.consistency_group_snapshots[0].id]) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.cgsnapshots_mock.delete.assert_called_once_with( + self.consistency_group_snapshots[0].id) + self.assertIsNone(result) + + def test_multiple_consistency_group_snapshots_delete(self): + arglist = [] + for c in self.consistency_group_snapshots: + arglist.append(c.id) + verifylist = [ + ('consistency_group_snapshot', arglist), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + calls = [] + for c in self.consistency_group_snapshots: + calls.append(call(c.id)) + self.cgsnapshots_mock.delete.assert_has_calls(calls) + self.assertIsNone(result) + + +class TestConsistencyGroupSnapshotList(TestConsistencyGroupSnapshot): + + consistency_group_snapshots = ( + volume_fakes.FakeConsistencyGroupSnapshot. + create_consistency_group_snapshots(count=2) + ) + consistency_group = ( + volume_fakes.FakeConsistencyGroup.create_one_consistency_group() + ) + + columns = [ + 'ID', + 'Status', + 'Name', + ] + columns_long = [ + 'ID', + 'Status', + 'ConsistencyGroup ID', + 'Name', + 'Description', + 'Created At', + ] + data = [] + for c in consistency_group_snapshots: + data.append(( + c.id, + c.status, + c.name, + )) + data_long = [] + for c in consistency_group_snapshots: + data_long.append(( + c.id, + c.status, + c.consistencygroup_id, + c.name, + c.description, + c.created_at, + )) + + def setUp(self): + super(TestConsistencyGroupSnapshotList, self).setUp() + + self.cgsnapshots_mock.list.return_value = ( + self.consistency_group_snapshots) + self.consistencygroups_mock.get.return_value = self.consistency_group + # Get the command to test + self.cmd = ( + consistency_group_snapshot. + ListConsistencyGroupSnapshot(self.app, None) + ) + + def test_consistency_group_snapshot_list_without_options(self): + arglist = [] + verifylist = [ + ("all_projects", False), + ("long", False), + ("status", None), + ("consistency_group", None), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + search_opts = { + 'all_tenants': False, + 'status': None, + 'consistencygroup_id': None, + } + self.cgsnapshots_mock.list.assert_called_once_with( + detailed=True, search_opts=search_opts) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, list(data)) + + def test_consistency_group_snapshot_list_with_long(self): + arglist = [ + "--long", + ] + verifylist = [ + ("all_projects", False), + ("long", True), + ("status", None), + ("consistency_group", None), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + search_opts = { + 'all_tenants': False, + 'status': None, + 'consistencygroup_id': None, + } + self.cgsnapshots_mock.list.assert_called_once_with( + detailed=True, search_opts=search_opts) + self.assertEqual(self.columns_long, columns) + self.assertEqual(self.data_long, list(data)) + + def test_consistency_group_snapshot_list_with_options(self): + arglist = [ + "--all-project", + "--status", self.consistency_group_snapshots[0].status, + "--consistency-group", self.consistency_group.id, + ] + verifylist = [ + ("all_projects", True), + ("long", False), + ("status", self.consistency_group_snapshots[0].status), + ("consistency_group", self.consistency_group.id), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + search_opts = { + 'all_tenants': True, + 'status': self.consistency_group_snapshots[0].status, + 'consistencygroup_id': self.consistency_group.id, + } + self.consistencygroups_mock.get.assert_called_once_with( + self.consistency_group.id) + self.cgsnapshots_mock.list.assert_called_once_with( + detailed=True, search_opts=search_opts) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, list(data)) + + +class TestConsistencyGroupSnapshotShow(TestConsistencyGroupSnapshot): + + _consistency_group_snapshot = ( + volume_fakes. + FakeConsistencyGroupSnapshot. + create_one_consistency_group_snapshot() + ) + + columns = ( + 'consistencygroup_id', + 'created_at', + 'description', + 'id', + 'name', + 'status', + ) + data = ( + _consistency_group_snapshot.consistencygroup_id, + _consistency_group_snapshot.created_at, + _consistency_group_snapshot.description, + _consistency_group_snapshot.id, + _consistency_group_snapshot.name, + _consistency_group_snapshot.status, + ) + + def setUp(self): + super(TestConsistencyGroupSnapshotShow, self).setUp() + + self.cgsnapshots_mock.get.return_value = ( + self._consistency_group_snapshot) + self.cmd = (consistency_group_snapshot. + ShowConsistencyGroupSnapshot(self.app, None)) + + def test_consistency_group_snapshot_show(self): + arglist = [ + self._consistency_group_snapshot.id + ] + verifylist = [ + ("consistency_group_snapshot", self._consistency_group_snapshot.id) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + self.cgsnapshots_mock.get.assert_called_once_with( + self._consistency_group_snapshot.id) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) diff --git a/openstackclient/tests/unit/volume/v2/test_snapshot.py b/openstackclient/tests/unit/volume/v2/test_snapshot.py index d355662d..b67dd6eb 100644 --- a/openstackclient/tests/unit/volume/v2/test_snapshot.py +++ b/openstackclient/tests/unit/volume/v2/test_snapshot.py @@ -20,7 +20,7 @@ from osc_lib import exceptions from osc_lib import utils from openstackclient.tests.unit.volume.v2 import fakes as volume_fakes -from openstackclient.volume.v2 import snapshot +from openstackclient.volume.v2 import volume_snapshot class TestSnapshot(volume_fakes.TestVolume): @@ -68,23 +68,23 @@ class TestSnapshotCreate(TestSnapshot): self.volumes_mock.get.return_value = self.volume self.snapshots_mock.create.return_value = self.new_snapshot # Get the command object to test - self.cmd = snapshot.CreateSnapshot(self.app, None) + self.cmd = volume_snapshot.CreateVolumeSnapshot(self.app, None) def test_snapshot_create(self): arglist = [ - "--name", self.new_snapshot.name, + "--volume", self.new_snapshot.volume_id, "--description", self.new_snapshot.description, "--force", '--property', 'Alpha=a', '--property', 'Beta=b', - self.new_snapshot.volume_id, + self.new_snapshot.name, ] verifylist = [ - ("name", self.new_snapshot.name), + ("volume", self.new_snapshot.volume_id), ("description", self.new_snapshot.description), ("force", True), ('property', {'Alpha': 'a', 'Beta': 'b'}), - ("volume", self.new_snapshot.volume_id), + ("snapshot_name", self.new_snapshot.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -102,7 +102,7 @@ class TestSnapshotCreate(TestSnapshot): def test_snapshot_create_without_name(self): arglist = [ - self.new_snapshot.volume_id, + "--volume", self.new_snapshot.volume_id, "--description", self.new_snapshot.description, "--force" ] @@ -125,6 +125,33 @@ class TestSnapshotCreate(TestSnapshot): self.assertEqual(self.columns, columns) self.assertEqual(self.data, data) + def test_snapshot_create_without_volume(self): + arglist = [ + "--description", self.new_snapshot.description, + "--force", + self.new_snapshot.name + ] + verifylist = [ + ("description", self.new_snapshot.description), + ("force", True), + ("snapshot_name", self.new_snapshot.name) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.volumes_mock.get.assert_called_once_with( + self.new_snapshot.name) + self.snapshots_mock.create.assert_called_once_with( + self.new_snapshot.volume_id, + force=True, + name=self.new_snapshot.name, + description=self.new_snapshot.description, + metadata=None, + ) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + class TestSnapshotDelete(TestSnapshot): @@ -138,7 +165,7 @@ class TestSnapshotDelete(TestSnapshot): self.snapshots_mock.delete.return_value = None # Get the command object to mock - self.cmd = snapshot.DeleteSnapshot(self.app, None) + self.cmd = volume_snapshot.DeleteVolumeSnapshot(self.app, None) def test_snapshot_delete(self): arglist = [ @@ -250,7 +277,7 @@ class TestSnapshotList(TestSnapshot): self.volumes_mock.list.return_value = [self.volume] self.snapshots_mock.list.return_value = self.snapshots # Get the command to test - self.cmd = snapshot.ListSnapshot(self.app, None) + self.cmd = volume_snapshot.ListVolumeSnapshot(self.app, None) def test_snapshot_list_without_options(self): arglist = [] @@ -330,7 +357,7 @@ class TestSnapshotSet(TestSnapshot): self.snapshots_mock.set_metadata.return_value = None self.snapshots_mock.update.return_value = None # Get the command object to mock - self.cmd = snapshot.SetSnapshot(self.app, None) + self.cmd = volume_snapshot.SetVolumeSnapshot(self.app, None) def test_snapshot_set(self): arglist = [ @@ -457,7 +484,7 @@ class TestSnapshotShow(TestSnapshot): self.snapshots_mock.get.return_value = self.snapshot # Get the command object to test - self.cmd = snapshot.ShowSnapshot(self.app, None) + self.cmd = volume_snapshot.ShowVolumeSnapshot(self.app, None) def test_snapshot_show(self): arglist = [ @@ -485,7 +512,7 @@ class TestSnapshotUnset(TestSnapshot): self.snapshots_mock.get.return_value = self.snapshot self.snapshots_mock.delete_metadata.return_value = None # Get the command object to mock - self.cmd = snapshot.UnsetSnapshot(self.app, None) + self.cmd = volume_snapshot.UnsetVolumeSnapshot(self.app, None) def test_snapshot_unset(self): arglist = [ diff --git a/openstackclient/tests/unit/volume/v2/test_type.py b/openstackclient/tests/unit/volume/v2/test_type.py index 84f87e3b..325872d7 100644 --- a/openstackclient/tests/unit/volume/v2/test_type.py +++ b/openstackclient/tests/unit/volume/v2/test_type.py @@ -165,7 +165,8 @@ class TestTypeList(TestType): columns = [ "ID", - "Name" + "Name", + "Is Public", ] columns_long = columns + [ "Description", @@ -177,12 +178,14 @@ class TestTypeList(TestType): data.append(( t.id, t.name, + t.is_public, )) data_long = [] for t in volume_types: data_long.append(( t.id, t.name, + t.is_public, t.description, utils.format_dict(t.extra_specs), )) diff --git a/openstackclient/tests/unit/volume/v2/test_volume.py b/openstackclient/tests/unit/volume/v2/test_volume.py index fc99bf6e..41728342 100644 --- a/openstackclient/tests/unit/volume/v2/test_volume.py +++ b/openstackclient/tests/unit/volume/v2/test_volume.py @@ -46,6 +46,9 @@ class TestVolume(volume_fakes.TestVolume): self.snapshots_mock = self.app.client_manager.volume.volume_snapshots self.snapshots_mock.reset_mock() + self.types_mock = self.app.client_manager.volume.volume_types + self.types_mock.reset_mock() + self.consistencygroups_mock = ( self.app.client_manager.volume.consistencygroups) self.consistencygroups_mock.reset_mock() @@ -1088,11 +1091,14 @@ class TestVolumeMigrate(TestVolume): class TestVolumeSet(TestVolume): + volume_type = volume_fakes.FakeType.create_one_type() + def setUp(self): super(TestVolumeSet, self).setUp() self.new_volume = volume_fakes.FakeVolume.create_one_volume() self.volumes_mock.get.return_value = self.new_volume + self.types_mock.get.return_value = self.volume_type # Get the command object to test self.cmd = volume.SetVolume(self.app, None) @@ -1221,6 +1227,66 @@ class TestVolumeSet(TestVolume): False) self.assertIsNone(result) + def test_volume_set_type(self): + arglist = [ + '--type', self.volume_type.id, + self.new_volume.id + ] + verifylist = [ + ('retype_policy', None), + ('type', self.volume_type.id), + ('volume', self.new_volume.id) + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + self.volumes_mock.retype.assert_called_once_with( + self.new_volume.id, + self.volume_type.id, + 'never') + self.assertIsNone(result) + + def test_volume_set_type_with_policy(self): + arglist = [ + '--retype-policy', 'on-demand', + '--type', self.volume_type.id, + self.new_volume.id + ] + verifylist = [ + ('retype_policy', 'on-demand'), + ('type', self.volume_type.id), + ('volume', self.new_volume.id) + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + self.volumes_mock.retype.assert_called_once_with( + self.new_volume.id, + self.volume_type.id, + 'on-demand') + self.assertIsNone(result) + + @mock.patch.object(volume.LOG, 'warning') + def test_volume_set_with_only_retype_policy(self, mock_warning): + arglist = [ + '--retype-policy', 'on-demand', + self.new_volume.id + ] + verifylist = [ + ('retype_policy', 'on-demand'), + ('volume', self.new_volume.id) + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + self.volumes_mock.retype.assert_not_called() + mock_warning.assert_called_with("'--retype-policy' option will " + "not work without '--type' option") + self.assertIsNone(result) + class TestVolumeShow(TestVolume): diff --git a/openstackclient/volume/v1/snapshot.py b/openstackclient/volume/v1/snapshot.py index 0f91ee72..e9e3894b 100644 --- a/openstackclient/volume/v1/snapshot.py +++ b/openstackclient/volume/v1/snapshot.py @@ -13,6 +13,10 @@ # under the License. # +# TODO(Huanxuan Ao): Remove this file and "snapshot create", "snapshot delete", +# "snapshot set", "snapshot show" and "snapshot unset" +# commands two cycles after Ocata. + """Volume v1 Snapshot action implementations""" import copy @@ -27,6 +31,8 @@ import six from openstackclient.i18n import _ +deprecated = True +LOG_DEP = logging.getLogger('deprecated') LOG = logging.getLogger(__name__) @@ -61,6 +67,8 @@ class CreateSnapshot(command.ShowOne): return parser def take_action(self, parsed_args): + LOG_DEP.warning(_('This command has been deprecated. ' + 'Please use "volume snapshot create" instead.')) volume_client = self.app.client_manager.volume volume_id = utils.find_resource(volume_client.volumes, parsed_args.volume).id @@ -92,6 +100,8 @@ class DeleteSnapshot(command.Command): return parser def take_action(self, parsed_args): + LOG_DEP.warning(_('This command has been deprecated. ' + 'Please use "volume snapshot delete" instead.')) volume_client = self.app.client_manager.volume result = 0 @@ -133,6 +143,8 @@ class ListSnapshot(command.Lister): return parser def take_action(self, parsed_args): + LOG_DEP.warning(_('This command has been deprecated. ' + 'Please use "volume snapshot list" instead.')) def _format_volume_id(volume_id): """Return a volume name if available @@ -214,6 +226,8 @@ class SetSnapshot(command.Command): return parser def take_action(self, parsed_args): + LOG_DEP.warning(_('This command has been deprecated. ' + 'Please use "volume snapshot set" instead.')) volume_client = self.app.client_manager.volume snapshot = utils.find_resource(volume_client.volume_snapshots, parsed_args.snapshot) @@ -258,6 +272,8 @@ class ShowSnapshot(command.ShowOne): return parser def take_action(self, parsed_args): + LOG_DEP.warning(_('This command has been deprecated. ' + 'Please use "volume snapshot show" instead.')) volume_client = self.app.client_manager.volume snapshot = utils.find_resource(volume_client.volume_snapshots, parsed_args.snapshot) @@ -289,6 +305,8 @@ class UnsetSnapshot(command.Command): return parser def take_action(self, parsed_args): + LOG_DEP.warning(_('This command has been deprecated. ' + 'Please use "volume snapshot unset" instead.')) volume_client = self.app.client_manager.volume snapshot = utils.find_resource( volume_client.volume_snapshots, parsed_args.snapshot) diff --git a/openstackclient/volume/v1/volume_snapshot.py b/openstackclient/volume/v1/volume_snapshot.py new file mode 100644 index 00000000..c2ecf75b --- /dev/null +++ b/openstackclient/volume/v1/volume_snapshot.py @@ -0,0 +1,305 @@ +# Copyright 2012-2013 OpenStack Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# + +"""Volume v1 Snapshot action implementations""" + +import copy +import logging + +from osc_lib.cli import parseractions +from osc_lib.command import command +from osc_lib import exceptions +from osc_lib import utils +import six + +from openstackclient.i18n import _ + + +LOG = logging.getLogger(__name__) + + +class CreateVolumeSnapshot(command.ShowOne): + """Create new volume snapshot""" + + def get_parser(self, prog_name): + parser = super(CreateVolumeSnapshot, self).get_parser(prog_name) + parser.add_argument( + 'snapshot_name', + metavar='<snapshot-name>', + nargs="?", + help=_('Name of the snapshot (default to None)'), + ) + parser.add_argument( + '--volume', + metavar='<volume>', + help=_('Volume to snapshot (name or ID) ' + '(default is <snapshot-name>)'), + ) + parser.add_argument( + '--description', + metavar='<description>', + help=_('Description of the snapshot'), + ) + parser.add_argument( + '--force', + dest='force', + action='store_true', + default=False, + help=_('Create a snapshot attached to an instance. ' + 'Default is False'), + ) + return parser + + def take_action(self, parsed_args): + volume_client = self.app.client_manager.volume + volume = parsed_args.volume + if not parsed_args.volume: + volume = parsed_args.snapshot_name + volume_id = utils.find_resource(volume_client.volumes, + volume).id + snapshot = volume_client.volume_snapshots.create( + volume_id, + parsed_args.force, + parsed_args.snapshot_name, + parsed_args.description + ) + + snapshot._info.update( + {'properties': utils.format_dict(snapshot._info.pop('metadata'))} + ) + + return zip(*sorted(six.iteritems(snapshot._info))) + + +class DeleteVolumeSnapshot(command.Command): + """Delete volume snapshot(s)""" + + def get_parser(self, prog_name): + parser = super(DeleteVolumeSnapshot, self).get_parser(prog_name) + parser.add_argument( + 'snapshots', + metavar='<snapshot>', + nargs="+", + help=_('Snapshot(s) to delete (name or ID)'), + ) + return parser + + def take_action(self, parsed_args): + volume_client = self.app.client_manager.volume + result = 0 + + for i in parsed_args.snapshots: + try: + snapshot_id = utils.find_resource( + volume_client.volume_snapshots, i).id + volume_client.volume_snapshots.delete(snapshot_id) + except Exception as e: + result += 1 + LOG.error(_("Failed to delete snapshot with " + "name or ID '%(snapshot)s': %(e)s"), + {'snapshot': i, 'e': e}) + + if result > 0: + total = len(parsed_args.snapshots) + msg = (_("%(result)s of %(total)s snapshots failed " + "to delete.") % {'result': result, 'total': total}) + raise exceptions.CommandError(msg) + + +class ListVolumeSnapshot(command.Lister): + """List volume snapshots""" + + def get_parser(self, prog_name): + parser = super(ListVolumeSnapshot, self).get_parser(prog_name) + parser.add_argument( + '--all-projects', + action='store_true', + default=False, + help=_('Include all projects (admin only)'), + ) + parser.add_argument( + '--long', + action='store_true', + default=False, + help=_('List additional fields in output'), + ) + return parser + + def take_action(self, parsed_args): + + def _format_volume_id(volume_id): + """Return a volume name if available + + :param volume_id: a volume ID + :rtype: either the volume ID or name + """ + + volume = volume_id + if volume_id in volume_cache.keys(): + volume = volume_cache[volume_id].display_name + return volume + + if parsed_args.long: + columns = ['ID', 'Display Name', 'Display Description', 'Status', + 'Size', 'Created At', 'Volume ID', 'Metadata'] + column_headers = copy.deepcopy(columns) + column_headers[6] = 'Volume' + column_headers[7] = 'Properties' + else: + columns = ['ID', 'Display Name', 'Display Description', 'Status', + 'Size'] + column_headers = copy.deepcopy(columns) + + # Always update Name and Description + column_headers[1] = 'Name' + column_headers[2] = 'Description' + + # Cache the volume list + volume_cache = {} + try: + for s in self.app.client_manager.volume.volumes.list(): + volume_cache[s.id] = s + except Exception: + # Just forget it if there's any trouble + pass + + search_opts = { + 'all_tenants': parsed_args.all_projects, + } + + data = self.app.client_manager.volume.volume_snapshots.list( + search_opts=search_opts) + return (column_headers, + (utils.get_item_properties( + s, columns, + formatters={'Metadata': utils.format_dict, + 'Volume ID': _format_volume_id}, + ) for s in data)) + + +class SetVolumeSnapshot(command.Command): + """Set volume snapshot properties""" + + def get_parser(self, prog_name): + parser = super(SetVolumeSnapshot, self).get_parser(prog_name) + parser.add_argument( + 'snapshot', + metavar='<snapshot>', + help=_('Snapshot to modify (name or ID)') + ) + parser.add_argument( + '--name', + metavar='<name>', + help=_('New snapshot name') + ) + parser.add_argument( + '--description', + metavar='<description>', + help=_('New snapshot description') + ) + parser.add_argument( + '--property', + metavar='<key=value>', + action=parseractions.KeyValueAction, + help=_('Property to add/change for this snapshot ' + '(repeat option to set multiple properties)'), + ) + return parser + + def take_action(self, parsed_args): + volume_client = self.app.client_manager.volume + snapshot = utils.find_resource(volume_client.volume_snapshots, + parsed_args.snapshot) + + result = 0 + if parsed_args.property: + try: + volume_client.volume_snapshots.set_metadata( + snapshot.id, parsed_args.property) + except Exception as e: + LOG.error(_("Failed to set snapshot property: %s"), e) + result += 1 + + kwargs = {} + if parsed_args.name: + kwargs['display_name'] = parsed_args.name + if parsed_args.description: + kwargs['display_description'] = parsed_args.description + if kwargs: + try: + snapshot.update(**kwargs) + except Exception as e: + LOG.error(_("Failed to update snapshot display name " + "or display description: %s"), e) + result += 1 + + if result > 0: + raise exceptions.CommandError(_("One or more of the " + "set operations failed")) + + +class ShowVolumeSnapshot(command.ShowOne): + """Display volume snapshot details""" + + def get_parser(self, prog_name): + parser = super(ShowVolumeSnapshot, self).get_parser(prog_name) + parser.add_argument( + 'snapshot', + metavar='<snapshot>', + help=_('Snapshot to display (name or ID)') + ) + return parser + + def take_action(self, parsed_args): + volume_client = self.app.client_manager.volume + snapshot = utils.find_resource(volume_client.volume_snapshots, + parsed_args.snapshot) + + snapshot._info.update( + {'properties': utils.format_dict(snapshot._info.pop('metadata'))} + ) + + return zip(*sorted(six.iteritems(snapshot._info))) + + +class UnsetVolumeSnapshot(command.Command): + """Unset volume snapshot properties""" + + def get_parser(self, prog_name): + parser = super(UnsetVolumeSnapshot, self).get_parser(prog_name) + parser.add_argument( + 'snapshot', + metavar='<snapshot>', + help=_('Snapshot to modify (name or ID)'), + ) + parser.add_argument( + '--property', + metavar='<key>', + action='append', + help=_('Property to remove from snapshot ' + '(repeat option to remove multiple properties)'), + ) + return parser + + def take_action(self, parsed_args): + volume_client = self.app.client_manager.volume + snapshot = utils.find_resource( + volume_client.volume_snapshots, parsed_args.snapshot) + + if parsed_args.property: + volume_client.volume_snapshots.delete_metadata( + snapshot.id, + parsed_args.property, + ) diff --git a/openstackclient/volume/v1/volume_type.py b/openstackclient/volume/v1/volume_type.py index 4f159239..8adce322 100644 --- a/openstackclient/volume/v1/volume_type.py +++ b/openstackclient/volume/v1/volume_type.py @@ -111,10 +111,10 @@ class ListVolumeType(command.Lister): def take_action(self, parsed_args): if parsed_args.long: - columns = ('ID', 'Name', 'Extra Specs') - column_headers = ('ID', 'Name', 'Properties') + columns = ('ID', 'Name', 'Is Public', 'Extra Specs') + column_headers = ('ID', 'Name', 'Is Public', 'Properties') else: - columns = ('ID', 'Name') + columns = ('ID', 'Name', 'Is Public') column_headers = columns data = self.app.client_manager.volume.volume_types.list() return (column_headers, diff --git a/openstackclient/volume/v2/consistency_group.py b/openstackclient/volume/v2/consistency_group.py index 0754fdc7..661bcbe5 100644 --- a/openstackclient/volume/v2/consistency_group.py +++ b/openstackclient/volume/v2/consistency_group.py @@ -14,12 +14,135 @@ """Volume v2 consistency group action implementations""" +import logging + from osc_lib.command import command +from osc_lib import exceptions from osc_lib import utils +import six from openstackclient.i18n import _ +LOG = logging.getLogger(__name__) + + +class DeleteConsistencyGroup(command.Command): + _description = _("Delete consistency group(s).") + + def get_parser(self, prog_name): + parser = super(DeleteConsistencyGroup, self).get_parser(prog_name) + parser.add_argument( + 'consistency_groups', + metavar='<consistency-group>', + nargs="+", + help=_('Consistency group(s) to delete (name or ID)'), + ) + parser.add_argument( + '--force', + action='store_true', + default=False, + help=_("Allow delete in state other than error or available"), + ) + return parser + + def take_action(self, parsed_args): + volume_client = self.app.client_manager.volume + result = 0 + + for i in parsed_args.consistency_groups: + try: + consistency_group_id = utils.find_resource( + volume_client.consistencygroups, i).id + volume_client.consistencygroups.delete( + consistency_group_id, parsed_args.force) + except Exception as e: + result += 1 + LOG.error(_("Failed to delete consistency group with " + "name or ID '%(consistency_group)s':%(e)s") + % {'consistency_group': i, 'e': e}) + + if result > 0: + total = len(parsed_args.consistency_groups) + msg = (_("%(result)s of %(total)s consistency groups failed " + "to delete.") % {'result': result, 'total': total}) + raise exceptions.CommandError(msg) + + +LOG = logging.getLogger(__name__) + + +class CreateConsistencyGroup(command.ShowOne): + _description = _("Create new consistency group.") + + def get_parser(self, prog_name): + parser = super(CreateConsistencyGroup, self).get_parser(prog_name) + parser.add_argument( + "name", + metavar="<name>", + nargs="?", + help=_("Name of new consistency group (default to None)") + ) + exclusive_group = parser.add_mutually_exclusive_group(required=True) + exclusive_group.add_argument( + "--volume-type", + metavar="<volume-type>", + help=_("Volume type of this consistency group (name or ID)") + ) + exclusive_group.add_argument( + "--consistency-group-source", + metavar="<consistency-group>", + help=_("Existing consistency group (name or ID)") + ) + parser.add_argument( + "--description", + metavar="<description>", + help=_("Description of this consistency group") + ) + parser.add_argument( + "--availability-zone", + metavar="<availability-zone>", + help=_("Availability zone for this consistency group " + "(not available if creating consistency group " + "from source)"), + ) + return parser + + def take_action(self, parsed_args): + volume_client = self.app.client_manager.volume + if parsed_args.volume_type: + volume_type_id = utils.find_resource( + volume_client.volume_types, + parsed_args.volume_type).id + consistency_group = volume_client.consistencygroups.create( + volume_type_id, + name=parsed_args.name, + description=parsed_args.description, + availability_zone=parsed_args.availability_zone + ) + elif parsed_args.consistency_group_source: + if parsed_args.availability_zone: + msg = _("'--availability-zone' option will not work " + "if creating consistency group from source") + LOG.warning(msg) + consistency_group_id = utils.find_resource( + volume_client.consistencygroups, + parsed_args.consistency_group_source).id + consistency_group_snapshot = None + # TODO(Huanxuan Ao): Support for creating from consistency group + # snapshot after adding "consistency_group_snapshot" resource + consistency_group = ( + volume_client.consistencygroups.create_from_src( + consistency_group_snapshot, + consistency_group_id, + name=parsed_args.name, + description=parsed_args.description + ) + ) + + return zip(*sorted(six.iteritems(consistency_group._info))) + + class ListConsistencyGroup(command.Lister): _description = _("List consistency groups.") @@ -55,3 +178,23 @@ class ListConsistencyGroup(command.Lister): s, columns, formatters={'Volume Types': utils.format_list}) for s in consistency_groups)) + + +class ShowConsistencyGroup(command.ShowOne): + _description = _("Display consistency group details.") + + def get_parser(self, prog_name): + parser = super(ShowConsistencyGroup, self).get_parser(prog_name) + parser.add_argument( + "consistency_group", + metavar="<consistency-group>", + help=_("Consistency group to display (name or ID)") + ) + return parser + + def take_action(self, parsed_args): + volume_client = self.app.client_manager.volume + consistency_group = utils.find_resource( + volume_client.consistencygroups, + parsed_args.consistency_group) + return zip(*sorted(six.iteritems(consistency_group._info))) diff --git a/openstackclient/volume/v2/consistency_group_snapshot.py b/openstackclient/volume/v2/consistency_group_snapshot.py new file mode 100644 index 00000000..540deb01 --- /dev/null +++ b/openstackclient/volume/v2/consistency_group_snapshot.py @@ -0,0 +1,190 @@ +# +# 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 v2 consistency group snapshot action implementations""" + +import logging + +from osc_lib.command import command +from osc_lib import exceptions +from osc_lib import utils +import six + +from openstackclient.i18n import _ + + +LOG = logging.getLogger(__name__) + + +class CreateConsistencyGroupSnapshot(command.ShowOne): + _description = _("Create new consistency group snapshot.") + + def get_parser(self, prog_name): + parser = super( + CreateConsistencyGroupSnapshot, self).get_parser(prog_name) + parser.add_argument( + "snapshot_name", + metavar="<snapshot-name>", + nargs="?", + help=_("Name of new consistency group snapshot (default to None)") + ) + parser.add_argument( + "--consistency-group", + metavar="<consistency-group>", + help=_("Consistency group to snapshot (name or ID) " + "(default to be the same as <snapshot-name>)") + ) + parser.add_argument( + "--description", + metavar="<description>", + help=_("Description of this consistency group snapshot") + ) + return parser + + def take_action(self, parsed_args): + volume_client = self.app.client_manager.volume + consistency_group = parsed_args.consistency_group + if not parsed_args.consistency_group: + # If "--consistency-group" not specified, then consistency_group + # will be the same as the new consistency group snapshot name + consistency_group = parsed_args.snapshot_name + consistency_group_id = utils.find_resource( + volume_client.consistencygroups, + consistency_group).id + consistency_group_snapshot = volume_client.cgsnapshots.create( + consistency_group_id, + name=parsed_args.snapshot_name, + description=parsed_args.description, + ) + + return zip(*sorted(six.iteritems(consistency_group_snapshot._info))) + + +class DeleteConsistencyGroupSnapshot(command.Command): + _description = _("Delete consistency group snapshot(s).") + + def get_parser(self, prog_name): + parser = super( + DeleteConsistencyGroupSnapshot, self).get_parser(prog_name) + parser.add_argument( + "consistency_group_snapshot", + metavar="<consistency-group-snapshot>", + nargs="+", + help=_("Consistency group snapshot(s) to delete (name or ID)") + ) + return parser + + def take_action(self, parsed_args): + volume_client = self.app.client_manager.volume + result = 0 + + for snapshot in parsed_args.consistency_group_snapshot: + try: + snapshot_id = utils.find_resource(volume_client.cgsnapshots, + snapshot).id + + volume_client.cgsnapshots.delete(snapshot_id) + except Exception as e: + result += 1 + LOG.error(_("Failed to delete consistency group snapshot " + "with name or ID '%(snapshot)s': %(e)s") + % {'snapshot': snapshot, 'e': e}) + + if result > 0: + total = len(parsed_args.consistency_group_snapshot) + msg = (_("%(result)s of %(total)s consistency group snapshots " + "failed to delete.") % {'result': result, 'total': total}) + raise exceptions.CommandError(msg) + + +class ListConsistencyGroupSnapshot(command.Lister): + _description = _("List consistency group snapshots.") + + def get_parser(self, prog_name): + parser = super( + ListConsistencyGroupSnapshot, self).get_parser(prog_name) + parser.add_argument( + '--all-projects', + action="store_true", + help=_('Show detail for all projects (admin only) ' + '(defaults to False)') + ) + parser.add_argument( + '--long', + action="store_true", + help=_('List additional fields in output') + ) + parser.add_argument( + '--status', + metavar="<status>", + choices=['available', 'error', 'creating', 'deleting', + 'error-deleting'], + help=_('Filters results by a status ("available", "error", ' + '"creating", "deleting" or "error_deleting")') + ) + parser.add_argument( + '--consistency-group', + metavar="<consistency-group>", + help=_('Filters results by a consistency group (name or ID)') + ) + return parser + + def take_action(self, parsed_args): + if parsed_args.long: + columns = ['ID', 'Status', 'ConsistencyGroup ID', + 'Name', 'Description', 'Created At'] + else: + columns = ['ID', 'Status', 'Name'] + volume_client = self.app.client_manager.volume + consistency_group_id = None + if parsed_args.consistency_group: + consistency_group_id = utils.find_resource( + volume_client.consistencygroups, + parsed_args.consistency_group, + ).id + search_opts = { + 'all_tenants': parsed_args.all_projects, + 'status': parsed_args.status, + 'consistencygroup_id': consistency_group_id, + } + consistency_group_snapshots = volume_client.cgsnapshots.list( + detailed=True, + search_opts=search_opts, + ) + + return (columns, ( + utils.get_item_properties( + s, columns) + for s in consistency_group_snapshots)) + + +class ShowConsistencyGroupSnapshot(command.ShowOne): + _description = _("Display consistency group snapshot details") + + def get_parser(self, prog_name): + parser = super( + ShowConsistencyGroupSnapshot, self).get_parser(prog_name) + parser.add_argument( + "consistency_group_snapshot", + metavar="<consistency-group-snapshot>", + help=_("Consistency group snapshot to display (name or ID)") + ) + return parser + + def take_action(self, parsed_args): + volume_client = self.app.client_manager.volume + consistency_group_snapshot = utils.find_resource( + volume_client.cgsnapshots, + parsed_args.consistency_group_snapshot) + return zip(*sorted(six.iteritems(consistency_group_snapshot._info))) diff --git a/openstackclient/volume/v2/snapshot.py b/openstackclient/volume/v2/snapshot.py index 8cda112a..a18887e3 100644 --- a/openstackclient/volume/v2/snapshot.py +++ b/openstackclient/volume/v2/snapshot.py @@ -12,6 +12,10 @@ # under the License. # +# TODO(Huanxuan Ao): Remove this file and "snapshot create", "snapshot delete", +# "snapshot set", "snapshot show" and "snapshot unset" +# commands two cycles after Ocata. + """Volume v2 snapshot action implementations""" import copy @@ -26,6 +30,8 @@ import six from openstackclient.i18n import _ +deprecated = True +LOG_DEP = logging.getLogger('deprecated') LOG = logging.getLogger(__name__) @@ -66,6 +72,8 @@ class CreateSnapshot(command.ShowOne): return parser def take_action(self, parsed_args): + LOG_DEP.warning(_('This command has been deprecated. ' + 'Please use "volume snapshot create" instead.')) volume_client = self.app.client_manager.volume volume_id = utils.find_resource( volume_client.volumes, parsed_args.volume).id @@ -96,6 +104,8 @@ class DeleteSnapshot(command.Command): return parser def take_action(self, parsed_args): + LOG_DEP.warning(_('This command has been deprecated. ' + 'Please use "volume snapshot delete" instead.')) volume_client = self.app.client_manager.volume result = 0 @@ -149,6 +159,8 @@ class ListSnapshot(command.Lister): return parser def take_action(self, parsed_args): + LOG_DEP.warning(_('This command has been deprecated. ' + 'Please use "volume snapshot list" instead.')) def _format_volume_id(volume_id): """Return a volume name if available @@ -239,6 +251,8 @@ class SetSnapshot(command.Command): return parser def take_action(self, parsed_args): + LOG_DEP.warning(_('This command has been deprecated. ' + 'Please use "volume snapshot set" instead.')) volume_client = self.app.client_manager.volume snapshot = utils.find_resource(volume_client.volume_snapshots, parsed_args.snapshot) @@ -292,6 +306,8 @@ class ShowSnapshot(command.ShowOne): return parser def take_action(self, parsed_args): + LOG_DEP.warning(_('This command has been deprecated. ' + 'Please use "volume snapshot show" instead.')) volume_client = self.app.client_manager.volume snapshot = utils.find_resource( volume_client.volume_snapshots, parsed_args.snapshot) @@ -322,6 +338,8 @@ class UnsetSnapshot(command.Command): return parser def take_action(self, parsed_args): + LOG_DEP.warning(_('This command has been deprecated. ' + 'Please use "volume snapshot unset" instead.')) volume_client = self.app.client_manager.volume snapshot = utils.find_resource( volume_client.volume_snapshots, parsed_args.snapshot) diff --git a/openstackclient/volume/v2/volume.py b/openstackclient/volume/v2/volume.py index 0531e0aa..80abfb55 100644 --- a/openstackclient/volume/v2/volume.py +++ b/openstackclient/volume/v2/volume.py @@ -509,6 +509,19 @@ class SetVolume(command.Command): 'in the database with no regard to actual status, ' 'exercise caution when using)'), ) + parser.add_argument( + '--type', + metavar='<volume-type>', + help=_('New volume type (name or ID)'), + ) + parser.add_argument( + '--retype-policy', + metavar='<retype-policy>', + choices=['never', 'on-demand'], + help=_('Migration policy while re-typing volume ' + '("never" or "on-demand", default is "never" ) ' + '(available only when "--type" option is specified)'), + ) bootable_group = parser.add_mutually_exclusive_group() bootable_group.add_argument( "--bootable", @@ -590,6 +603,28 @@ class SetVolume(command.Command): LOG.error(_("Failed to set volume read-only access " "mode flag: %s"), e) result += 1 + if parsed_args.type: + # get the migration policy + migration_policy = 'never' + if parsed_args.retype_policy: + migration_policy = parsed_args.retype_policy + try: + # find the volume type + volume_type = utils.find_resource( + volume_client.volume_types, + parsed_args.type) + # reset to the new volume type + volume_client.volumes.retype( + volume.id, + volume_type.id, + migration_policy) + except Exception as e: + LOG.error(_("Failed to set volume type: %s"), e) + result += 1 + elif parsed_args.retype_policy: + # If the "--retype-policy" is specified without "--type" + LOG.warning(_("'--retype-policy' option will not work " + "without '--type' option")) kwargs = {} if parsed_args.name: diff --git a/openstackclient/volume/v2/volume_snapshot.py b/openstackclient/volume/v2/volume_snapshot.py new file mode 100644 index 00000000..43f30326 --- /dev/null +++ b/openstackclient/volume/v2/volume_snapshot.py @@ -0,0 +1,338 @@ +# +# 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 v2 snapshot action implementations""" + +import copy +import logging + +from osc_lib.cli import parseractions +from osc_lib.command import command +from osc_lib import exceptions +from osc_lib import utils +import six + +from openstackclient.i18n import _ + + +LOG = logging.getLogger(__name__) + + +class CreateVolumeSnapshot(command.ShowOne): + """Create new volume snapshot""" + + def get_parser(self, prog_name): + parser = super(CreateVolumeSnapshot, self).get_parser(prog_name) + parser.add_argument( + "snapshot_name", + metavar="<snapshot-name>", + nargs="?", + help=_("Name of the new snapshot (default to None)") + ) + parser.add_argument( + "--volume", + metavar="<volume>", + help=_("Volume to snapshot (name or ID) " + "(default is <snapshot-name>)") + ) + parser.add_argument( + "--description", + metavar="<description>", + help=_("Description of the snapshot") + ) + parser.add_argument( + "--force", + action="store_true", + default=False, + help=_("Create a snapshot attached to an instance. " + "Default is False") + ) + parser.add_argument( + "--property", + metavar="<key=value>", + action=parseractions.KeyValueAction, + help=_("Set a property to this snapshot " + "(repeat option to set multiple properties)"), + ) + return parser + + def take_action(self, parsed_args): + volume_client = self.app.client_manager.volume + volume = parsed_args.volume + if not parsed_args.volume: + volume = parsed_args.snapshot_name + volume_id = utils.find_resource( + volume_client.volumes, volume).id + snapshot = volume_client.volume_snapshots.create( + volume_id, + force=parsed_args.force, + name=parsed_args.snapshot_name, + description=parsed_args.description, + metadata=parsed_args.property, + ) + snapshot._info.update( + {'properties': utils.format_dict(snapshot._info.pop('metadata'))} + ) + return zip(*sorted(six.iteritems(snapshot._info))) + + +class DeleteVolumeSnapshot(command.Command): + """Delete volume snapshot(s)""" + + def get_parser(self, prog_name): + parser = super(DeleteVolumeSnapshot, self).get_parser(prog_name) + parser.add_argument( + "snapshots", + metavar="<snapshot>", + nargs="+", + help=_("Snapshot(s) to delete (name or ID)") + ) + return parser + + def take_action(self, parsed_args): + volume_client = self.app.client_manager.volume + result = 0 + + for i in parsed_args.snapshots: + try: + snapshot_id = utils.find_resource( + volume_client.volume_snapshots, i).id + volume_client.volume_snapshots.delete(snapshot_id) + except Exception as e: + result += 1 + LOG.error(_("Failed to delete snapshot with " + "name or ID '%(snapshot)s': %(e)s") + % {'snapshot': i, 'e': e}) + + if result > 0: + total = len(parsed_args.snapshots) + msg = (_("%(result)s of %(total)s snapshots failed " + "to delete.") % {'result': result, 'total': total}) + raise exceptions.CommandError(msg) + + +class ListVolumeSnapshot(command.Lister): + """List volume snapshots""" + + def get_parser(self, prog_name): + parser = super(ListVolumeSnapshot, self).get_parser(prog_name) + parser.add_argument( + '--all-projects', + action='store_true', + default=False, + help=_('Include all projects (admin only)'), + ) + parser.add_argument( + '--long', + action='store_true', + default=False, + help=_('List additional fields in output'), + ) + parser.add_argument( + '--marker', + metavar='<marker>', + help=_('The last snapshot ID of the previous page'), + ) + parser.add_argument( + '--limit', + type=int, + action=parseractions.NonNegativeAction, + metavar='<limit>', + help=_('Maximum number of snapshots to display'), + ) + return parser + + def take_action(self, parsed_args): + + def _format_volume_id(volume_id): + """Return a volume name if available + + :param volume_id: a volume ID + :rtype: either the volume ID or name + """ + + volume = volume_id + if volume_id in volume_cache.keys(): + volume = volume_cache[volume_id].name + return volume + + if parsed_args.long: + columns = ['ID', 'Name', 'Description', 'Status', + 'Size', 'Created At', 'Volume ID', 'Metadata'] + column_headers = copy.deepcopy(columns) + column_headers[6] = 'Volume' + column_headers[7] = 'Properties' + else: + columns = ['ID', 'Name', 'Description', 'Status', 'Size'] + column_headers = copy.deepcopy(columns) + + # Cache the volume list + volume_cache = {} + try: + for s in self.app.client_manager.volume.volumes.list(): + volume_cache[s.id] = s + except Exception: + # Just forget it if there's any trouble + pass + + search_opts = { + 'all_tenants': parsed_args.all_projects, + } + + data = self.app.client_manager.volume.volume_snapshots.list( + search_opts=search_opts, + marker=parsed_args.marker, + limit=parsed_args.limit, + ) + return (column_headers, + (utils.get_item_properties( + s, columns, + formatters={'Metadata': utils.format_dict, + 'Volume ID': _format_volume_id}, + ) for s in data)) + + +class SetVolumeSnapshot(command.Command): + """Set volume snapshot properties""" + + def get_parser(self, prog_name): + parser = super(SetVolumeSnapshot, self).get_parser(prog_name) + parser.add_argument( + 'snapshot', + metavar='<snapshot>', + help=_('Snapshot to modify (name or ID)') + ) + parser.add_argument( + '--name', + metavar='<name>', + help=_('New snapshot name') + ) + parser.add_argument( + '--description', + metavar='<description>', + help=_('New snapshot description') + ) + parser.add_argument( + '--property', + metavar='<key=value>', + action=parseractions.KeyValueAction, + help=_('Property to add/change for this snapshot ' + '(repeat option to set multiple properties)'), + ) + parser.add_argument( + '--state', + metavar='<state>', + choices=['available', 'error', 'creating', 'deleting', + 'error-deleting'], + help=_('New snapshot state. ("available", "error", "creating", ' + '"deleting", or "error_deleting") (admin only) ' + '(This option simply changes the state of the snapshot ' + 'in the database with no regard to actual status, ' + 'exercise caution when using)'), + ) + return parser + + def take_action(self, parsed_args): + volume_client = self.app.client_manager.volume + snapshot = utils.find_resource(volume_client.volume_snapshots, + parsed_args.snapshot) + + result = 0 + if parsed_args.property: + try: + volume_client.volume_snapshots.set_metadata( + snapshot.id, parsed_args.property) + except Exception as e: + LOG.error(_("Failed to set snapshot property: %s"), e) + result += 1 + + if parsed_args.state: + try: + volume_client.volume_snapshots.reset_state( + snapshot.id, parsed_args.state) + except Exception as e: + LOG.error(_("Failed to set snapshot state: %s"), e) + result += 1 + + kwargs = {} + if parsed_args.name: + kwargs['name'] = parsed_args.name + if parsed_args.description: + kwargs['description'] = parsed_args.description + if kwargs: + try: + volume_client.volume_snapshots.update( + snapshot.id, **kwargs) + except Exception as e: + LOG.error(_("Failed to update snapshot name " + "or description: %s"), e) + result += 1 + + if result > 0: + raise exceptions.CommandError(_("One or more of the " + "set operations failed")) + + +class ShowVolumeSnapshot(command.ShowOne): + """Display volume snapshot details""" + + def get_parser(self, prog_name): + parser = super(ShowVolumeSnapshot, self).get_parser(prog_name) + parser.add_argument( + "snapshot", + metavar="<snapshot>", + help=_("Snapshot to display (name or ID)") + ) + return parser + + def take_action(self, parsed_args): + volume_client = self.app.client_manager.volume + snapshot = utils.find_resource( + volume_client.volume_snapshots, parsed_args.snapshot) + snapshot._info.update( + {'properties': utils.format_dict(snapshot._info.pop('metadata'))} + ) + return zip(*sorted(six.iteritems(snapshot._info))) + + +class UnsetVolumeSnapshot(command.Command): + """Unset volume snapshot properties""" + + def get_parser(self, prog_name): + parser = super(UnsetVolumeSnapshot, self).get_parser(prog_name) + parser.add_argument( + 'snapshot', + metavar='<snapshot>', + help=_('Snapshot to modify (name or ID)'), + ) + parser.add_argument( + '--property', + metavar='<key>', + action='append', + default=[], + help=_('Property to remove from snapshot ' + '(repeat option to remove multiple properties)'), + ) + return parser + + def take_action(self, parsed_args): + volume_client = self.app.client_manager.volume + snapshot = utils.find_resource( + volume_client.volume_snapshots, parsed_args.snapshot) + + if parsed_args.property: + volume_client.volume_snapshots.delete_metadata( + snapshot.id, + parsed_args.property, + ) diff --git a/openstackclient/volume/v2/volume_type.py b/openstackclient/volume/v2/volume_type.py index d0df677d..42ebb53e 100644 --- a/openstackclient/volume/v2/volume_type.py +++ b/openstackclient/volume/v2/volume_type.py @@ -176,10 +176,11 @@ class ListVolumeType(command.Lister): def take_action(self, parsed_args): if parsed_args.long: - columns = ['ID', 'Name', 'Description', 'Extra Specs'] - column_headers = ['ID', 'Name', 'Description', 'Properties'] + columns = ['ID', 'Name', 'Is Public', 'Description', 'Extra Specs'] + column_headers = [ + 'ID', 'Name', 'Is Public', 'Description', 'Properties'] else: - columns = ['ID', 'Name'] + columns = ['ID', 'Name', 'Is Public'] column_headers = columns is_public = None |
