diff options
Diffstat (limited to 'openstackclient')
77 files changed, 3219 insertions, 1046 deletions
diff --git a/openstackclient/api/object_store_v1.py b/openstackclient/api/object_store_v1.py index c8514a57..8092abd0 100644 --- a/openstackclient/api/object_store_v1.py +++ b/openstackclient/api/object_store_v1.py @@ -87,7 +87,7 @@ class APIv1(api.BaseAPI): def container_list( self, - all_data=False, + full_listing=False, limit=None, marker=None, end_marker=None, @@ -96,7 +96,7 @@ class APIv1(api.BaseAPI): ): """Get containers in an account - :param boolean all_data: + :param boolean full_listing: if True, return a full listing, else returns a max of 10000 listings :param integer limit: @@ -113,7 +113,7 @@ class APIv1(api.BaseAPI): params['format'] = 'json' - if all_data: + if full_listing: data = listing = self.container_list( limit=limit, marker=marker, @@ -299,7 +299,7 @@ class APIv1(api.BaseAPI): def object_list( self, container=None, - all_data=False, + full_listing=False, limit=None, marker=None, end_marker=None, @@ -311,7 +311,7 @@ class APIv1(api.BaseAPI): :param string container: container name to get a listing for - :param boolean all_data: + :param boolean full_listing: if True, return a full listing, else returns a max of 10000 listings :param integer limit: @@ -328,11 +328,11 @@ class APIv1(api.BaseAPI): headers will be a dict and all header names will be lowercase. """ - if container is None or object is None: + if container is None: return None params['format'] = 'json' - if all_data: + if full_listing: data = listing = self.object_list( container=container, limit=limit, diff --git a/openstackclient/common/quota.py b/openstackclient/common/quota.py index 7a0dda14..11de986b 100644 --- a/openstackclient/common/quota.py +++ b/openstackclient/common/quota.py @@ -274,9 +274,18 @@ class ListQuota(command.Lister, BaseQuota): return parser def take_action(self, parsed_args): - projects = self.app.client_manager.identity.projects.list() result = [] - project_ids = [getattr(p, 'id', '') for p in projects] + project_ids = [] + if parsed_args.project is None: + for p in self.app.client_manager.identity.projects.list(): + project_ids.append(getattr(p, 'id', '')) + else: + identity_client = self.app.client_manager.identity + project = utils.find_resource( + identity_client.projects, + parsed_args.project, + ) + project_ids.append(getattr(project, 'id', '')) if parsed_args.compute: if parsed_args.detail: diff --git a/openstackclient/common/sdk_utils.py b/openstackclient/common/sdk_utils.py new file mode 100644 index 00000000..9f085617 --- /dev/null +++ b/openstackclient/common/sdk_utils.py @@ -0,0 +1,60 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import six + + +def get_osc_show_columns_for_sdk_resource( + sdk_resource, + osc_column_map, + invisible_columns=None +): + """Get and filter the display and attribute columns for an SDK resource. + + Common utility function for preparing the output of an OSC show command. + Some of the columns may need to get renamed, others made invisible. + + :param sdk_resource: An SDK resource + :param osc_column_map: A hash of mappings for display column names + :param invisible_columns: A list of invisible column names + + :returns: Two tuples containing the names of the display and attribute + columns + """ + + if getattr(sdk_resource, 'allow_get', None) is not None: + resource_dict = sdk_resource.to_dict( + body=True, headers=False, ignore_none=False) + else: + resource_dict = sdk_resource + + # Build the OSC column names to display for the SDK resource. + attr_map = {} + display_columns = list(resource_dict.keys()) + invisible_columns = [] if invisible_columns is None else invisible_columns + for col_name in invisible_columns: + if col_name in display_columns: + display_columns.remove(col_name) + for sdk_attr, osc_attr in six.iteritems(osc_column_map): + if sdk_attr in display_columns: + attr_map[osc_attr] = sdk_attr + display_columns.remove(sdk_attr) + if osc_attr not in display_columns: + display_columns.append(osc_attr) + sorted_display_columns = sorted(display_columns) + + # Build the SDK attribute names for the OSC column names. + attr_columns = [] + for column in sorted_display_columns: + new_column = attr_map[column] if column in attr_map else column + attr_columns.append(new_column) + return tuple(sorted_display_columns), tuple(attr_columns) diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index 5cc73284..93e9f966 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -143,7 +143,7 @@ def _prep_server_detail(compute_client, image_client, server, refresh=True): if image_info: image_id = image_info.get('id', '') try: - image = utils.find_resource(image_client.images, image_id) + image = image_client.get_image(image_id) info['image'] = "%s (%s)" % (image.name, image_id) except Exception: info['image'] = image_id @@ -735,10 +735,8 @@ class CreateServer(command.ShowOne): # Lookup parsed_args.image image = None if parsed_args.image: - image = utils.find_resource( - image_client.images, - parsed_args.image, - ) + image = image_client.find_image( + parsed_args.image, ignore_missing=False) if not image and parsed_args.image_property: def emit_duplicated_warning(img, image_property): @@ -749,7 +747,7 @@ class CreateServer(command.ShowOne): 'chosen_one': img_uuid_list[0]}) def _match_image(image_api, wanted_properties): - image_list = image_api.image_list() + image_list = image_api.images() images_matched = [] for img in image_list: img_dict = {} @@ -768,7 +766,7 @@ class CreateServer(command.ShowOne): return [] return images_matched - images = _match_image(image_client.api, parsed_args.image_property) + images = _match_image(image_client, parsed_args.image_property) if len(images) > 1: emit_duplicated_warning(images, parsed_args.image_property) @@ -890,8 +888,8 @@ class CreateServer(command.ShowOne): # one specified by --image, then the compute service will # create a volume from the image and attach it to the # server as a non-root volume. - image_id = utils.find_resource( - image_client.images, dev_map[0]).id + image_id = image_client.find_image(dev_map[0], + ignore_missing=False).id mapping['uuid'] = image_id # 3. append size and delete_on_termination if exist if len(dev_map) > 2 and dev_map[2]: @@ -1324,8 +1322,8 @@ class ListServer(command.Lister): # image name is given, map it to ID. image_id = None if parsed_args.image: - image_id = utils.find_resource(image_client.images, - parsed_args.image).id + image_id = image_client.find_image(parsed_args.image, + ignore_missing=False).id search_opts = { 'reservation_id': parsed_args.reservation_id, @@ -1476,12 +1474,12 @@ class ListServer(command.Lister): (s.image.get('id') for s in data if s.image))): try: - images[i_id] = image_client.images.get(i_id) + images[i_id] = image_client.get_image(i_id) except Exception: pass else: try: - images_list = image_client.images.list() + images_list = image_client.images() for i in images_list: images[i.id] = i except Exception: @@ -1923,9 +1921,12 @@ class RebuildServer(command.ShowOne): compute_client.servers, parsed_args.server) # If parsed_args.image is not set, default to the currently used one. - image_id = parsed_args.image or server.to_dict().get( - 'image', {}).get('id') - image = utils.find_resource(image_client.images, image_id) + if parsed_args.image: + image = image_client.find_image( + parsed_args.image, ignore_missing=False) + else: + image_id = server.to_dict().get('image', {}).get('id') + image = image_client.get_image(image_id) kwargs = {} if parsed_args.property: @@ -2195,10 +2196,7 @@ class RescueServer(command.Command): image = None if parsed_args.image: - image = utils.find_resource( - image_client.images, - parsed_args.image, - ) + image = image_client.find_image(parsed_args.image) utils.find_resource( compute_client.servers, diff --git a/openstackclient/compute/v2/server_backup.py b/openstackclient/compute/v2/server_backup.py index 1d560dc0..a5d43fc6 100644 --- a/openstackclient/compute/v2/server_backup.py +++ b/openstackclient/compute/v2/server_backup.py @@ -100,14 +100,11 @@ class CreateServerBackup(command.ShowOne): ) image_client = self.app.client_manager.image - image = utils.find_resource( - image_client.images, - backup_name, - ) + image = image_client.find_image(backup_name, ignore_missing=False) if parsed_args.wait: if utils.wait_for_status( - image_client.images.get, + image_client.get_image, image.id, callback=_show_progress, ): diff --git a/openstackclient/compute/v2/server_image.py b/openstackclient/compute/v2/server_image.py index b93cd4d8..fea87af8 100644 --- a/openstackclient/compute/v2/server_image.py +++ b/openstackclient/compute/v2/server_image.py @@ -79,14 +79,11 @@ class CreateServerImage(command.ShowOne): ) image_client = self.app.client_manager.image - image = utils.find_resource( - image_client.images, - image_id, - ) + image = image_client.find_image(image_id) if parsed_args.wait: if utils.wait_for_status( - image_client.images.get, + image_client.get_image, image_id, callback=_show_progress, ): diff --git a/openstackclient/identity/common.py b/openstackclient/identity/common.py index 7be2a17b..e70d87d2 100644 --- a/openstackclient/identity/common.py +++ b/openstackclient/identity/common.py @@ -213,6 +213,15 @@ def _find_identity_resource(identity_client_manager, name_or_id, return resource_type(None, {'id': name_or_id, 'name': name_or_id}) +def get_immutable_options(parsed_args): + options = {} + if parsed_args.immutable: + options['immutable'] = True + if parsed_args.no_immutable: + options['immutable'] = False + return options + + def add_user_domain_option_to_parser(parser): parser.add_argument( '--user-domain', @@ -261,3 +270,18 @@ def add_inherited_option_to_parser(parser): help=_('Specifies if the role grant is inheritable to the sub ' 'projects'), ) + + +def add_resource_option_to_parser(parser): + enable_group = parser.add_mutually_exclusive_group() + enable_group.add_argument( + '--immutable', + action='store_true', + help=_('Make resource immutable. An immutable project may not ' + 'be deleted or modified except to remove the immutable flag'), + ) + enable_group.add_argument( + '--no-immutable', + action='store_true', + help=_('Make resource mutable (default)'), + ) diff --git a/openstackclient/identity/v3/domain.py b/openstackclient/identity/v3/domain.py index dbcc97f6..e33fce05 100644 --- a/openstackclient/identity/v3/domain.py +++ b/openstackclient/identity/v3/domain.py @@ -60,6 +60,7 @@ class CreateDomain(command.ShowOne): action='store_true', help=_('Return existing domain'), ) + common.add_resource_option_to_parser(parser) return parser def take_action(self, parsed_args): @@ -69,10 +70,13 @@ class CreateDomain(command.ShowOne): if parsed_args.disable: enabled = False + options = common.get_immutable_options(parsed_args) + try: domain = identity_client.domains.create( name=parsed_args.name, description=parsed_args.description, + options=options, enabled=enabled, ) except ks_exc.Conflict: @@ -163,6 +167,7 @@ class SetDomain(command.Command): action='store_true', help=_('Disable domain'), ) + common.add_resource_option_to_parser(parser) return parser def take_action(self, parsed_args): @@ -180,6 +185,10 @@ class SetDomain(command.Command): if parsed_args.disable: kwargs['enabled'] = False + options = common.get_immutable_options(parsed_args) + if options: + kwargs['options'] = options + identity_client.domains.update(domain.id, **kwargs) diff --git a/openstackclient/identity/v3/project.py b/openstackclient/identity/v3/project.py index 9ecc70ef..5e8ce829 100644 --- a/openstackclient/identity/v3/project.py +++ b/openstackclient/identity/v3/project.py @@ -78,6 +78,7 @@ class CreateProject(command.ShowOne): action='store_true', help=_('Return existing project'), ) + common.add_resource_option_to_parser(parser) tag.add_tag_option_to_parser_for_create(parser, _('project')) return parser @@ -99,9 +100,20 @@ class CreateProject(command.ShowOne): enabled = True if parsed_args.disable: enabled = False + + options = common.get_immutable_options(parsed_args) + kwargs = {} if parsed_args.property: kwargs = parsed_args.property.copy() + if 'is_domain' in kwargs.keys(): + if kwargs['is_domain'].lower() == "true": + kwargs['is_domain'] = True + elif kwargs['is_domain'].lower() == "false": + kwargs['is_domain'] = False + elif kwargs['is_domain'].lower() == "none": + kwargs['is_domain'] = None + kwargs['tags'] = list(set(parsed_args.tags)) try: @@ -111,6 +123,7 @@ class CreateProject(command.ShowOne): parent=parent, description=parsed_args.description, enabled=enabled, + options=options, **kwargs ) except ks_exc.Conflict: @@ -317,6 +330,7 @@ class SetProject(command.Command): help=_('Set a property on <project> ' '(repeat option to set multiple properties)'), ) + common.add_resource_option_to_parser(parser) tag.add_tag_option_to_parser_for_set(parser, _('project')) return parser @@ -336,6 +350,9 @@ class SetProject(command.Command): kwargs['enabled'] = True if parsed_args.disable: kwargs['enabled'] = False + options = common.get_immutable_options(parsed_args) + if options: + kwargs['options'] = options if parsed_args.property: kwargs.update(parsed_args.property) tag.update_tags_in_args(parsed_args, project, kwargs) diff --git a/openstackclient/identity/v3/role.py b/openstackclient/identity/v3/role.py index 986f823f..980ebf11 100644 --- a/openstackclient/identity/v3/role.py +++ b/openstackclient/identity/v3/role.py @@ -177,6 +177,11 @@ class CreateRole(command.ShowOne): help=_('New role name'), ) parser.add_argument( + '--description', + metavar='<description>', + help=_('Add description about the role'), + ) + parser.add_argument( '--domain', metavar='<domain>', help=_('Domain the role belongs to (name or ID)'), @@ -186,6 +191,7 @@ class CreateRole(command.ShowOne): action='store_true', help=_('Return existing role'), ) + common.add_resource_option_to_parser(parser) return parser def take_action(self, parsed_args): @@ -196,9 +202,12 @@ class CreateRole(command.ShowOne): domain_id = common.find_domain(identity_client, parsed_args.domain).id + options = common.get_immutable_options(parsed_args) + try: role = identity_client.roles.create( - name=parsed_args.name, domain=domain_id) + name=parsed_args.name, domain=domain_id, + description=parsed_args.description, options=options) except ks_exc.Conflict: if parsed_args.or_show: @@ -346,6 +355,11 @@ class SetRole(command.Command): help=_('Role to modify (name or ID)'), ) parser.add_argument( + '--description', + metavar='<description>', + help=_('Add description about the role'), + ) + parser.add_argument( '--domain', metavar='<domain>', help=_('Domain the role belongs to (name or ID)'), @@ -355,6 +369,7 @@ class SetRole(command.Command): metavar='<name>', help=_('Set role name'), ) + common.add_resource_option_to_parser(parser) return parser def take_action(self, parsed_args): @@ -365,11 +380,14 @@ class SetRole(command.Command): domain_id = common.find_domain(identity_client, parsed_args.domain).id + options = common.get_immutable_options(parsed_args) role = utils.find_resource(identity_client.roles, parsed_args.role, domain_id=domain_id) - identity_client.roles.update(role.id, name=parsed_args.name) + identity_client.roles.update(role.id, name=parsed_args.name, + description=parsed_args.description, + options=options) class ShowRole(command.ShowOne): diff --git a/openstackclient/identity/v3/user.py b/openstackclient/identity/v3/user.py index ca85c5d8..cbc112a0 100644 --- a/openstackclient/identity/v3/user.py +++ b/openstackclient/identity/v3/user.py @@ -30,6 +30,114 @@ from openstackclient.identity import common LOG = logging.getLogger(__name__) +def _get_options_for_user(identity_client, parsed_args): + options = {} + if parsed_args.ignore_lockout_failure_attempts: + options['ignore_lockout_failure_attempts'] = True + if parsed_args.no_ignore_lockout_failure_attempts: + options['ignore_lockout_failure_attempts'] = False + if parsed_args.ignore_password_expiry: + options['ignore_password_expiry'] = True + if parsed_args.no_ignore_password_expiry: + options['ignore_password_expiry'] = False + if parsed_args.ignore_change_password_upon_first_use: + options['ignore_change_password_upon_first_use'] = True + if parsed_args.no_ignore_change_password_upon_first_use: + options['ignore_change_password_upon_first_use'] = False + if parsed_args.enable_lock_password: + options['lock_password'] = True + if parsed_args.disable_lock_password: + options['lock_password'] = False + if parsed_args.enable_multi_factor_auth: + options['multi_factor_auth_enabled'] = True + if parsed_args.disable_multi_factor_auth: + options['multi_factor_auth_enabled'] = False + if parsed_args.multi_factor_auth_rule: + auth_rules = [rule.split(",") for rule in + parsed_args.multi_factor_auth_rule] + if auth_rules: + options['multi_factor_auth_rules'] = auth_rules + return options + + +def _add_user_options(parser): + # Add additional user options + + parser.add_argument( + '--ignore-lockout-failure-attempts', + action="store_true", + help=_('Opt into ignoring the number of times a user has ' + 'authenticated and locking out the user as a result'), + ) + parser.add_argument( + '--no-ignore-lockout-failure-attempts', + action="store_true", + help=_('Opt out of ignoring the number of times a user has ' + 'authenticated and locking out the user as a result'), + ) + parser.add_argument( + '--ignore-password-expiry', + action="store_true", + help=_('Opt into allowing user to continue using passwords that ' + 'may be expired'), + ) + parser.add_argument( + '--no-ignore-password-expiry', + action="store_true", + help=_('Opt out of allowing user to continue using passwords ' + 'that may be expired'), + ) + parser.add_argument( + '--ignore-change-password-upon-first-use', + action="store_true", + help=_('Control if a user should be forced to change their password ' + 'immediately after they log into keystone for the first time. ' + 'Opt into ignoring the user to change their password during ' + 'first time login in keystone'), + ) + parser.add_argument( + '--no-ignore-change-password-upon-first-use', + action="store_true", + help=_('Control if a user should be forced to change their password ' + 'immediately after they log into keystone for the first time. ' + 'Opt out of ignoring the user to change their password during ' + 'first time login in keystone'), + ) + parser.add_argument( + '--enable-lock-password', + action="store_true", + help=_('Disables the ability for a user to change its password ' + 'through self-service APIs'), + ) + parser.add_argument( + '--disable-lock-password', + action="store_true", + help=_('Enables the ability for a user to change its password ' + 'through self-service APIs'), + ) + parser.add_argument( + '--enable-multi-factor-auth', + action="store_true", + help=_('Enables the MFA (Multi Factor Auth)'), + ) + parser.add_argument( + '--disable-multi-factor-auth', + action="store_true", + help=_('Disables the MFA (Multi Factor Auth)'), + ) + parser.add_argument( + '--multi-factor-auth-rule', + metavar='<rule>', + action="append", + default=[], + help=_('Set multi-factor auth rules. For example, to set a rule ' + 'requiring the "password" and "totp" auth methods to be ' + 'provided, use: "--multi-factor-auth-rule password,totp". ' + 'May be provided multiple times to set different rule ' + 'combinations.') + ) + + class CreateUser(command.ShowOne): _description = _("Create new user") @@ -72,6 +180,8 @@ class CreateUser(command.ShowOne): metavar='<description>', help=_('User description'), ) + _add_user_options(parser) + enable_group = parser.add_mutually_exclusive_group() enable_group.add_argument( '--enable', @@ -113,6 +223,7 @@ class CreateUser(command.ShowOne): if not parsed_args.password: LOG.warning(_("No password was supplied, authentication will fail " "when a user does not have a password.")) + options = _get_options_for_user(identity_client, parsed_args) try: user = identity_client.users.create( @@ -122,7 +233,8 @@ class CreateUser(command.ShowOne): password=parsed_args.password, email=parsed_args.email, description=parsed_args.description, - enabled=enabled + enabled=enabled, + options=options, ) except ks_exc.Conflict: if parsed_args.or_show: @@ -333,6 +445,8 @@ class SetUser(command.Command): metavar='<description>', help=_('Set user description'), ) + _add_user_options(parser) + enable_group = parser.add_mutually_exclusive_group() enable_group.add_argument( '--enable', @@ -390,6 +504,10 @@ class SetUser(command.Command): if parsed_args.disable: kwargs['enabled'] = False + options = _get_options_for_user(identity_client, parsed_args) + if options: + kwargs['options'] = options + identity_client.users.update(user.id, **kwargs) diff --git a/openstackclient/image/client.py b/openstackclient/image/client.py index b67c291f..9a0d7bac 100644 --- a/openstackclient/image/client.py +++ b/openstackclient/image/client.py @@ -26,56 +26,18 @@ DEFAULT_API_VERSION = '2' API_VERSION_OPTION = 'os_image_api_version' API_NAME = "image" API_VERSIONS = { - "1": "glanceclient.v1.client.Client", - "2": "glanceclient.v2.client.Client", -} - -IMAGE_API_TYPE = 'image' -IMAGE_API_VERSIONS = { - '1': 'openstackclient.api.image_v1.APIv1', - '2': 'openstackclient.api.image_v2.APIv2', + "1": "openstack.connection.Connection", + "2": "openstack.connection.Connection", } def make_client(instance): - """Returns an image service client""" - image_client = utils.get_client_class( - API_NAME, - instance._api_version[API_NAME], - API_VERSIONS) - LOG.debug('Instantiating image client: %s', image_client) - endpoint = instance.get_endpoint_for_service_type( - API_NAME, - region_name=instance.region_name, - interface=instance.interface, + LOG.debug( + 'Image client initialized using OpenStack SDK: %s', + instance.sdk_connection.image, ) - - client = image_client( - endpoint, - token=instance.auth.get_token(instance.session), - cacert=instance.cacert, - insecure=not instance.verify, - ) - - # Create the low-level API - - image_api = utils.get_client_class( - API_NAME, - instance._api_version[API_NAME], - IMAGE_API_VERSIONS) - LOG.debug('Instantiating image api: %s', image_api) - - client.api = image_api( - session=instance.session, - endpoint=instance.get_endpoint_for_service_type( - IMAGE_API_TYPE, - region_name=instance.region_name, - interface=instance.interface, - ) - ) - - return client + return instance.sdk_connection.image def build_option_parser(parser): diff --git a/openstackclient/image/v1/image.py b/openstackclient/image/v1/image.py index a711a128..cf1d6817 100644 --- a/openstackclient/image/v1/image.py +++ b/openstackclient/image/v1/image.py @@ -22,13 +22,13 @@ import os import sys from cliff import columns as cliff_columns -from glanceclient.common import utils as gc_utils from osc_lib.api import utils as api_utils from osc_lib.cli import format_columns from osc_lib.cli import parseractions from osc_lib.command import command from osc_lib import utils +from openstackclient.common import sdk_utils from openstackclient.i18n import _ if os.name == "nt": @@ -47,6 +47,36 @@ DISK_CHOICES = ["ami", "ari", "aki", "vhd", "vmdk", "raw", "qcow2", "vhdx", LOG = logging.getLogger(__name__) +def _get_columns(item): + # Trick sdk_utils to return URI attribute + column_map = { + 'is_protected': 'protected', + 'owner_id': 'owner' + } + hidden_columns = ['location', 'checksum', + 'copy_from', 'created_at', 'status', 'updated_at'] + return sdk_utils.get_osc_show_columns_for_sdk_resource( + item.to_dict(), column_map, hidden_columns) + + +_formatters = { +} + + +class HumanReadableSizeColumn(cliff_columns.FormattableColumn): + def human_readable(self): + """Return a formatted visibility string + + :rtype: + A string formatted to public/private + """ + + if self._value: + return utils.format_size(self._value) + else: + return '' + + class VisibilityColumn(cliff_columns.FormattableColumn): def human_readable(self): """Return a formatted visibility string @@ -210,7 +240,7 @@ class CreateImage(command.ShowOne): # Special case project option back to API attribute name 'owner' val = getattr(parsed_args, 'project', None) if val: - kwargs['owner'] = val + kwargs['owner_id'] = val # Handle exclusive booleans with care # Avoid including attributes in kwargs if an option is not @@ -219,9 +249,9 @@ class CreateImage(command.ShowOne): # to do nothing when no options are present as opposed to always # setting a default. if parsed_args.protected: - kwargs['protected'] = True + kwargs['is_protected'] = True if parsed_args.unprotected: - kwargs['protected'] = False + kwargs['is_protected'] = False if parsed_args.public: kwargs['is_public'] = True if parsed_args.private: @@ -250,27 +280,35 @@ class CreateImage(command.ShowOne): kwargs["data"] = io.open(parsed_args.file, "rb") else: # Read file from stdin - if sys.stdin.isatty() is not True: + if not sys.stdin.isatty(): if msvcrt: msvcrt.setmode(sys.stdin.fileno(), os.O_BINARY) - # Send an open file handle to glanceclient so it will - # do a chunked transfer - kwargs["data"] = sys.stdin + if hasattr(sys.stdin, 'buffer'): + kwargs['data'] = sys.stdin.buffer + else: + kwargs["data"] = sys.stdin if not parsed_args.volume: # Wrap the call to catch exceptions in order to close files try: - image = image_client.images.create(**kwargs) + image = image_client.create_image(**kwargs) finally: # Clean up open files - make sure data isn't a string if ('data' in kwargs and hasattr(kwargs['data'], 'close') and kwargs['data'] != sys.stdin): kwargs['data'].close() + if image: + display_columns, columns = _get_columns(image) + _formatters['properties'] = format_columns.DictColumn + data = utils.get_item_properties(image, columns, + formatters=_formatters) + return (display_columns, data) + elif info: info.update(image._info) info['properties'] = format_columns.DictColumn( info.get('properties', {})) - return zip(*sorted(info.items())) + return zip(*sorted(info.items())) class DeleteImage(command.Command): @@ -289,11 +327,8 @@ class DeleteImage(command.Command): def take_action(self, parsed_args): image_client = self.app.client_manager.image for image in parsed_args.images: - image_obj = utils.find_resource( - image_client.images, - image, - ) - image_client.images.delete(image_obj.id) + image_obj = image_client.find_image(image) + image_client.delete_image(image_obj.id) class ListImage(command.Lister): @@ -359,15 +394,9 @@ class ListImage(command.Lister): kwargs = {} if parsed_args.public: - kwargs['public'] = True + kwargs['is_public'] = True if parsed_args.private: - kwargs['private'] = True - # Note: We specifically need to do that below to get the 'status' - # column. - # - # Always set kwargs['detailed'] to True, and then filter the columns - # according to whether the --long option is specified or not. - kwargs['detailed'] = True + kwargs['is_private'] = True if parsed_args.long: columns = ( @@ -379,8 +408,8 @@ class ListImage(command.Lister): 'Checksum', 'Status', 'is_public', - 'protected', - 'owner', + 'is_protected', + 'owner_id', 'properties', ) column_headers = ( @@ -401,16 +430,7 @@ class ListImage(command.Lister): column_headers = columns # List of image data received - data = [] - # 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'] + data = list(image_client.images(**kwargs)) if parsed_args.property: # NOTE(dtroyer): coerce to a list to subscript it in py3 @@ -426,7 +446,7 @@ class ListImage(command.Lister): return ( column_headers, - (utils.get_dict_properties( + (utils.get_item_properties( s, columns, formatters={ @@ -456,13 +476,9 @@ class SaveImage(command.Command): def take_action(self, parsed_args): image_client = self.app.client_manager.image - image = utils.find_resource( - image_client.images, - parsed_args.image, - ) - data = image_client.images.data(image) + image = image_client.find_image(parsed_args.image) - gc_utils.save_image(data, parsed_args.file) + image_client.download_image(image.id, output=parsed_args.file) class SetImage(command.Command): @@ -621,22 +637,17 @@ class SetImage(command.Command): # to do nothing when no options are present as opposed to always # setting a default. if parsed_args.protected: - kwargs['protected'] = True + kwargs['is_protected'] = True if parsed_args.unprotected: - kwargs['protected'] = False + kwargs['is_protected'] = False if parsed_args.public: kwargs['is_public'] = True if parsed_args.private: kwargs['is_public'] = False - if parsed_args.force: - kwargs['force'] = True # Wrap the call to catch exceptions in order to close files try: - image = utils.find_resource( - image_client.images, - parsed_args.image, - ) + image = image_client.find_image(parsed_args.image) if not parsed_args.location and not parsed_args.copy_from: if parsed_args.volume: @@ -666,9 +677,10 @@ class SetImage(command.Command): if parsed_args.stdin: if msvcrt: msvcrt.setmode(sys.stdin.fileno(), os.O_BINARY) - # Send an open file handle to glanceclient so it - # will do a chunked transfer - kwargs["data"] = sys.stdin + if hasattr(sys.stdin, 'buffer'): + kwargs['data'] = sys.stdin.buffer + else: + kwargs["data"] = sys.stdin else: LOG.warning(_('Use --stdin to enable read image ' 'data from standard input')) @@ -677,7 +689,7 @@ class SetImage(command.Command): image.properties.update(kwargs['properties']) kwargs['properties'] = image.properties - image = image_client.images.update(image.id, **kwargs) + image = image_client.update_image(image.id, **kwargs) finally: # Clean up open files - make sure data isn't a string if ('data' in kwargs and hasattr(kwargs['data'], 'close') and @@ -705,16 +717,12 @@ class ShowImage(command.ShowOne): def take_action(self, parsed_args): image_client = self.app.client_manager.image - image = utils.find_resource( - image_client.images, - parsed_args.image, - ) + image = image_client.find_image(parsed_args.image) - info = {} - info.update(image._info) if parsed_args.human_readable: - if 'size' in info: - info['size'] = utils.format_size(info['size']) - info['properties'] = format_columns.DictColumn( - info.get('properties', {})) - return zip(*sorted(info.items())) + _formatters['size'] = HumanReadableSizeColumn + display_columns, columns = _get_columns(image) + _formatters['properties'] = format_columns.DictColumn + data = utils.get_item_properties(image, columns, + formatters=_formatters) + return (display_columns, data) diff --git a/openstackclient/image/v2/image.py b/openstackclient/image/v2/image.py index feeb2567..53ce560d 100644 --- a/openstackclient/image/v2/image.py +++ b/openstackclient/image/v2/image.py @@ -18,8 +18,10 @@ import argparse from base64 import b64encode import logging +import os +import sys -from glanceclient.common import utils as gc_utils +import openstack.cloud._utils from openstack.image import image_signer from osc_lib.api import utils as api_utils from osc_lib.cli import format_columns @@ -27,11 +29,16 @@ 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.common import sdk_utils from openstackclient.i18n import _ from openstackclient.identity import common +if os.name == "nt": + import msvcrt +else: + msvcrt = None + CONTAINER_CHOICES = ["ami", "ari", "aki", "bare", "docker", "ova", "ovf"] DEFAULT_CONTAINER_FORMAT = 'bare' @@ -44,7 +51,7 @@ MEMBER_STATUS_CHOICES = ["accepted", "pending", "rejected", "all"] LOG = logging.getLogger(__name__) -def _format_image(image): +def _format_image(image, human_readable=False): """Format an image to make it more consistent with OSC operations.""" info = {} @@ -56,15 +63,25 @@ def _format_image(image): 'min_disk', 'protected', 'id', 'file', 'checksum', 'owner', 'virtual_size', 'min_ram', 'schema'] + # TODO(gtema/anybody): actually it should be possible to drop this method, + # since SDK already delivers a proper object + image = image.to_dict(ignore_none=True, original_names=True) + # split out the usual key and the properties which are top-level - for key in six.iterkeys(image): + for key in image: if key in fields_to_show: info[key] = image.get(key) elif key == 'tags': continue # handle this later - else: + elif key == 'properties': + # NOTE(gtema): flatten content of properties + properties.update(image.get(key)) + elif key != 'location': properties[key] = image.get(key) + if human_readable: + info['size'] = utils.format_size(image['size']) + # format the tags if they are there info['tags'] = format_columns.ListColumn(image.get('tags')) @@ -75,6 +92,51 @@ def _format_image(image): return info +_formatters = { + 'tags': format_columns.ListColumn, +} + + +def _get_member_columns(item): + # Trick sdk_utils to return URI attribute + column_map = { + 'image_id': 'image_id' + } + hidden_columns = ['id', 'location', 'name'] + return sdk_utils.get_osc_show_columns_for_sdk_resource( + item.to_dict(), column_map, hidden_columns) + + +def get_data_file(args): + if args.file: + return (open(args.file, 'rb'), args.file) + else: + # distinguish cases where: + # (1) stdin is not valid (as in cron jobs): + # openstack ... <&- + # (2) image data is provided through stdin: + # openstack ... < /tmp/file + # (3) no image data provided + # openstack ... + try: + os.fstat(0) + except OSError: + # (1) stdin is not valid + return (None, None) + if not sys.stdin.isatty(): + # (2) image data is provided through stdin + image = sys.stdin + if hasattr(sys.stdin, 'buffer'): + image = sys.stdin.buffer + if msvcrt: + msvcrt.setmode(sys.stdin.fileno(), os.O_BINARY) + + return (image, None) + else: + # (3) + return (None, None) + + class AddProjectToImage(command.ShowOne): _description = _("Associate project with image") @@ -97,20 +159,26 @@ class AddProjectToImage(command.ShowOne): image_client = self.app.client_manager.image identity_client = self.app.client_manager.identity - project_id = common.find_project(identity_client, - parsed_args.project, - parsed_args.project_domain).id + if openstack.cloud._utils._is_uuid_like(parsed_args.project): + project_id = parsed_args.project + else: + project_id = common.find_project( + identity_client, + parsed_args.project, + parsed_args.project_domain).id - image_id = utils.find_resource( - image_client.images, - parsed_args.image).id + image = image_client.find_image(parsed_args.image, + ignore_missing=False) - image_member = image_client.image_members.create( - image_id, - project_id, + obj = image_client.add_member( + image=image.id, + member_id=project_id, ) - return zip(*sorted(image_member.items())) + display_columns, columns = _get_member_columns(obj) + data = utils.get_item_properties(obj, columns, formatters={}) + + return (display_columns, data) class CreateImage(command.ShowOne): @@ -302,9 +370,9 @@ class CreateImage(command.ShowOne): # to do nothing when no options are present as opposed to always # setting a default. if parsed_args.protected: - kwargs['protected'] = True + kwargs['is_protected'] = True if parsed_args.unprotected: - kwargs['protected'] = False + kwargs['is_protected'] = False if parsed_args.public: kwargs['visibility'] = 'public' if parsed_args.private: @@ -314,24 +382,30 @@ class CreateImage(command.ShowOne): if parsed_args.shared: kwargs['visibility'] = 'shared' if parsed_args.project: - kwargs['owner'] = common.find_project( + kwargs['owner_id'] = common.find_project( identity_client, parsed_args.project, parsed_args.project_domain, ).id # open the file first to ensure any failures are handled before the - # image is created - fp = gc_utils.get_data_file(parsed_args) + # image is created. Get the file name (if it is file, and not stdin) + # for easier further handling. + (fp, fname) = get_data_file(parsed_args) info = {} + if fp is not None and parsed_args.volume: raise exceptions.CommandError(_("Uploading data and using " "container are not allowed at " "the same time")) - if fp is None and parsed_args.file: LOG.warning(_("Failed to get an image file.")) return {}, {} + elif fname: + kwargs['filename'] = fname + elif fp: + kwargs['validate_checksum'] = False + kwargs['data'] = fp # sign an image using a given local private key file if parsed_args.sign_key_path or parsed_args.sign_cert_id: @@ -361,8 +435,8 @@ class CreateImage(command.ShowOne): sign_key_path, password=pw) except Exception: - msg = (_("Error during sign operation: private key could " - "not be loaded.")) + msg = (_("Error during sign operation: private key " + "could not be loaded.")) raise exceptions.CommandError(msg) signature = signer.generate_signature(fp) @@ -371,7 +445,8 @@ class CreateImage(command.ShowOne): kwargs['img_signature_certificate_uuid'] = sign_cert_id kwargs['img_signature_hash_method'] = signer.hash_method if signer.padding_method: - kwargs['img_signature_key_type'] = signer.padding_method + kwargs['img_signature_key_type'] = \ + signer.padding_method # If a volume is specified. if parsed_args.volume: @@ -393,26 +468,7 @@ class CreateImage(command.ShowOne): except TypeError: info['volume_type'] = None else: - image = image_client.images.create(**kwargs) - - if fp is not None: - with fp: - try: - image_client.images.upload(image.id, fp) - except Exception: - # If the upload fails for some reason attempt to remove the - # dangling queued image made by the create() call above but - # only if the user did not specify an id which indicates - # the Image already exists and should be left alone. - try: - if 'id' not in kwargs: - image_client.images.delete(image.id) - except Exception: - pass # we don't care about this one - raise # now, throw the upload exception again - - # update the image after the data has been uploaded - image = image_client.images.get(image.id) + image = image_client.create_image(**kwargs) if not info: info = _format_image(image) @@ -439,11 +495,9 @@ class DeleteImage(command.Command): image_client = self.app.client_manager.image for image in parsed_args.images: try: - image_obj = utils.find_resource( - image_client.images, - image, - ) - image_client.images.delete(image_obj.id) + image_obj = image_client.find_image(image, + ignore_missing=False) + image_client.delete_image(image_obj.id) except Exception as e: del_result += 1 LOG.error(_("Failed to delete image with name or " @@ -569,18 +623,17 @@ class ListImage(command.Lister): kwargs = {} if parsed_args.public: - kwargs['public'] = True + kwargs['visibility'] = 'public' if parsed_args.private: - kwargs['private'] = True + kwargs['visibility'] = 'private' if parsed_args.community: - kwargs['community'] = True + kwargs['visibility'] = 'community' if parsed_args.shared: - kwargs['shared'] = True + kwargs['visibility'] = 'shared' if parsed_args.limit: kwargs['limit'] = parsed_args.limit if parsed_args.marker: - kwargs['marker'] = utils.find_resource(image_client.images, - parsed_args.marker).id + kwargs['marker'] = image_client.find_image(parsed_args.marker).id if parsed_args.name: kwargs['name'] = parsed_args.name if parsed_args.status: @@ -599,8 +652,8 @@ class ListImage(command.Lister): 'Checksum', 'Status', 'visibility', - 'protected', - 'owner', + 'is_protected', + 'owner_id', 'tags', ) column_headers = ( @@ -621,24 +674,10 @@ class ListImage(command.Lister): column_headers = columns # List of image data received - data = [] - limit = None if 'limit' in kwargs: - limit = kwargs['limit'] - 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 limit: - break + # Disable automatic pagination in SDK + kwargs['paginated'] = False + data = list(image_client.images(**kwargs)) if parsed_args.property: for attr, value in parsed_args.property.items(): @@ -653,12 +692,10 @@ class ListImage(command.Lister): return ( column_headers, - (utils.get_dict_properties( + (utils.get_item_properties( s, columns, - formatters={ - 'tags': format_columns.ListColumn, - }, + formatters=_formatters, ) for s in data) ) @@ -684,11 +721,9 @@ class ListImageProjects(command.Lister): "Status" ) - image_id = utils.find_resource( - image_client.images, - parsed_args.image).id + image_id = image_client.find_image(parsed_args.image).id - data = image_client.image_members.list(image_id) + data = image_client.members(image=image_id) return (columns, (utils.get_item_properties( @@ -722,11 +757,12 @@ class RemoveProjectImage(command.Command): parsed_args.project, parsed_args.project_domain).id - image_id = utils.find_resource( - image_client.images, - parsed_args.image).id + image = image_client.find_image(parsed_args.image, + ignore_missing=False) - image_client.image_members.delete(image_id, project_id) + image_client.remove_member( + member=project_id, + image=image.id) class SaveImage(command.Command): @@ -748,19 +784,9 @@ class SaveImage(command.Command): def take_action(self, parsed_args): image_client = self.app.client_manager.image - image = utils.find_resource( - image_client.images, - parsed_args.image, - ) - data = image_client.images.data(image.id) + image = image_client.find_image(parsed_args.image) - if data.wrapped is None: - msg = _('Image %s has no data.') % image.id - LOG.error(msg) - self.app.stdout.write(msg + '\n') - raise SystemExit - - gc_utils.save_image(data, parsed_args.file) + image_client.download_image(image.id, output=parsed_args.file) class SetImage(command.Command): @@ -979,9 +1005,9 @@ class SetImage(command.Command): # to do nothing when no options are present as opposed to always # setting a default. if parsed_args.protected: - kwargs['protected'] = True + kwargs['is_protected'] = True if parsed_args.unprotected: - kwargs['protected'] = False + kwargs['is_protected'] = False if parsed_args.public: kwargs['visibility'] = 'public' if parsed_args.private: @@ -997,17 +1023,20 @@ class SetImage(command.Command): parsed_args.project, parsed_args.project_domain, ).id - kwargs['owner'] = project_id + kwargs['owner_id'] = project_id + + image = image_client.find_image(parsed_args.image, + ignore_missing=False) - image = utils.find_resource( - image_client.images, parsed_args.image) + # image = utils.find_resource( + # image_client.images, parsed_args.image) activation_status = None if parsed_args.deactivate: - image_client.images.deactivate(image.id) + image_client.deactivate_image(image.id) activation_status = "deactivated" if parsed_args.activate: - image_client.images.reactivate(image.id) + image_client.reactivate_image(image.id) activation_status = "activated" membership_group_args = ('accept', 'reject', 'pending') @@ -1022,15 +1051,15 @@ class SetImage(command.Command): # most one item in the membership_status list. if membership_status[0] != 'pending': membership_status[0] += 'ed' # Glance expects the past form - image_client.image_members.update( - image.id, project_id, membership_status[0]) + image_client.update_member( + image=image.id, member=project_id, status=membership_status[0]) if parsed_args.tags: # Tags should be extended, but duplicates removed kwargs['tags'] = list(set(image.tags).union(set(parsed_args.tags))) try: - image = image_client.images.update(image.id, **kwargs) + image = image_client.update_image(image.id, **kwargs) except Exception: if activation_status is not None: LOG.info(_("Image %(id)s was %(status)s."), @@ -1058,14 +1087,11 @@ class ShowImage(command.ShowOne): def take_action(self, parsed_args): image_client = self.app.client_manager.image - image = utils.find_resource( - image_client.images, - parsed_args.image, - ) - if parsed_args.human_readable: - image['size'] = utils.format_size(image['size']) - info = _format_image(image) + image = image_client.find_image(parsed_args.image, + ignore_missing=False) + + info = _format_image(image, parsed_args.human_readable) return zip(*sorted(info.items())) @@ -1101,10 +1127,8 @@ class UnsetImage(command.Command): def take_action(self, parsed_args): image_client = self.app.client_manager.image - image = utils.find_resource( - image_client.images, - parsed_args.image, - ) + image = image_client.find_image(parsed_args.image, + ignore_missing=False) kwargs = {} tagret = 0 @@ -1112,7 +1136,7 @@ class UnsetImage(command.Command): if parsed_args.tags: for k in parsed_args.tags: try: - image_client.image_tags.delete(image.id, k) + image_client.remove_tag(image.id, k) except Exception: LOG.error(_("tag unset failed, '%s' is a " "nonexistent tag "), k) @@ -1120,13 +1144,26 @@ class UnsetImage(command.Command): if parsed_args.properties: for k in parsed_args.properties: - if k not in image: + if k in image: + kwargs[k] = None + elif k in image.properties: + # Since image is an "evil" object from SDK POV we need to + # pass modified properties object, so that SDK can figure + # out, what was changed inside + # NOTE: ping gtema to improve that in SDK + new_props = kwargs.get('properties', + image.get('properties').copy()) + new_props.pop(k, None) + kwargs['properties'] = new_props + else: LOG.error(_("property unset failed, '%s' is a " "nonexistent property "), k) propret += 1 - image_client.images.update( - image.id, - parsed_args.properties, + + # We must give to update a current image for the reference on what + # has changed + image_client.update_image( + image, **kwargs) tagtotal = len(parsed_args.tags) diff --git a/openstackclient/network/sdk_utils.py b/openstackclient/network/sdk_utils.py index af9c74f9..cff30713 100644 --- a/openstackclient/network/sdk_utils.py +++ b/openstackclient/network/sdk_utils.py @@ -10,6 +10,8 @@ # License for the specific language governing permissions and limitations # under the License. +import munch + def get_osc_show_columns_for_sdk_resource( sdk_resource, @@ -38,6 +40,9 @@ def get_osc_show_columns_for_sdk_resource( # Build the OSC column names to display for the SDK resource. attr_map = {} display_columns = list(resource_dict.keys()) + for col_name in display_columns: + if isinstance(resource_dict[col_name], munch.Munch): + display_columns.remove(col_name) invisible_columns = [] if invisible_columns is None else invisible_columns for col_name in invisible_columns: if col_name in display_columns: diff --git a/openstackclient/network/v2/address_scope.py b/openstackclient/network/v2/address_scope.py index 7efbb631..71c1a9af 100644 --- a/openstackclient/network/v2/address_scope.py +++ b/openstackclient/network/v2/address_scope.py @@ -15,7 +15,6 @@ import logging -from osc_lib.cli import format_columns from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils @@ -28,11 +27,6 @@ from openstackclient.network import sdk_utils LOG = logging.getLogger(__name__) -_formatters = { - 'location': format_columns.DictColumn, -} - - def _get_columns(item): column_map = { 'is_shared': 'shared', @@ -106,7 +100,7 @@ class CreateAddressScope(command.ShowOne): attrs = _get_attrs(self.app.client_manager, parsed_args) obj = client.create_address_scope(**attrs) display_columns, columns = _get_columns(obj) - data = utils.get_item_properties(obj, columns, formatters=_formatters) + data = utils.get_item_properties(obj, columns, formatters={}) return (display_columns, data) @@ -295,6 +289,6 @@ class ShowAddressScope(command.ShowOne): parsed_args.address_scope, ignore_missing=False) display_columns, columns = _get_columns(obj) - data = utils.get_item_properties(obj, columns, formatters=_formatters) + data = utils.get_item_properties(obj, columns, formatters={}) return (display_columns, data) diff --git a/openstackclient/network/v2/floating_ip.py b/openstackclient/network/v2/floating_ip.py index 4525913f..a2765cd1 100644 --- a/openstackclient/network/v2/floating_ip.py +++ b/openstackclient/network/v2/floating_ip.py @@ -13,7 +13,6 @@ """IP Floating action implementations""" -from osc_lib.cli import format_columns from osc_lib.command import command from osc_lib import utils from osc_lib.utils import tags as _tag @@ -25,7 +24,6 @@ from openstackclient.network import sdk_utils _formatters = { - 'location': format_columns.DictColumn, 'port_details': utils.format_dict, } @@ -412,6 +410,11 @@ class SetFloatingIP(command.Command): help=_("Fixed IP of the port " "(required only if port has multiple IPs)") ) + parser.add_argument( + '--description', + metavar='<description>', + help=_('Set floating IP description') + ) qos_policy_group = parser.add_mutually_exclusive_group() qos_policy_group.add_argument( '--qos-policy', @@ -443,6 +446,9 @@ class SetFloatingIP(command.Command): if parsed_args.fixed_ip_address: attrs['fixed_ip_address'] = parsed_args.fixed_ip_address + if parsed_args.description: + attrs['description'] = parsed_args.description + if parsed_args.qos_policy: attrs['qos_policy_id'] = client.find_qos_policy( parsed_args.qos_policy, ignore_missing=False).id diff --git a/openstackclient/network/v2/floating_ip_port_forwarding.py b/openstackclient/network/v2/floating_ip_port_forwarding.py index f94bcc06..06b3df8b 100644 --- a/openstackclient/network/v2/floating_ip_port_forwarding.py +++ b/openstackclient/network/v2/floating_ip_port_forwarding.py @@ -75,6 +75,12 @@ class CreateFloatingIPPortForwarding(command.ShowOne): required=True, help=_("The protocol used in the floating IP " "port forwarding, for instance: TCP, UDP") + ), + parser.add_argument( + '--description', + metavar='<description>', + help=_("A text to describe/contextualize the use of the " + "port forwarding configuration") ) parser.add_argument( 'floating_ip', @@ -113,6 +119,9 @@ class CreateFloatingIPPortForwarding(command.ShowOne): attrs['internal_ip_address'] = parsed_args.internal_ip_address attrs['protocol'] = parsed_args.protocol + if parsed_args.description is not None: + attrs['description'] = parsed_args.description + obj = client.create_floating_ip_port_forwarding( floating_ip.id, **attrs @@ -212,6 +221,7 @@ class ListFloatingIPPortForwarding(command.Lister): 'internal_port', 'external_port', 'protocol', + 'description', ) headers = ( 'ID', @@ -220,6 +230,7 @@ class ListFloatingIPPortForwarding(command.Lister): 'Internal Port', 'External Port', 'Protocol', + 'Description', ) query = {} @@ -296,6 +307,12 @@ class SetFloatingIPPortForwarding(command.Command): metavar='<protocol>', choices=['tcp', 'udp'], help=_("The IP protocol used in the floating IP port forwarding") + ), + parser.add_argument( + '--description', + metavar='<description>', + help=_("A text to describe/contextualize the use of " + "the port forwarding configuration") ) return parser @@ -332,6 +349,9 @@ class SetFloatingIPPortForwarding(command.Command): if parsed_args.protocol: attrs['protocol'] = parsed_args.protocol + if parsed_args.description is not None: + attrs['description'] = parsed_args.description + client.update_floating_ip_port_forwarding( floating_ip.id, parsed_args.port_forwarding_id, **attrs) diff --git a/openstackclient/network/v2/ip_availability.py b/openstackclient/network/v2/ip_availability.py index c026baa0..ddc88e55 100644 --- a/openstackclient/network/v2/ip_availability.py +++ b/openstackclient/network/v2/ip_availability.py @@ -21,9 +21,7 @@ from openstackclient.i18n import _ from openstackclient.identity import common as identity_common from openstackclient.network import sdk_utils - _formatters = { - 'location': format_columns.DictColumn, 'subnet_ip_availability': format_columns.ListDictColumn, } diff --git a/openstackclient/network/v2/network.py b/openstackclient/network/v2/network.py index 3f579b6d..7a12d523 100644 --- a/openstackclient/network/v2/network.py +++ b/openstackclient/network/v2/network.py @@ -16,7 +16,6 @@ from cliff import columns as cliff_columns from osc_lib.cli import format_columns from osc_lib.command import command -from osc_lib import exceptions from osc_lib import utils from osc_lib.utils import tags as _tag @@ -41,7 +40,6 @@ _formatters = { 'subnet_ids': format_columns.ListColumn, 'admin_state_up': AdminStateColumn, 'is_admin_state_up': AdminStateColumn, - 'location': format_columns.DictColumn, 'router:external': RouterExternalColumn, 'is_router_external': RouterExternalColumn, 'availability_zones': format_columns.ListColumn, @@ -126,9 +124,6 @@ def _get_attrs_network(client_manager, parsed_args): attrs['is_default'] = False if parsed_args.default: attrs['is_default'] = True - if attrs.get('is_default') and not attrs.get('router:external'): - msg = _("Cannot set default for internal network") - raise exceptions.CommandError(msg) # Update Provider network options if parsed_args.provider_network_type: attrs['provider:network_type'] = parsed_args.provider_network_type @@ -706,8 +701,7 @@ class SetNetwork(command.Command): default_router_grp.add_argument( '--default', action='store_true', - help=_("Set the network as the default external network " - "(cannot be used with internal network).") + help=_("Set the network as the default external network") ) default_router_grp.add_argument( '--no-default', diff --git a/openstackclient/network/v2/network_agent.py b/openstackclient/network/v2/network_agent.py index a6ed3629..16784854 100644 --- a/openstackclient/network/v2/network_agent.py +++ b/openstackclient/network/v2/network_agent.py @@ -43,7 +43,6 @@ _formatters = { 'alive': AliveColumn, 'admin_state_up': AdminStateColumn, 'is_admin_state_up': AdminStateColumn, - 'location': format_columns.DictColumn, 'configurations': format_columns.DictColumn, } diff --git a/openstackclient/network/v2/network_auto_allocated_topology.py b/openstackclient/network/v2/network_auto_allocated_topology.py index f6070a02..36f39200 100644 --- a/openstackclient/network/v2/network_auto_allocated_topology.py +++ b/openstackclient/network/v2/network_auto_allocated_topology.py @@ -15,7 +15,6 @@ import logging -from osc_lib.cli import format_columns from osc_lib.command import command from osc_lib import utils @@ -26,11 +25,6 @@ from openstackclient.network import sdk_utils LOG = logging.getLogger(__name__) -_formatters = { - 'location': format_columns.DictColumn, -} - - def _get_columns(item): column_map = { 'tenant_id': 'project_id', @@ -99,17 +93,16 @@ class CreateAutoAllocatedTopology(command.ShowOne): obj = client.validate_auto_allocated_topology(parsed_args.project) columns = _format_check_resource_columns() - data = utils.get_item_properties( - _format_check_resource(obj), - columns, - formatters=_formatters, - ) + data = utils.get_item_properties(_format_check_resource(obj), + columns, + formatters={}) + return (columns, data) def get_topology(self, client, parsed_args): obj = client.get_auto_allocated_topology(parsed_args.project) display_columns, columns = _get_columns(obj) - data = utils.get_item_properties(obj, columns, formatters=_formatters) + data = utils.get_item_properties(obj, columns, formatters={}) return (display_columns, data) def take_action(self, parsed_args): diff --git a/openstackclient/network/v2/network_flavor.py b/openstackclient/network/v2/network_flavor.py index 355d04c1..c9d368bf 100644 --- a/openstackclient/network/v2/network_flavor.py +++ b/openstackclient/network/v2/network_flavor.py @@ -15,7 +15,6 @@ import logging -from osc_lib.cli import format_columns from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils @@ -28,11 +27,6 @@ from openstackclient.network import sdk_utils LOG = logging.getLogger(__name__) -_formatters = { - 'location': format_columns.DictColumn, -} - - def _get_columns(item): column_map = { 'is_enabled': 'enabled', @@ -142,7 +136,7 @@ class CreateNetworkFlavor(command.ShowOne): attrs = _get_attrs(self.app.client_manager, parsed_args) obj = client.create_flavor(**attrs) display_columns, columns = _get_columns(obj) - data = utils.get_item_properties(obj, columns, formatters=_formatters) + data = utils.get_item_properties(obj, columns, formatters={}) return (display_columns, data) @@ -306,5 +300,5 @@ class ShowNetworkFlavor(command.ShowOne): client = self.app.client_manager.network obj = client.find_flavor(parsed_args.flavor, ignore_missing=False) display_columns, columns = _get_columns(obj) - data = utils.get_item_properties(obj, columns, formatters=_formatters) + data = utils.get_item_properties(obj, columns) return display_columns, data diff --git a/openstackclient/network/v2/network_flavor_profile.py b/openstackclient/network/v2/network_flavor_profile.py index 492fd432..6cf0c412 100644 --- a/openstackclient/network/v2/network_flavor_profile.py +++ b/openstackclient/network/v2/network_flavor_profile.py @@ -13,7 +13,6 @@ import logging -from osc_lib.cli import format_columns from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils @@ -26,11 +25,6 @@ from openstackclient.network import sdk_utils LOG = logging.getLogger(__name__) -_formatters = { - 'location': format_columns.DictColumn, -} - - def _get_columns(item): column_map = { 'is_enabled': 'enabled', @@ -116,7 +110,7 @@ class CreateNetworkFlavorProfile(command.ShowOne): obj = client.create_service_profile(**attrs) display_columns, columns = _get_columns(obj) - data = utils.get_item_properties(obj, columns, formatters=_formatters) + data = utils.get_item_properties(obj, columns, formatters={}) return (display_columns, data) @@ -252,5 +246,5 @@ class ShowNetworkFlavorProfile(command.ShowOne): obj = client.find_service_profile(parsed_args.flavor_profile, ignore_missing=False) display_columns, columns = _get_columns(obj) - data = utils.get_item_properties(obj, columns, formatters=_formatters) + data = utils.get_item_properties(obj, columns) return (display_columns, data) diff --git a/openstackclient/network/v2/network_meter.py b/openstackclient/network/v2/network_meter.py index cde7a304..df0e1da1 100644 --- a/openstackclient/network/v2/network_meter.py +++ b/openstackclient/network/v2/network_meter.py @@ -15,7 +15,6 @@ import logging -from osc_lib.cli import format_columns from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils @@ -27,11 +26,6 @@ from openstackclient.network import sdk_utils LOG = logging.getLogger(__name__) -_formatters = { - 'location': format_columns.DictColumn, -} - - def _get_columns(item): column_map = { 'is_shared': 'shared', @@ -108,7 +102,7 @@ class CreateMeter(command.ShowOne): attrs = _get_attrs(self.app.client_manager, parsed_args) obj = client.create_metering_label(**attrs) display_columns, columns = _get_columns(obj) - data = utils.get_item_properties(obj, columns, formatters=_formatters) + data = utils.get_item_properties(obj, columns, formatters={}) return (display_columns, data) @@ -192,5 +186,5 @@ class ShowMeter(command.ShowOne): obj = client.find_metering_label(parsed_args.meter, ignore_missing=False) display_columns, columns = _get_columns(obj) - data = utils.get_item_properties(obj, columns, formatters=_formatters) + data = utils.get_item_properties(obj, columns) return display_columns, data diff --git a/openstackclient/network/v2/network_meter_rule.py b/openstackclient/network/v2/network_meter_rule.py index 5f31255a..49ff9e1b 100644 --- a/openstackclient/network/v2/network_meter_rule.py +++ b/openstackclient/network/v2/network_meter_rule.py @@ -15,7 +15,6 @@ import logging -from osc_lib.cli import format_columns from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils @@ -27,11 +26,6 @@ from openstackclient.network import sdk_utils LOG = logging.getLogger(__name__) -_formatters = { - 'location': format_columns.DictColumn, -} - - def _get_columns(item): column_map = { 'tenant_id': 'project_id', @@ -122,7 +116,7 @@ class CreateMeterRule(command.ShowOne): attrs = _get_attrs(self.app.client_manager, parsed_args) obj = client.create_metering_label_rule(**attrs) display_columns, columns = _get_columns(obj) - data = utils.get_item_properties(obj, columns, formatters=_formatters) + data = utils.get_item_properties(obj, columns, formatters={}) return (display_columns, data) @@ -205,5 +199,5 @@ class ShowMeterRule(command.ShowOne): obj = client.find_metering_label_rule(parsed_args.meter_rule_id, ignore_missing=False) display_columns, columns = _get_columns(obj) - data = utils.get_item_properties(obj, columns, formatters=_formatters) + data = utils.get_item_properties(obj, columns) return display_columns, data diff --git a/openstackclient/network/v2/network_qos_policy.py b/openstackclient/network/v2/network_qos_policy.py index 1622de4a..fd5ff937 100644 --- a/openstackclient/network/v2/network_qos_policy.py +++ b/openstackclient/network/v2/network_qos_policy.py @@ -15,7 +15,6 @@ import logging -from osc_lib.cli import format_columns from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils @@ -28,11 +27,6 @@ from openstackclient.network import sdk_utils LOG = logging.getLogger(__name__) -_formatters = { - 'location': format_columns.DictColumn, -} - - def _get_columns(item): column_map = { 'is_shared': 'shared', @@ -125,7 +119,7 @@ class CreateNetworkQosPolicy(command.ShowOne): attrs = _get_attrs(self.app.client_manager, parsed_args) obj = client.create_qos_policy(**attrs) display_columns, columns = _get_columns(obj) - data = utils.get_item_properties(obj, columns, formatters=_formatters) + data = utils.get_item_properties(obj, columns, formatters={}) return (display_columns, data) @@ -285,5 +279,5 @@ class ShowNetworkQosPolicy(command.ShowOne): obj = client.find_qos_policy(parsed_args.policy, ignore_missing=False) display_columns, columns = _get_columns(obj) - data = utils.get_item_properties(obj, columns, formatters=_formatters) + data = utils.get_item_properties(obj, columns) return (display_columns, data) diff --git a/openstackclient/network/v2/network_qos_rule.py b/openstackclient/network/v2/network_qos_rule.py index d74beda7..28c5600a 100644 --- a/openstackclient/network/v2/network_qos_rule.py +++ b/openstackclient/network/v2/network_qos_rule.py @@ -15,7 +15,6 @@ import itertools -from osc_lib.cli import format_columns from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils @@ -47,11 +46,6 @@ ACTION_SET = 'update' ACTION_SHOW = 'get' -_formatters = { - 'location': format_columns.DictColumn, -} - - def _get_columns(item): column_map = { 'tenant_id': 'project_id', @@ -214,7 +208,7 @@ class CreateNetworkQosRule(command.ShowOne): msg = (_('Failed to create Network QoS rule: %(e)s') % {'e': e}) raise exceptions.CommandError(msg) display_columns, columns = _get_columns(obj) - data = utils.get_item_properties(obj, columns, formatters=_formatters) + data = utils.get_item_properties(obj, columns) return display_columns, data @@ -364,5 +358,5 @@ class ShowNetworkQosRule(command.ShowOne): {'rule': rule_id, 'e': e}) raise exceptions.CommandError(msg) display_columns, columns = _get_columns(obj) - data = utils.get_item_properties(obj, columns, formatters=_formatters) + data = utils.get_item_properties(obj, columns) return display_columns, data diff --git a/openstackclient/network/v2/network_qos_rule_type.py b/openstackclient/network/v2/network_qos_rule_type.py index e842944c..7b92c8ad 100644 --- a/openstackclient/network/v2/network_qos_rule_type.py +++ b/openstackclient/network/v2/network_qos_rule_type.py @@ -13,7 +13,6 @@ # License for the specific language governing permissions and limitations # under the License. -from osc_lib.cli import format_columns from osc_lib.command import command from osc_lib import utils @@ -21,11 +20,6 @@ from openstackclient.i18n import _ from openstackclient.network import sdk_utils -_formatters = { - 'location': format_columns.DictColumn, -} - - def _get_columns(item): column_map = { "type": "rule_type_name", @@ -71,5 +65,5 @@ class ShowNetworkQosRuleType(command.ShowOne): client = self.app.client_manager.network obj = client.get_qos_rule_type(parsed_args.rule_type) display_columns, columns = _get_columns(obj) - data = utils.get_item_properties(obj, columns, formatters=_formatters) + data = utils.get_item_properties(obj, columns) return display_columns, data diff --git a/openstackclient/network/v2/network_rbac.py b/openstackclient/network/v2/network_rbac.py index 1781193f..b88ef019 100644 --- a/openstackclient/network/v2/network_rbac.py +++ b/openstackclient/network/v2/network_rbac.py @@ -15,7 +15,6 @@ import logging -from osc_lib.cli import format_columns from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils @@ -28,11 +27,6 @@ from openstackclient.network import sdk_utils LOG = logging.getLogger(__name__) -_formatters = { - 'location': format_columns.DictColumn, -} - - def _get_columns(item): column_map = { 'target_tenant': 'target_project_id', @@ -58,6 +52,15 @@ def _get_attrs(client_manager, parsed_args): object_id = network_client.find_security_group( parsed_args.rbac_object, ignore_missing=False).id + if parsed_args.type == 'address_scope': + object_id = network_client.find_address_scope( + parsed_args.rbac_object, + ignore_missing=False).id + if parsed_args.type == 'subnetpool': + object_id = network_client.find_subnet_pool( + parsed_args.rbac_object, + ignore_missing=False).id + attrs['object_id'] = object_id identity_client = client_manager.identity @@ -97,9 +100,11 @@ class CreateNetworkRBAC(command.ShowOne): '--type', metavar="<type>", required=True, - choices=['security_group', 'qos_policy', 'network'], + choices=['address_scope', 'security_group', 'subnetpool', + 'qos_policy', 'network'], help=_('Type of the object that RBAC policy ' - 'affects ("security_group", "qos_policy" or "network")') + 'affects ("address_scope", "security_group", "subnetpool",' + ' "qos_policy" or "network")') ) parser.add_argument( '--action', @@ -142,7 +147,7 @@ class CreateNetworkRBAC(command.ShowOne): attrs = _get_attrs(self.app.client_manager, parsed_args) obj = client.create_rbac_policy(**attrs) display_columns, columns = _get_columns(obj) - data = utils.get_item_properties(obj, columns, formatters=_formatters) + data = utils.get_item_properties(obj, columns) return display_columns, data @@ -188,10 +193,11 @@ class ListNetworkRBAC(command.Lister): parser.add_argument( '--type', metavar='<type>', - choices=['security_group', 'qos_policy', 'network'], + choices=['address_scope', 'security_group', 'subnetpool', + 'qos_policy', 'network'], help=_('List network RBAC policies according to ' - 'given object type ("security_group", "qos_policy" ' - 'or "network")') + 'given object type ("address_scope", "security_group", ' + '"subnetpool", "qos_policy" or "network")') ) parser.add_argument( '--action', @@ -299,5 +305,5 @@ class ShowNetworkRBAC(command.ShowOne): obj = client.find_rbac_policy(parsed_args.rbac_policy, ignore_missing=False) display_columns, columns = _get_columns(obj) - data = utils.get_item_properties(obj, columns, formatters=_formatters) + data = utils.get_item_properties(obj, columns) return display_columns, data diff --git a/openstackclient/network/v2/network_segment.py b/openstackclient/network/v2/network_segment.py index 5899dc69..c1a672e2 100644 --- a/openstackclient/network/v2/network_segment.py +++ b/openstackclient/network/v2/network_segment.py @@ -15,7 +15,6 @@ import logging -from osc_lib.cli import format_columns from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils @@ -27,11 +26,6 @@ from openstackclient.network import sdk_utils LOG = logging.getLogger(__name__) -_formatters = { - 'location': format_columns.DictColumn, -} - - def _get_columns(item): return sdk_utils.get_osc_show_columns_for_sdk_resource(item, {}) @@ -96,7 +90,7 @@ class CreateNetworkSegment(command.ShowOne): attrs['segmentation_id'] = parsed_args.segment obj = client.create_segment(**attrs) display_columns, columns = _get_columns(obj) - data = utils.get_item_properties(obj, columns, formatters=_formatters) + data = utils.get_item_properties(obj, columns) return (display_columns, data) @@ -248,5 +242,5 @@ class ShowNetworkSegment(command.ShowOne): ignore_missing=False ) display_columns, columns = _get_columns(obj) - data = utils.get_item_properties(obj, columns, formatters=_formatters) + data = utils.get_item_properties(obj, columns) return (display_columns, data) diff --git a/openstackclient/network/v2/network_segment_range.py b/openstackclient/network/v2/network_segment_range.py index 2cdae642..6229995a 100644 --- a/openstackclient/network/v2/network_segment_range.py +++ b/openstackclient/network/v2/network_segment_range.py @@ -19,11 +19,9 @@ import itertools import logging -from osc_lib.cli import format_columns from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils -import six from openstackclient.i18n import _ from openstackclient.identity import common as identity_common @@ -32,17 +30,13 @@ from openstackclient.network import sdk_utils LOG = logging.getLogger(__name__) -_formatters = { - 'location': format_columns.DictColumn, -} - def _get_columns(item): return sdk_utils.get_osc_show_columns_for_sdk_resource(item, {}) def _get_ranges(item): - item = [int(i) if isinstance(i, six.string_types) else i for i in item] + item = sorted([int(i) for i in item]) for a, b in itertools.groupby(enumerate(item), lambda xy: xy[1] - xy[0]): b = list(b) yield "%s-%s" % (b[0][1], b[-1][1]) if b[0][1] != b[-1][1] else \ @@ -217,7 +211,7 @@ class CreateNetworkSegmentRange(command.ShowOne): attrs['physical_network'] = parsed_args.physical_network obj = network_client.create_network_segment_range(**attrs) display_columns, columns = _get_columns(obj) - data = utils.get_item_properties(obj, columns, formatters=_formatters) + data = utils.get_item_properties(obj, columns) data = _update_additional_fields_from_props(columns, props=data) return (display_columns, data) @@ -456,6 +450,6 @@ class ShowNetworkSegmentRange(command.ShowOne): ignore_missing=False ) display_columns, columns = _get_columns(obj) - data = utils.get_item_properties(obj, columns, formatters=_formatters) + data = utils.get_item_properties(obj, columns) data = _update_additional_fields_from_props(columns, props=data) return (display_columns, data) diff --git a/openstackclient/network/v2/port.py b/openstackclient/network/v2/port.py index a22bcafb..a21324ae 100644 --- a/openstackclient/network/v2/port.py +++ b/openstackclient/network/v2/port.py @@ -51,7 +51,6 @@ _formatters = { 'dns_assignment': format_columns.ListDictColumn, 'extra_dhcp_opts': format_columns.ListDictColumn, 'fixed_ips': format_columns.ListDictColumn, - 'location': format_columns.DictColumn, 'security_group_ids': format_columns.ListColumn, 'tags': format_columns.ListColumn, } @@ -516,6 +515,10 @@ class ListPort(command.Lister): "network:dhcp).") ) parser.add_argument( + '--host', + metavar='<host-id>', + help=_("List only ports bound to this host ID")) + parser.add_argument( '--network', metavar='<network>', help=_("List only ports connected to this network (name or ID)")) @@ -603,6 +606,8 @@ class ListPort(command.Lister): server = utils.find_resource(compute_client.servers, parsed_args.server) filters['device_id'] = server.id + if parsed_args.host: + filters['binding:host_id'] = parsed_args.host if parsed_args.network: network = network_client.find_network(parsed_args.network, ignore_missing=False) diff --git a/openstackclient/network/v2/router.py b/openstackclient/network/v2/router.py index 464dbbec..e3e8accd 100644 --- a/openstackclient/network/v2/router.py +++ b/openstackclient/network/v2/router.py @@ -61,7 +61,6 @@ _formatters = { 'external_gateway_info': RouterInfoColumn, 'availability_zones': format_columns.ListColumn, 'availability_zone_hints': format_columns.ListColumn, - 'location': format_columns.DictColumn, 'routes': RoutesColumn, 'tags': format_columns.ListColumn, } @@ -168,6 +167,93 @@ class AddSubnetToRouter(command.Command): subnet_id=subnet.id) +class AddExtraRoutesToRouter(command.ShowOne): + _description = _("Add extra static routes to a router's routing table.") + + def get_parser(self, prog_name): + parser = super(AddExtraRoutesToRouter, self).get_parser(prog_name) + parser.add_argument( + 'router', + metavar='<router>', + help=_("Router to which extra static routes " + "will be added (name or ID).") + ) + parser.add_argument( + '--route', + metavar='destination=<subnet>,gateway=<ip-address>', + action=parseractions.MultiKeyValueAction, + dest='routes', + default=[], + required_keys=['destination', 'gateway'], + help=_("Add extra static route to the router. " + "destination: destination subnet (in CIDR notation), " + "gateway: nexthop IP address. " + "Repeat option to add multiple routes. " + "Trying to add a route that's already present " + "(exactly, including destination and nexthop) " + "in the routing table is allowed and is considered " + "a successful operation.") + ) + return parser + + def take_action(self, parsed_args): + if parsed_args.routes is not None: + for route in parsed_args.routes: + route['nexthop'] = route.pop('gateway') + client = self.app.client_manager.network + router_obj = client.add_extra_routes_to_router( + client.find_router(parsed_args.router, ignore_missing=False), + body={'router': {'routes': parsed_args.routes}}) + display_columns, columns = _get_columns(router_obj) + data = utils.get_item_properties( + router_obj, columns, formatters=_formatters) + return (display_columns, data) + + +class RemoveExtraRoutesFromRouter(command.ShowOne): + _description = _( + "Remove extra static routes from a router's routing table.") + + def get_parser(self, prog_name): + parser = super(RemoveExtraRoutesFromRouter, self).get_parser(prog_name) + parser.add_argument( + 'router', + metavar='<router>', + help=_("Router from which extra static routes " + "will be removed (name or ID).") + ) + parser.add_argument( + '--route', + metavar='destination=<subnet>,gateway=<ip-address>', + action=parseractions.MultiKeyValueAction, + dest='routes', + default=[], + required_keys=['destination', 'gateway'], + help=_("Remove extra static route from the router. " + "destination: destination subnet (in CIDR notation), " + "gateway: nexthop IP address. " + "Repeat option to remove multiple routes. " + "Trying to remove a route that's already missing " + "(fully, including destination and nexthop) " + "from the routing table is allowed and is considered " + "a successful operation.") + ) + return parser + + def take_action(self, parsed_args): + if parsed_args.routes is not None: + for route in parsed_args.routes: + route['nexthop'] = route.pop('gateway') + client = self.app.client_manager.network + router_obj = client.remove_extra_routes_from_router( + client.find_router(parsed_args.router, ignore_missing=False), + body={'router': {'routes': parsed_args.routes}}) + display_columns, columns = _get_columns(router_obj) + data = utils.get_item_properties( + router_obj, columns, formatters=_formatters) + return (display_columns, data) + + # TODO(yanxing'an): Use the SDK resource mapped attribute names once the # OSC minimum requirements include SDK 1.0. class CreateRouter(command.ShowOne): @@ -540,17 +626,21 @@ class SetRouter(command.Command): dest='routes', default=None, required_keys=['destination', 'gateway'], - help=_("Routes associated with the router " + help=_("Add routes to the router " "destination: destination subnet (in CIDR notation) " "gateway: nexthop IP address " - "(repeat option to set multiple routes)") + "(repeat option to add multiple routes). " + "This is deprecated in favor of 'router add/remove route' " + "since it is prone to race conditions between concurrent " + "clients when not used together with --no-route to " + "overwrite the current value of 'routes'.") ) parser.add_argument( '--no-route', action='store_true', help=_("Clear routes associated with the router. " "Specify both --route and --no-route to overwrite " - "current value of route.") + "current value of routes.") ) routes_ha = parser.add_mutually_exclusive_group() routes_ha.add_argument( diff --git a/openstackclient/network/v2/security_group.py b/openstackclient/network/v2/security_group.py index f8153fa8..0732c23e 100644 --- a/openstackclient/network/v2/security_group.py +++ b/openstackclient/network/v2/security_group.py @@ -16,7 +16,6 @@ import argparse from cliff import columns as cliff_columns -from osc_lib.cli import format_columns from osc_lib.command import command from osc_lib import utils from osc_lib.utils import tags as _tag @@ -77,13 +76,11 @@ class ComputeSecurityGroupRulesColumn(cliff_columns.FormattableColumn): _formatters_network = { - 'location': format_columns.DictColumn, 'security_group_rules': NetworkSecurityGroupRulesColumn, } _formatters_compute = { - 'location': format_columns.DictColumn, 'rules': ComputeSecurityGroupRulesColumn, } @@ -120,6 +117,19 @@ class CreateSecurityGroup(common.NetworkAndComputeShowOne): metavar='<project>', help=self.enhance_help_neutron(_("Owner's project (name or ID)")) ) + stateful_group = parser.add_mutually_exclusive_group() + stateful_group.add_argument( + "--stateful", + action='store_true', + default=None, + help=_("Security group is stateful (Default)") + ) + stateful_group.add_argument( + "--stateless", + action='store_true', + default=None, + help=_("Security group is stateless") + ) identity_common.add_project_domain_option_to_parser( parser, enhance_help=self.enhance_help_neutron) _tag.add_tag_option_to_parser_for_create( @@ -138,6 +148,10 @@ class CreateSecurityGroup(common.NetworkAndComputeShowOne): attrs = {} attrs['name'] = parsed_args.name attrs['description'] = self._get_description(parsed_args) + if parsed_args.stateful: + attrs['stateful'] = True + if parsed_args.stateless: + attrs['stateful'] = False if parsed_args.project is not None: identity_client = self.app.client_manager.identity project_id = identity_common.find_project( @@ -202,6 +216,7 @@ class DeleteSecurityGroup(common.NetworkAndComputeDelete): # the OSC minimum requirements include SDK 1.0. class ListSecurityGroup(common.NetworkAndComputeLister): _description = _("List security groups") + FIELDS_TO_RETRIEVE = ['id', 'name', 'description', 'project_id', 'tags'] def update_parser_network(self, parser): if not self.is_docs_build: @@ -251,7 +266,8 @@ class ListSecurityGroup(common.NetworkAndComputeLister): filters['project_id'] = project_id _tag.get_tag_filtering_args(parsed_args, filters) - data = client.security_groups(**filters) + data = client.security_groups(fields=self.FIELDS_TO_RETRIEVE, + **filters) columns = ( "ID", @@ -313,6 +329,19 @@ class SetSecurityGroup(common.NetworkAndComputeCommand): metavar="<description>", help=_("New security group description") ) + stateful_group = parser.add_mutually_exclusive_group() + stateful_group.add_argument( + "--stateful", + action='store_true', + default=None, + help=_("Security group is stateful (Default)") + ) + stateful_group.add_argument( + "--stateless", + action='store_true', + default=None, + help=_("Security group is stateless") + ) return parser def update_parser_network(self, parser): @@ -329,6 +358,10 @@ class SetSecurityGroup(common.NetworkAndComputeCommand): attrs['name'] = parsed_args.name if parsed_args.description is not None: attrs['description'] = parsed_args.description + if parsed_args.stateful: + attrs['stateful'] = True + if parsed_args.stateless: + attrs['stateful'] = False # NOTE(rtheis): Previous behavior did not raise a CommandError # if there were no updates. Maintain this behavior and issue # the update. diff --git a/openstackclient/network/v2/security_group_rule.py b/openstackclient/network/v2/security_group_rule.py index f48478ea..1fbd97ab 100644 --- a/openstackclient/network/v2/security_group_rule.py +++ b/openstackclient/network/v2/security_group_rule.py @@ -16,7 +16,6 @@ import argparse import logging -from osc_lib.cli import format_columns from osc_lib.cli import parseractions from osc_lib import exceptions from osc_lib import utils @@ -31,11 +30,6 @@ from openstackclient.network import utils as network_utils LOG = logging.getLogger(__name__) -_formatters = { - 'location': format_columns.DictColumn, -} - - def _format_security_group_rule_show(obj): data = network_utils.transform_compute_security_group_rule(obj) return zip(*sorted(data.items())) @@ -353,7 +347,7 @@ class CreateSecurityGroupRule(common.NetworkAndComputeShowOne): # Create and show the security group rule. obj = client.create_security_group_rule(**attrs) display_columns, columns = _get_columns(obj) - data = utils.get_item_properties(obj, columns, formatters=_formatters) + data = utils.get_item_properties(obj, columns) return (display_columns, data) def take_action_compute(self, client, parsed_args): @@ -620,7 +614,7 @@ class ShowSecurityGroupRule(common.NetworkAndComputeShowOne): if not obj['remote_ip_prefix']: obj['remote_ip_prefix'] = _format_remote_ip_prefix(obj) display_columns, columns = _get_columns(obj) - data = utils.get_item_properties(obj, columns, formatters=_formatters) + data = utils.get_item_properties(obj, columns) return (display_columns, data) def take_action_compute(self, client, parsed_args): diff --git a/openstackclient/network/v2/subnet.py b/openstackclient/network/v2/subnet.py index f6844065..f87f7abe 100644 --- a/openstackclient/network/v2/subnet.py +++ b/openstackclient/network/v2/subnet.py @@ -61,7 +61,6 @@ _formatters = { 'allocation_pools': AllocationPoolsColumn, 'dns_nameservers': format_columns.ListColumn, 'host_routes': HostRoutesColumn, - 'location': format_columns.DictColumn, 'service_types': format_columns.ListColumn, 'tags': format_columns.ListColumn, } diff --git a/openstackclient/network/v2/subnet_pool.py b/openstackclient/network/v2/subnet_pool.py index 2750574a..56cf6152 100644 --- a/openstackclient/network/v2/subnet_pool.py +++ b/openstackclient/network/v2/subnet_pool.py @@ -42,7 +42,6 @@ def _get_columns(item): _formatters = { - 'location': format_columns.DictColumn, 'prefixes': format_columns.ListColumn, 'tags': format_columns.ListColumn, } diff --git a/openstackclient/tests/functional/identity/v3/common.py b/openstackclient/tests/functional/identity/v3/common.py index 86f090bc..a5edd9a5 100644 --- a/openstackclient/tests/functional/identity/v3/common.py +++ b/openstackclient/tests/functional/identity/v3/common.py @@ -33,7 +33,7 @@ class IdentityTests(base.TestCase): 'password_expires_at'] PROJECT_FIELDS = ['description', 'id', 'domain_id', 'is_domain', 'enabled', 'name', 'parent_id'] - ROLE_FIELDS = ['id', 'name', 'domain_id'] + ROLE_FIELDS = ['id', 'name', 'domain_id', 'description'] SERVICE_FIELDS = ['id', 'enabled', 'name', 'type', 'description'] REGION_FIELDS = ['description', 'enabled', 'parent_region', 'region'] ENDPOINT_FIELDS = ['id', 'region', 'region_id', 'service_id', diff --git a/openstackclient/tests/functional/identity/v3/test_project.py b/openstackclient/tests/functional/identity/v3/test_project.py index 96d41c3a..27cf4481 100644 --- a/openstackclient/tests/functional/identity/v3/test_project.py +++ b/openstackclient/tests/functional/identity/v3/test_project.py @@ -79,7 +79,6 @@ class ProjectTests(common.IdentityTests): '--disable ' '--property k0=v0 ' '%(name)s' % {'new_name': new_project_name, - 'domain': self.domain_name, 'name': project_name}) self.assertEqual(0, len(raw_output)) # check project details diff --git a/openstackclient/tests/functional/identity/v3/test_role.py b/openstackclient/tests/functional/identity/v3/test_role.py index 38bfff71..3954c4e3 100644 --- a/openstackclient/tests/functional/identity/v3/test_role.py +++ b/openstackclient/tests/functional/identity/v3/test_role.py @@ -20,6 +20,21 @@ class RoleTests(common.IdentityTests): def test_role_create(self): self._create_dummy_role() + def test_role_create_with_description(self): + role_name = data_utils.rand_name('TestRole') + description = data_utils.rand_name('description') + raw_output = self.openstack( + 'role create ' + '--description %(description)s ' + '%(name)s' % {'description': description, + 'name': role_name}) + role = self.parse_show_as_object(raw_output) + self.addCleanup(self.openstack, 'role delete %s' % role['id']) + items = self.parse_show(raw_output) + self.assert_show_fields(items, self.ROLE_FIELDS) + self.assertEqual(description, role['description']) + return role_name + def test_role_delete(self): role_name = self._create_dummy_role(add_clean_up=False) raw_output = self.openstack('role delete %s' % role_name) @@ -47,6 +62,16 @@ class RoleTests(common.IdentityTests): role = self.parse_show_as_object(raw_output) self.assertEqual(new_role_name, role['name']) + def test_role_set_description(self): + role_name = self._create_dummy_role() + description = data_utils.rand_name("NewDescription") + raw_output = self.openstack('role set --description %s %s' + % (description, role_name)) + self.assertEqual(0, len(raw_output)) + raw_output = self.openstack('role show %s' % role_name) + role = self.parse_show_as_object(raw_output) + self.assertEqual(description, role['description']) + def test_role_add(self): role_name = self._create_dummy_role() username = self._create_dummy_user() diff --git a/openstackclient/tests/functional/network/v2/test_router.py b/openstackclient/tests/functional/network/v2/test_router.py index 05aad7a0..0769dca6 100644 --- a/openstackclient/tests/functional/network/v2/test_router.py +++ b/openstackclient/tests/functional/network/v2/test_router.py @@ -261,3 +261,46 @@ class RouterTests(common.NetworkTagTests): new_name )) self.assertIsNone(cmd_output["external_gateway_info"]) + + def test_router_add_remove_route(self): + network_name = uuid.uuid4().hex + subnet_name = uuid.uuid4().hex + router_name = uuid.uuid4().hex + + self.openstack('network create %s' % network_name) + self.addCleanup(self.openstack, 'network delete %s' % network_name) + + self.openstack( + 'subnet create %s ' + '--network %s --subnet-range 10.0.0.0/24' % ( + subnet_name, network_name)) + + self.openstack('router create %s' % router_name) + self.addCleanup(self.openstack, 'router delete %s' % router_name) + + self.openstack('router add subnet %s %s' % (router_name, subnet_name)) + self.addCleanup(self.openstack, 'router remove subnet %s %s' % ( + router_name, subnet_name)) + + out1 = json.loads(self.openstack( + 'router add route -f json %s ' + '--route destination=10.0.10.0/24,gateway=10.0.0.10' % + router_name)), + self.assertEqual(1, len(out1[0]['routes'])) + + self.addCleanup( + self.openstack, 'router set %s --no-route' % router_name) + + out2 = json.loads(self.openstack( + 'router add route -f json %s ' + '--route destination=10.0.10.0/24,gateway=10.0.0.10 ' + '--route destination=10.0.11.0/24,gateway=10.0.0.11' % + router_name)), + self.assertEqual(2, len(out2[0]['routes'])) + + out3 = json.loads(self.openstack( + 'router remove route -f json %s ' + '--route destination=10.0.11.0/24,gateway=10.0.0.11 ' + '--route destination=10.0.12.0/24,gateway=10.0.0.12' % + router_name)), + self.assertEqual(1, len(out3[0]['routes'])) diff --git a/openstackclient/tests/functional/network/v2/test_security_group.py b/openstackclient/tests/functional/network/v2/test_security_group.py index 8ae24b72..d46f8db7 100644 --- a/openstackclient/tests/functional/network/v2/test_security_group.py +++ b/openstackclient/tests/functional/network/v2/test_security_group.py @@ -42,7 +42,7 @@ class SecurityGroupTests(common.NetworkTests): def test_security_group_set(self): other_name = uuid.uuid4().hex raw_output = self.openstack( - 'security group set --description NSA --name ' + + 'security group set --description NSA --stateless --name ' + other_name + ' ' + self.NAME ) self.assertEqual('', raw_output) @@ -50,8 +50,10 @@ class SecurityGroupTests(common.NetworkTests): cmd_output = json.loads(self.openstack( 'security group show -f json ' + other_name)) self.assertEqual('NSA', cmd_output['description']) + self.assertFalse(cmd_output['stateful']) def test_security_group_show(self): cmd_output = json.loads(self.openstack( 'security group show -f json ' + self.NAME)) self.assertEqual(self.NAME, cmd_output['name']) + self.assertTrue(cmd_output['stateful']) diff --git a/openstackclient/tests/functional/volume/v2/test_volume_snapshot.py b/openstackclient/tests/functional/volume/v2/test_volume_snapshot.py index 8d32d997..4977a73e 100644 --- a/openstackclient/tests/functional/volume/v2/test_volume_snapshot.py +++ b/openstackclient/tests/functional/volume/v2/test_volume_snapshot.py @@ -121,6 +121,24 @@ class VolumeSnapshotTests(common.BaseVolumeTests): cmd_output["size"], ) self.wait_for_status('volume snapshot', name2, 'available') + + raw_output = self.openstack( + 'volume snapshot set ' + + '--state error_deleting ' + + name2 + ) + self.assertOutput('', raw_output) + + # Test list --long, --status + cmd_output = json.loads(self.openstack( + 'volume snapshot list -f json ' + + '--long ' + + '--status error_deleting' + )) + names = [x["Name"] for x in cmd_output] + self.assertNotIn(name1, names) + self.assertIn(name2, names) + raw_output = self.openstack( 'volume snapshot set ' + '--state error ' + diff --git a/openstackclient/tests/unit/api/test_object_store_v1.py b/openstackclient/tests/unit/api/test_object_store_v1.py index 96c68d5a..b9e0740c 100644 --- a/openstackclient/tests/unit/api/test_object_store_v1.py +++ b/openstackclient/tests/unit/api/test_object_store_v1.py @@ -30,8 +30,10 @@ FAKE_CONTAINER = 'rainbarrel' FAKE_OBJECT = 'spigot' LIST_CONTAINER_RESP = [ - 'qaz', - 'fred', + {"name": "qaz", "count": 0, "bytes": 0, + "last_modified": "2020-05-16T05:52:07.377550"}, + {"name": "fred", "count": 0, "bytes": 0, + "last_modified": "2020-05-16T05:55:07.377550"}, ] LIST_OBJECT_RESP = [ @@ -117,34 +119,32 @@ class TestContainer(TestObjectAPIv1): ) self.assertEqual(LIST_CONTAINER_RESP, ret) -# def test_container_list_full_listing(self): -# sess = self.app.client_manager.session -# -# def side_effect(*args, **kwargs): -# rv = sess.get().json.return_value -# sess.get().json.return_value = [] -# sess.get().json.side_effect = None -# return rv -# -# resp = [{'name': 'is-name'}] -# sess.get().json.return_value = resp -# sess.get().json.side_effect = side_effect -# -# data = lib_container.list_containers( -# self.app.client_manager.session, -# fake_url, -# full_listing=True, -# ) -# -# # Check expected values -# sess.get.assert_called_with( -# fake_url, -# params={ -# 'format': 'json', -# 'marker': 'is-name', -# } -# ) -# self.assertEqual(resp, data) + def test_container_list_full_listing(self): + self.requests_mock.register_uri( + 'GET', + FAKE_URL + '?limit=1&format=json', + json=[LIST_CONTAINER_RESP[0]], + status_code=200, + ) + self.requests_mock.register_uri( + 'GET', + FAKE_URL + + '?marker=%s&limit=1&format=json' % LIST_CONTAINER_RESP[0]['name'], + json=[LIST_CONTAINER_RESP[1]], + status_code=200, + ) + self.requests_mock.register_uri( + 'GET', + FAKE_URL + + '?marker=%s&limit=1&format=json' % LIST_CONTAINER_RESP[1]['name'], + json=[], + status_code=200, + ) + ret = self.api.container_list( + limit=1, + full_listing=True, + ) + self.assertEqual(LIST_CONTAINER_RESP, ret) def test_container_show(self): headers = { diff --git a/openstackclient/tests/unit/common/test_parseractions.py b/openstackclient/tests/unit/common/test_parseractions.py index d015da43..736cd0b6 100644 --- a/openstackclient/tests/unit/common/test_parseractions.py +++ b/openstackclient/tests/unit/common/test_parseractions.py @@ -92,7 +92,7 @@ class TestMultiKeyValueAction(utils.TestCase): {'req1': 'aaa', 'req2': 'bbb'}, {'req1': '', 'req2': ''}, ] - self.assertItemsEqual(expect, actual) + self.assertCountEqual(expect, actual) def test_empty_required_optional(self): self.parser.add_argument( @@ -116,7 +116,7 @@ class TestMultiKeyValueAction(utils.TestCase): {'req1': 'aaa', 'req2': 'bbb'}, {'req1': '', 'req2': ''}, ] - self.assertItemsEqual(expect, actual) + self.assertCountEqual(expect, actual) def test_error_values_with_comma(self): self.assertRaises( diff --git a/openstackclient/tests/unit/common/test_quota.py b/openstackclient/tests/unit/common/test_quota.py index 0018e067..6504c5b0 100644 --- a/openstackclient/tests/unit/common/test_quota.py +++ b/openstackclient/tests/unit/common/test_quota.py @@ -392,6 +392,29 @@ class TestQuotaList(TestQuota): parsed_args, ) + def test_quota_list_compute_by_project(self): + # Two projects with non-default quotas + self.compute.quotas.get = mock.Mock( + side_effect=self.compute_quotas, + ) + + arglist = [ + '--compute', + '--project', self.projects[0].name, + ] + verifylist = [ + ('compute', True), + ('project', self.projects[0].name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + ret_quotas = list(data) + + self.assertEqual(self.compute_column_header, columns) + self.assertEqual(self.compute_reference_data, ret_quotas[0]) + self.assertEqual(1, len(ret_quotas)) + def test_quota_list_network(self): # Two projects with non-default quotas self.network.get_quota = mock.Mock( @@ -461,6 +484,29 @@ class TestQuotaList(TestQuota): self.assertEqual(self.network_reference_data, ret_quotas[0]) self.assertEqual(1, len(ret_quotas)) + def test_quota_list_network_by_project(self): + # Two projects with non-default quotas + self.network.get_quota = mock.Mock( + side_effect=self.network_quotas, + ) + + arglist = [ + '--network', + '--project', self.projects[0].name, + ] + verifylist = [ + ('network', True), + ('project', self.projects[0].name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + ret_quotas = list(data) + + self.assertEqual(self.network_column_header, columns) + self.assertEqual(self.network_reference_data, ret_quotas[0]) + self.assertEqual(1, len(ret_quotas)) + def test_quota_list_volume(self): # Two projects with non-default quotas self.volume.quotas.get = mock.Mock( @@ -530,6 +576,29 @@ class TestQuotaList(TestQuota): self.assertEqual(self.volume_reference_data, ret_quotas[0]) self.assertEqual(1, len(ret_quotas)) + def test_quota_list_volume_by_project(self): + # Two projects with non-default quotas + self.volume.quotas.get = mock.Mock( + side_effect=self.volume_quotas, + ) + + arglist = [ + '--volume', + '--project', self.projects[0].name, + ] + verifylist = [ + ('volume', True), + ('project', self.projects[0].name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + ret_quotas = list(data) + + self.assertEqual(self.volume_column_header, columns) + self.assertEqual(self.volume_reference_data, ret_quotas[0]) + self.assertEqual(1, len(ret_quotas)) + class TestQuotaSet(TestQuota): diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py index 27eefd85..7e4c71c5 100644 --- a/openstackclient/tests/unit/compute/v2/test_server.py +++ b/openstackclient/tests/unit/compute/v2/test_server.py @@ -55,6 +55,12 @@ class TestServer(compute_fakes.TestComputev2): self.images_mock = self.app.client_manager.image.images self.images_mock.reset_mock() + self.find_image_mock = self.app.client_manager.image.find_image + self.find_image_mock.reset_mock() + + self.get_image_mock = self.app.client_manager.image.get_image + self.get_image_mock.reset_mock() + # Get a shortcut to the volume client VolumeManager Mock self.volumes_mock = self.app.client_manager.volume.volumes self.volumes_mock.reset_mock() @@ -770,7 +776,8 @@ class TestServerCreate(TestServer): self.servers_mock.create.return_value = self.new_server self.image = image_fakes.FakeImage.create_one_image() - self.images_mock.get.return_value = self.image + self.find_image_mock.return_value = self.image + self.get_image_mock.return_value = self.image self.flavor = compute_fakes.FakeFlavor.create_one_flavor() self.flavors_mock.get.return_value = self.flavor @@ -1916,19 +1923,13 @@ class TestServerCreate(TestServer): ('config_drive', False), ('server_name', self.new_server.name), ] - _image = image_fakes.FakeImage.create_one_image() # create a image_info as the side_effect of the fake image_list() image_info = { - 'id': _image.id, - 'name': _image.name, - 'owner': _image.owner, 'hypervisor_type': 'qemu', } - self.api_mock = mock.Mock() - self.api_mock.image_list.side_effect = [ - [image_info], [], - ] - self.app.client_manager.image.api = self.api_mock + + _image = image_fakes.FakeImage.create_one_image(image_info) + self.images_mock.return_value = [_image] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -1953,7 +1954,7 @@ class TestServerCreate(TestServer): # ServerManager.create(name, image, flavor, **kwargs) self.servers_mock.create.assert_called_with( self.new_server.name, - image_info, + _image, self.flavor, **kwargs ) @@ -1977,20 +1978,13 @@ class TestServerCreate(TestServer): ('config_drive', False), ('server_name', self.new_server.name), ] - _image = image_fakes.FakeImage.create_one_image() # create a image_info as the side_effect of the fake image_list() image_info = { - 'id': _image.id, - 'name': _image.name, - 'owner': _image.owner, 'hypervisor_type': 'qemu', 'hw_disk_bus': 'ide', } - self.api_mock = mock.Mock() - self.api_mock.image_list.side_effect = [ - [image_info], [], - ] - self.app.client_manager.image.api = self.api_mock + _image = image_fakes.FakeImage.create_one_image(image_info) + self.images_mock.return_value = [_image] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -2015,7 +2009,7 @@ class TestServerCreate(TestServer): # ServerManager.create(name, image, flavor, **kwargs) self.servers_mock.create.assert_called_with( self.new_server.name, - image_info, + _image, self.flavor, **kwargs ) @@ -2039,20 +2033,14 @@ class TestServerCreate(TestServer): ('config_drive', False), ('server_name', self.new_server.name), ] - _image = image_fakes.FakeImage.create_one_image() # create a image_info as the side_effect of the fake image_list() image_info = { - 'id': _image.id, - 'name': _image.name, - 'owner': _image.owner, 'hypervisor_type': 'qemu', 'hw_disk_bus': 'ide', } - self.api_mock = mock.Mock() - self.api_mock.image_list.side_effect = [ - [image_info], [], - ] - self.app.client_manager.image.api = self.api_mock + + _image = image_fakes.FakeImage.create_one_image(image_info) + self.images_mock.return_value = [_image] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -2585,7 +2573,10 @@ class TestServerList(TestServer): self.servers_mock.list.return_value = self.servers self.image = image_fakes.FakeImage.create_one_image() - self.images_mock.get.return_value = self.image + + # self.images_mock.return_value = [self.image] + self.find_image_mock.return_value = self.image + self.get_image_mock.return_value = self.image self.flavor = compute_fakes.FakeFlavor.create_one_flavor() self.flavors_mock.get.return_value = self.flavor @@ -2599,7 +2590,7 @@ class TestServerList(TestServer): self.data_no_name_lookup = [] Image = collections.namedtuple('Image', 'id name') - self.images_mock.list.return_value = [ + self.images_mock.return_value = [ Image(id=s.image['id'], name=self.image.name) # Image will be an empty string if boot-from-volume for s in self.servers if s.image @@ -2662,11 +2653,11 @@ class TestServerList(TestServer): columns, data = self.cmd.take_action(parsed_args) self.servers_mock.list.assert_called_with(**self.kwargs) - self.images_mock.list.assert_called() + self.images_mock.assert_called() self.flavors_mock.list.assert_called() # we did not pass image or flavor, so gets on those must be absent self.assertFalse(self.flavors_mock.get.call_count) - self.assertFalse(self.images_mock.get.call_count) + self.assertFalse(self.get_image_mock.call_count) self.assertEqual(self.columns, columns) self.assertEqual(tuple(self.data), tuple(data)) @@ -2753,7 +2744,7 @@ class TestServerList(TestServer): self.servers_mock.list.assert_called_with(**self.kwargs) self.assertFalse(self.images_mock.list.call_count) self.assertFalse(self.flavors_mock.list.call_count) - self.images_mock.get.assert_called() + self.get_image_mock.assert_called() self.flavors_mock.get.assert_called() self.assertEqual(self.columns, columns) @@ -2771,7 +2762,8 @@ class TestServerList(TestServer): parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.images_mock.get.assert_any_call(self.image.id) + self.find_image_mock.assert_called_with(self.image.id, + ignore_missing=False) self.search_opts['image'] = self.image.id self.servers_mock.list.assert_called_with(**self.kwargs) @@ -3558,7 +3550,7 @@ class TestServerRebuild(TestServer): # Return value for utils.find_resource for image self.image = image_fakes.FakeImage.create_one_image() - self.images_mock.get.return_value = self.image + self.get_image_mock.return_value = self.image # Fake the rebuilt new server. attrs = { @@ -3585,6 +3577,41 @@ class TestServerRebuild(TestServer): self.cmd = server.RebuildServer(self.app, None) + def test_rebuild_with_image_name(self): + image_name = 'my-custom-image' + user_image = image_fakes.FakeImage.create_one_image( + attrs={'name': image_name}) + self.find_image_mock.return_value = user_image + + attrs = { + 'image': { + 'id': user_image.id + }, + 'networks': {}, + 'adminPass': 'passw0rd', + } + new_server = compute_fakes.FakeServer.create_one_server(attrs=attrs) + self.server.rebuild.return_value = new_server + + arglist = [ + self.server.id, + '--image', image_name + ] + verifylist = [ + ('server', self.server.id), + ('image', image_name) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # Get the command object to test. + self.cmd.take_action(parsed_args) + + self.servers_mock.get.assert_called_with(self.server.id) + self.find_image_mock.assert_called_with( + image_name, ignore_missing=False) + self.get_image_mock.assert_called_with(user_image.id) + self.server.rebuild.assert_called_with(user_image, None) + def test_rebuild_with_current_image(self): arglist = [ self.server.id, @@ -3598,7 +3625,8 @@ class TestServerRebuild(TestServer): self.cmd.take_action(parsed_args) self.servers_mock.get.assert_called_with(self.server.id) - self.images_mock.get.assert_called_with(self.image.id) + self.find_image_mock.assert_not_called() + self.get_image_mock.assert_called_with(self.image.id) self.server.rebuild.assert_called_with(self.image, None) def test_rebuild_with_current_image_and_password(self): @@ -3617,7 +3645,7 @@ class TestServerRebuild(TestServer): self.cmd.take_action(parsed_args) self.servers_mock.get.assert_called_with(self.server.id) - self.images_mock.get.assert_called_with(self.image.id) + self.get_image_mock.assert_called_with(self.image.id) self.server.rebuild.assert_called_with(self.image, password) def test_rebuild_with_description_api_older(self): @@ -3665,7 +3693,7 @@ class TestServerRebuild(TestServer): self.cmd.take_action(parsed_args) self.servers_mock.get.assert_called_with(self.server.id) - self.images_mock.get.assert_called_with(self.image.id) + self.get_image_mock.assert_called_with(self.image.id) self.server.rebuild.assert_called_with(self.image, None, description=description) @@ -3694,7 +3722,7 @@ class TestServerRebuild(TestServer): ) self.servers_mock.get.assert_called_with(self.server.id) - self.images_mock.get.assert_called_with(self.image.id) + self.get_image_mock.assert_called_with(self.image.id) self.server.rebuild.assert_called_with(self.image, None) @mock.patch.object(common_utils, 'wait_for_status', return_value=False) @@ -3718,7 +3746,7 @@ class TestServerRebuild(TestServer): ) self.servers_mock.get.assert_called_with(self.server.id) - self.images_mock.get.assert_called_with(self.image.id) + self.get_image_mock.assert_called_with(self.image.id) self.server.rebuild.assert_called_with(self.image, None) def test_rebuild_with_property(self): @@ -3738,7 +3766,7 @@ class TestServerRebuild(TestServer): self.cmd.take_action(parsed_args) self.servers_mock.get.assert_called_with(self.server.id) - self.images_mock.get.assert_called_with(self.image.id) + self.get_image_mock.assert_called_with(self.image.id) self.server.rebuild.assert_called_with( self.image, None, meta=expected_property) @@ -3767,7 +3795,7 @@ class TestServerRebuild(TestServer): key_name=self.server.key_name, ) self.servers_mock.get.assert_called_with(self.server.id) - self.images_mock.get.assert_called_with(self.image.id) + self.get_image_mock.assert_called_with(self.image.id) self.server.rebuild.assert_called_with(*args, **kwargs) def test_rebuild_with_keypair_name_older_version(self): @@ -3814,7 +3842,7 @@ class TestServerRebuild(TestServer): key_name=None, ) self.servers_mock.get.assert_called_with(self.server.id) - self.images_mock.get.assert_called_with(self.image.id) + self.get_image_mock.assert_called_with(self.image.id) self.server.rebuild.assert_called_with(*args, **kwargs) def test_rebuild_with_key_name_and_unset(self): @@ -3872,7 +3900,7 @@ class TestServerRescue(TestServer): # Return value for utils.find_resource for image self.image = image_fakes.FakeImage.create_one_image() - self.images_mock.get.return_value = self.image + self.get_image_mock.return_value = self.image new_server = compute_fakes.FakeServer.create_one_server() attrs = { @@ -3913,7 +3941,7 @@ class TestServerRescue(TestServer): def test_rescue_with_new_image(self): new_image = image_fakes.FakeImage.create_one_image() - self.images_mock.get.return_value = new_image + self.find_image_mock.return_value = new_image arglist = [ '--image', new_image.id, self.server.id, @@ -3928,7 +3956,7 @@ class TestServerRescue(TestServer): self.cmd.take_action(parsed_args) self.servers_mock.get.assert_called_with(self.server.id) - self.images_mock.get.assert_called_with(new_image.id) + self.find_image_mock.assert_called_with(new_image.id) self.server.rescue.assert_called_with(image=new_image, password=None) def test_rescue_with_current_image_and_password(self): @@ -4679,7 +4707,7 @@ class TestServerShow(TestServer): # This is the return value for utils.find_resource() self.servers_mock.get.return_value = self.server - self.images_mock.get.return_value = self.image + self.get_image_mock.return_value = self.image self.flavors_mock.get.return_value = self.flavor # Get the command object to test @@ -5140,7 +5168,8 @@ class TestServerGeneral(TestServer): 'links': u'http://xxx.yyy.com', } _server = compute_fakes.FakeServer.create_one_server(attrs=server_info) - find_resource.side_effect = [_server, _image, _flavor] + find_resource.side_effect = [_server, _flavor] + self.get_image_mock.return_value = _image # Prepare result data. info = { diff --git a/openstackclient/tests/unit/compute/v2/test_server_backup.py b/openstackclient/tests/unit/compute/v2/test_server_backup.py index 7dd459d8..5cdc2080 100644 --- a/openstackclient/tests/unit/compute/v2/test_server_backup.py +++ b/openstackclient/tests/unit/compute/v2/test_server_backup.py @@ -32,8 +32,8 @@ class TestServerBackup(compute_fakes.TestComputev2): self.servers_mock.reset_mock() # Get a shortcut to the image client ImageManager Mock - self.images_mock = self.app.client_manager.image.images - self.images_mock.reset_mock() + self.images_mock = self.app.client_manager.image + self.images_mock.find_image.reset_mock() # Set object attributes to be tested. Could be overwritten in subclass. self.attrs = {} @@ -60,15 +60,18 @@ class TestServerBackupCreate(TestServerBackup): # Just return whatever Image is testing with these days def image_columns(self, image): - columnlist = tuple(sorted(image.keys())) + # columnlist = tuple(sorted(image.keys())) + columnlist = ( + 'id', 'name', 'owner', 'protected', 'status', 'tags', 'visibility' + ) return columnlist def image_data(self, image): datalist = ( image['id'], image['name'], - image['owner'], - image['protected'], + image['owner_id'], + image['is_protected'], 'active', format_columns.ListColumn(image.get('tags')), image['visibility'], @@ -102,7 +105,8 @@ class TestServerBackupCreate(TestServerBackup): count=count, ) - self.images_mock.get = mock.Mock(side_effect=images) + # self.images_mock.get = mock.Mock(side_effect=images) + self.images_mock.find_image = mock.Mock(side_effect=images) return images def test_server_backup_defaults(self): @@ -174,16 +178,18 @@ class TestServerBackupCreate(TestServerBackup): @mock.patch.object(common_utils, 'wait_for_status', return_value=False) def test_server_backup_wait_fail(self, mock_wait_for_status): servers = self.setup_servers_mock(count=1) - images = image_fakes.FakeImage.create_images( - attrs={ - 'name': servers[0].name, - 'status': 'active', - }, - count=5, - ) - - self.images_mock.get = mock.Mock( - side_effect=images, + images = self.setup_images_mock(count=1, servers=servers) +# images = image_fakes.FakeImage.create_images( +# attrs={ +# 'name': servers[0].name, +# 'status': 'active', +# }, +# count=1, +# ) +# +# self.images_mock.find_image.return_value = images[0] + self.images_mock.get_image = mock.Mock( + side_effect=images[0], ) arglist = [ @@ -215,7 +221,7 @@ class TestServerBackupCreate(TestServerBackup): ) mock_wait_for_status.assert_called_once_with( - self.images_mock.get, + self.images_mock.get_image, images[0].id, callback=mock.ANY ) @@ -223,16 +229,10 @@ class TestServerBackupCreate(TestServerBackup): @mock.patch.object(common_utils, 'wait_for_status', return_value=True) def test_server_backup_wait_ok(self, mock_wait_for_status): servers = self.setup_servers_mock(count=1) - images = image_fakes.FakeImage.create_images( - attrs={ - 'name': servers[0].name, - 'status': 'active', - }, - count=5, - ) + images = self.setup_images_mock(count=1, servers=servers) - self.images_mock.get = mock.Mock( - side_effect=images, + self.images_mock.get_image = mock.Mock( + side_effect=images[0], ) arglist = [ @@ -263,7 +263,7 @@ class TestServerBackupCreate(TestServerBackup): ) mock_wait_for_status.assert_called_once_with( - self.images_mock.get, + self.images_mock.get_image, images[0].id, callback=mock.ANY ) diff --git a/openstackclient/tests/unit/compute/v2/test_server_image.py b/openstackclient/tests/unit/compute/v2/test_server_image.py index f9d7b10e..1cec5b68 100644 --- a/openstackclient/tests/unit/compute/v2/test_server_image.py +++ b/openstackclient/tests/unit/compute/v2/test_server_image.py @@ -31,8 +31,8 @@ class TestServerImage(compute_fakes.TestComputev2): self.servers_mock.reset_mock() # Get a shortcut to the image client ImageManager Mock - self.images_mock = self.app.client_manager.image.images - self.images_mock.reset_mock() + self.images_mock = self.app.client_manager.image + self.images_mock.find_image.reset_mock() # Set object attributes to be tested. Could be overwritten in subclass. self.attrs = {} @@ -58,15 +58,18 @@ class TestServerImage(compute_fakes.TestComputev2): class TestServerImageCreate(TestServerImage): def image_columns(self, image): - columnlist = tuple(sorted(image.keys())) + # columnlist = tuple(sorted(image.keys())) + columnlist = ( + 'id', 'name', 'owner', 'protected', 'status', 'tags', 'visibility' + ) return columnlist def image_data(self, image): datalist = ( image['id'], image['name'], - image['owner'], - image['protected'], + image['owner_id'], + image['is_protected'], 'active', format_columns.ListColumn(image.get('tags')), image['visibility'], @@ -100,7 +103,7 @@ class TestServerImageCreate(TestServerImage): count=count, ) - self.images_mock.get = mock.Mock(side_effect=images) + self.images_mock.find_image = mock.Mock(side_effect=images) self.servers_mock.create_image = mock.Mock( return_value=images[0].id, ) @@ -188,7 +191,7 @@ class TestServerImageCreate(TestServerImage): ) mock_wait_for_status.assert_called_once_with( - self.images_mock.get, + self.images_mock.get_image, images[0].id, callback=mock.ANY ) @@ -220,7 +223,7 @@ class TestServerImageCreate(TestServerImage): ) mock_wait_for_status.assert_called_once_with( - self.images_mock.get, + self.images_mock.get_image, images[0].id, callback=mock.ANY ) diff --git a/openstackclient/tests/unit/identity/v3/fakes.py b/openstackclient/tests/unit/identity/v3/fakes.py index fc4a48e3..58d5d14d 100644 --- a/openstackclient/tests/unit/identity/v3/fakes.py +++ b/openstackclient/tests/unit/identity/v3/fakes.py @@ -108,6 +108,9 @@ MAPPING_RESPONSE_2 = { "rules": MAPPING_RULES_2 } +mfa_opt1 = 'password,totp' +mfa_opt2 = 'password' + project_id = '8-9-64' project_name = 'beatles' project_description = 'Fab Four' @@ -176,6 +179,7 @@ ids_for_children = [PROJECT_WITH_GRANDPARENT['id']] role_id = 'r1' role_name = 'roller' +role_description = 'role description' ROLE = { 'id': role_id, diff --git a/openstackclient/tests/unit/identity/v3/test_access_rule.py b/openstackclient/tests/unit/identity/v3/test_access_rule.py index f8b6093a..904fe323 100644 --- a/openstackclient/tests/unit/identity/v3/test_access_rule.py +++ b/openstackclient/tests/unit/identity/v3/test_access_rule.py @@ -14,8 +14,8 @@ # import copy +from unittest import mock -import mock from osc_lib import exceptions from osc_lib import utils diff --git a/openstackclient/tests/unit/identity/v3/test_domain.py b/openstackclient/tests/unit/identity/v3/test_domain.py index 014986e5..46f389e8 100644 --- a/openstackclient/tests/unit/identity/v3/test_domain.py +++ b/openstackclient/tests/unit/identity/v3/test_domain.py @@ -68,6 +68,7 @@ class TestDomainCreate(TestDomain): kwargs = { 'name': self.domain.name, 'description': None, + 'options': {}, 'enabled': True, } self.domains_mock.create.assert_called_with( @@ -97,6 +98,7 @@ class TestDomainCreate(TestDomain): kwargs = { 'name': self.domain.name, 'description': 'new desc', + 'options': {}, 'enabled': True, } self.domains_mock.create.assert_called_with( @@ -126,6 +128,7 @@ class TestDomainCreate(TestDomain): kwargs = { 'name': self.domain.name, 'description': None, + 'options': {}, 'enabled': True, } self.domains_mock.create.assert_called_with( @@ -155,6 +158,7 @@ class TestDomainCreate(TestDomain): kwargs = { 'name': self.domain.name, 'description': None, + 'options': {}, 'enabled': False, } self.domains_mock.create.assert_called_with( @@ -164,6 +168,66 @@ class TestDomainCreate(TestDomain): self.assertEqual(self.columns, columns) self.assertEqual(self.datalist, data) + def test_domain_create_with_immutable(self): + arglist = [ + '--immutable', + self.domain.name, + ] + verifylist = [ + ('immutable', True), + ('name', self.domain.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # In base command class ShowOne in cliff, abstract method take_action() + # returns a two-part tuple with a tuple of column names and a tuple of + # data to be shown. + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'name': self.domain.name, + 'description': None, + 'options': {'immutable': True}, + 'enabled': True, + } + self.domains_mock.create.assert_called_with( + **kwargs + ) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.datalist, data) + + def test_domain_create_with_no_immutable(self): + arglist = [ + '--no-immutable', + self.domain.name, + ] + verifylist = [ + ('no_immutable', True), + ('name', self.domain.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # In base command class ShowOne in cliff, abstract method take_action() + # returns a two-part tuple with a tuple of column names and a tuple of + # data to be shown. + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'name': self.domain.name, + 'description': None, + 'options': {'immutable': False}, + 'enabled': True, + } + self.domains_mock.create.assert_called_with( + **kwargs + ) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.datalist, data) + class TestDomainDelete(TestDomain): @@ -354,6 +418,52 @@ class TestDomainSet(TestDomain): ) self.assertIsNone(result) + def test_domain_set_immutable_option(self): + arglist = [ + '--immutable', + self.domain.id, + ] + verifylist = [ + ('immutable', True), + ('domain', self.domain.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'options': {'immutable': True}, + } + self.domains_mock.update.assert_called_with( + self.domain.id, + **kwargs + ) + self.assertIsNone(result) + + def test_domain_set_no_immutable_option(self): + arglist = [ + '--no-immutable', + self.domain.id, + ] + verifylist = [ + ('no_immutable', True), + ('domain', self.domain.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'options': {'immutable': False}, + } + self.domains_mock.update.assert_called_with( + self.domain.id, + **kwargs + ) + self.assertIsNone(result) + class TestDomainShow(TestDomain): diff --git a/openstackclient/tests/unit/identity/v3/test_project.py b/openstackclient/tests/unit/identity/v3/test_project.py index 466bea18..dfd0805b 100644 --- a/openstackclient/tests/unit/identity/v3/test_project.py +++ b/openstackclient/tests/unit/identity/v3/test_project.py @@ -98,7 +98,8 @@ class TestProjectCreate(TestProject): 'description': None, 'enabled': True, 'parent': None, - 'tags': [] + 'tags': [], + 'options': {}, } # ProjectManager.create(name=, domain=, description=, # enabled=, **kwargs) @@ -156,7 +157,8 @@ class TestProjectCreate(TestProject): 'description': 'new desc', 'enabled': True, 'parent': None, - 'tags': [] + 'tags': [], + 'options': {}, } # ProjectManager.create(name=, domain=, description=, # enabled=, **kwargs) @@ -194,7 +196,8 @@ class TestProjectCreate(TestProject): 'description': None, 'enabled': True, 'parent': None, - 'tags': [] + 'tags': [], + 'options': {}, } # ProjectManager.create(name=, domain=, description=, # enabled=, **kwargs) @@ -232,7 +235,8 @@ class TestProjectCreate(TestProject): 'description': None, 'enabled': True, 'parent': None, - 'tags': [] + 'tags': [], + 'options': {}, } self.projects_mock.create.assert_called_with( **kwargs @@ -266,7 +270,8 @@ class TestProjectCreate(TestProject): 'description': None, 'enabled': True, 'parent': None, - 'tags': [] + 'tags': [], + 'options': {}, } # ProjectManager.create(name=, domain=, description=, # enabled=, **kwargs) @@ -302,7 +307,8 @@ class TestProjectCreate(TestProject): 'description': None, 'enabled': False, 'parent': None, - 'tags': [] + 'tags': [], + 'options': {}, } # ProjectManager.create(name=, domain=, # description=, enabled=, **kwargs) @@ -339,7 +345,8 @@ class TestProjectCreate(TestProject): 'parent': None, 'fee': 'fi', 'fo': 'fum', - 'tags': [] + 'tags': [], + 'options': {}, } # ProjectManager.create(name=, domain=, description=, # enabled=, **kwargs) @@ -350,6 +357,126 @@ class TestProjectCreate(TestProject): self.assertEqual(self.columns, columns) self.assertEqual(self.datalist, data) + def test_project_create_is_domain_false_property(self): + arglist = [ + '--property', 'is_domain=false', + self.project.name, + ] + verifylist = [ + ('parent', None), + ('enable', False), + ('disable', False), + ('name', self.project.name), + ('tags', []), + ('property', {'is_domain': 'false'}), + ('name', self.project.name), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # In base command class ShowOne in cliff, abstract method take_action() + # returns a two-part tuple with a tuple of column names and a tuple of + # data to be shown. + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'name': self.project.name, + 'domain': None, + 'description': None, + 'enabled': True, + 'parent': None, + 'is_domain': False, + 'tags': [], + 'options': {}, + } + self.projects_mock.create.assert_called_with( + **kwargs + ) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.datalist, data) + + def test_project_create_is_domain_true_property(self): + arglist = [ + '--property', 'is_domain=true', + self.project.name, + ] + verifylist = [ + ('parent', None), + ('enable', False), + ('disable', False), + ('name', self.project.name), + ('tags', []), + ('property', {'is_domain': 'true'}), + ('name', self.project.name), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # In base command class ShowOne in cliff, abstract method take_action() + # returns a two-part tuple with a tuple of column names and a tuple of + # data to be shown. + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'name': self.project.name, + 'domain': None, + 'description': None, + 'enabled': True, + 'parent': None, + 'is_domain': True, + 'tags': [], + 'options': {}, + } + self.projects_mock.create.assert_called_with( + **kwargs + ) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.datalist, data) + + def test_project_create_is_domain_none_property(self): + arglist = [ + '--property', 'is_domain=none', + self.project.name, + ] + verifylist = [ + ('parent', None), + ('enable', False), + ('disable', False), + ('name', self.project.name), + ('tags', []), + ('property', {'is_domain': 'none'}), + ('name', self.project.name), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # In base command class ShowOne in cliff, abstract method take_action() + # returns a two-part tuple with a tuple of column names and a tuple of + # data to be shown. + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'name': self.project.name, + 'domain': None, + 'description': None, + 'enabled': True, + 'parent': None, + 'is_domain': None, + 'tags': [], + 'options': {}, + } + self.projects_mock.create.assert_called_with( + **kwargs + ) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.datalist, data) + def test_project_create_parent(self): self.parent = identity_fakes.FakeProject.create_one_project() self.project = identity_fakes.FakeProject.create_one_project( @@ -380,7 +507,8 @@ class TestProjectCreate(TestProject): 'parent': self.parent.id, 'description': None, 'enabled': True, - 'tags': [] + 'tags': [], + 'options': {}, } self.projects_mock.create.assert_called_with( @@ -465,8 +593,89 @@ class TestProjectCreate(TestProject): 'description': None, 'enabled': True, 'parent': None, - 'tags': ['foo'] + 'tags': ['foo'], + 'options': {}, + } + self.projects_mock.create.assert_called_with( + **kwargs + ) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.datalist, data) + + def test_project_create_with_immutable_option(self): + arglist = [ + '--immutable', + self.project.name, + ] + verifylist = [ + ('immutable', True), + ('description', None), + ('enable', False), + ('disable', False), + ('name', self.project.name), + ('parent', None), + ('tags', []) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # In base command class ShowOne in cliff, abstract method take_action() + # returns a two-part tuple with a tuple of column names and a tuple of + # data to be shown. + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'name': self.project.name, + 'domain': None, + 'description': None, + 'enabled': True, + 'parent': None, + 'tags': [], + 'options': {'immutable': True}, } + # ProjectManager.create(name=, domain=, description=, + # enabled=, **kwargs) + self.projects_mock.create.assert_called_with( + **kwargs + ) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.datalist, data) + + def test_project_create_with_no_immutable_option(self): + arglist = [ + '--no-immutable', + self.project.name, + ] + verifylist = [ + ('no_immutable', True), + ('description', None), + ('enable', False), + ('disable', False), + ('name', self.project.name), + ('parent', None), + ('tags', []) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # In base command class ShowOne in cliff, abstract method take_action() + # returns a two-part tuple with a tuple of column names and a tuple of + # data to be shown. + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'name': self.project.name, + 'domain': None, + 'description': None, + 'enabled': True, + 'parent': None, + 'tags': [], + 'options': {'immutable': False}, + } + # ProjectManager.create(name=, domain=, description=, + # enabled=, **kwargs) self.projects_mock.create.assert_called_with( **kwargs ) @@ -927,6 +1136,60 @@ class TestProjectSet(TestProject): ) self.assertIsNone(result) + def test_project_set_with_immutable_option(self): + arglist = [ + '--domain', self.project.domain_id, + '--immutable', + self.project.name, + ] + verifylist = [ + ('domain', self.project.domain_id), + ('immutable', True), + ('enable', False), + ('disable', False), + ('project', self.project.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'options': {'immutable': True}, + } + self.projects_mock.update.assert_called_with( + self.project.id, + **kwargs + ) + self.assertIsNone(result) + + def test_project_set_with_no_immutable_option(self): + arglist = [ + '--domain', self.project.domain_id, + '--no-immutable', + self.project.name, + ] + verifylist = [ + ('domain', self.project.domain_id), + ('no_immutable', True), + ('enable', False), + ('disable', False), + ('project', self.project.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'options': {'immutable': False}, + } + self.projects_mock.update.assert_called_with( + self.project.id, + **kwargs + ) + self.assertIsNone(result) + class TestProjectShow(TestProject): diff --git a/openstackclient/tests/unit/identity/v3/test_role.py b/openstackclient/tests/unit/identity/v3/test_role.py index ead2cb58..544da7c1 100644 --- a/openstackclient/tests/unit/identity/v3/test_role.py +++ b/openstackclient/tests/unit/identity/v3/test_role.py @@ -332,6 +332,8 @@ class TestRoleCreate(TestRole): kwargs = { 'domain': None, 'name': identity_fakes.role_name, + 'description': None, + 'options': {}, } # RoleManager.create(name=, domain=) @@ -375,6 +377,8 @@ class TestRoleCreate(TestRole): kwargs = { 'domain': identity_fakes.domain_id, 'name': identity_fakes.ROLE_2['name'], + 'description': None, + 'options': {}, } # RoleManager.create(name=, domain=) @@ -391,6 +395,140 @@ class TestRoleCreate(TestRole): ) self.assertEqual(datalist, data) + def test_role_create_with_description(self): + + self.roles_mock.create.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.ROLE_2), + loaded=True, + ) + arglist = [ + '--description', identity_fakes.role_description, + identity_fakes.ROLE_2['name'], + ] + verifylist = [ + ('description', identity_fakes.role_description), + ('name', identity_fakes.ROLE_2['name']), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # In base command class ShowOne in cliff, abstract method take_action() + # returns a two-part tuple with a tuple of column names and a tuple of + # data to be shown. + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'description': identity_fakes.role_description, + 'name': identity_fakes.ROLE_2['name'], + 'domain': None, + 'options': {}, + } + + # RoleManager.create(name=, domain=) + self.roles_mock.create.assert_called_with( + **kwargs + ) + + collist = ('domain', 'id', 'name') + self.assertEqual(collist, columns) + datalist = ( + 'd1', + identity_fakes.ROLE_2['id'], + identity_fakes.ROLE_2['name'], + ) + self.assertEqual(datalist, data) + + def test_role_create_with_immutable_option(self): + + self.roles_mock.create.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.ROLE_2), + loaded=True, + ) + arglist = [ + '--immutable', + identity_fakes.ROLE_2['name'], + ] + verifylist = [ + ('immutable', True), + ('name', identity_fakes.ROLE_2['name']), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # In base command class ShowOne in cliff, abstract method take_action() + # returns a two-part tuple with a tuple of column names and a tuple of + # data to be shown. + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + + 'options': {'immutable': True}, + 'description': None, + 'name': identity_fakes.ROLE_2['name'], + 'domain': None, + } + + # RoleManager.create(name=, domain=) + self.roles_mock.create.assert_called_with( + **kwargs + ) + + collist = ('domain', 'id', 'name') + self.assertEqual(collist, columns) + datalist = ( + 'd1', + identity_fakes.ROLE_2['id'], + identity_fakes.ROLE_2['name'], + ) + self.assertEqual(datalist, data) + + def test_role_create_with_no_immutable_option(self): + + self.roles_mock.create.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.ROLE_2), + loaded=True, + ) + arglist = [ + '--no-immutable', + identity_fakes.ROLE_2['name'], + ] + verifylist = [ + ('no_immutable', True), + ('name', identity_fakes.ROLE_2['name']), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # In base command class ShowOne in cliff, abstract method take_action() + # returns a two-part tuple with a tuple of column names and a tuple of + # data to be shown. + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + + 'options': {'immutable': False}, + 'description': None, + 'name': identity_fakes.ROLE_2['name'], + 'domain': None, + } + + # RoleManager.create(name=, domain=) + self.roles_mock.create.assert_called_with( + **kwargs + ) + + collist = ('domain', 'id', 'name') + self.assertEqual(collist, columns) + datalist = ( + 'd1', + identity_fakes.ROLE_2['id'], + identity_fakes.ROLE_2['name'], + ) + self.assertEqual(datalist, data) + class TestRoleDelete(TestRole): @@ -825,6 +963,8 @@ class TestRoleSet(TestRole): # Set expected values kwargs = { 'name': 'over', + 'description': None, + 'options': {}, } # RoleManager.update(role, name=) self.roles_mock.update.assert_called_with( @@ -856,6 +996,107 @@ class TestRoleSet(TestRole): # Set expected values kwargs = { 'name': 'over', + 'description': None, + 'options': {}, + } + # RoleManager.update(role, name=) + self.roles_mock.update.assert_called_with( + identity_fakes.ROLE_2['id'], + **kwargs + ) + self.assertIsNone(result) + + def test_role_set_description(self): + self.roles_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.ROLE_2), + loaded=True, + ) + arglist = [ + '--name', 'over', + '--description', identity_fakes.role_description, + identity_fakes.ROLE_2['name'], + ] + verifylist = [ + ('name', 'over'), + ('description', identity_fakes.role_description), + ('role', identity_fakes.ROLE_2['name']), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'name': 'over', + 'description': identity_fakes.role_description, + 'options': {}, + } + # RoleManager.update(role, name=) + self.roles_mock.update.assert_called_with( + identity_fakes.ROLE_2['id'], + **kwargs + ) + self.assertIsNone(result) + + def test_role_set_with_immutable(self): + self.roles_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.ROLE_2), + loaded=True, + ) + arglist = [ + '--name', 'over', + '--immutable', + identity_fakes.ROLE_2['name'], + ] + verifylist = [ + ('name', 'over'), + ('immutable', True), + ('role', identity_fakes.ROLE_2['name']), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'name': 'over', + 'description': None, + 'options': {'immutable': True}, + } + # RoleManager.update(role, name=) + self.roles_mock.update.assert_called_with( + identity_fakes.ROLE_2['id'], + **kwargs + ) + self.assertIsNone(result) + + def test_role_set_with_no_immutable(self): + self.roles_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.ROLE_2), + loaded=True, + ) + arglist = [ + '--name', 'over', + '--no-immutable', + identity_fakes.ROLE_2['name'], + ] + verifylist = [ + ('name', 'over'), + ('no_immutable', True), + ('role', identity_fakes.ROLE_2['name']), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'name': 'over', + 'description': None, + 'options': {'immutable': False}, } # RoleManager.update(role, name=) self.roles_mock.update.assert_called_with( diff --git a/openstackclient/tests/unit/identity/v3/test_user.py b/openstackclient/tests/unit/identity/v3/test_user.py index 4b14bca0..c71435ba 100644 --- a/openstackclient/tests/unit/identity/v3/test_user.py +++ b/openstackclient/tests/unit/identity/v3/test_user.py @@ -111,6 +111,7 @@ class TestUserCreate(TestUser): 'description': None, 'domain': None, 'email': None, + 'options': {}, 'enabled': True, 'password': None, } @@ -150,6 +151,7 @@ class TestUserCreate(TestUser): 'description': None, 'domain': None, 'email': None, + 'options': {}, 'enabled': True, 'password': 'secret', } @@ -190,6 +192,7 @@ class TestUserCreate(TestUser): 'description': None, 'domain': None, 'email': None, + 'options': {}, 'enabled': True, 'password': 'abc123', } @@ -228,6 +231,7 @@ class TestUserCreate(TestUser): 'domain': None, 'email': 'barney@example.com', 'enabled': True, + 'options': {}, 'password': None, } # UserManager.create(name=, domain=, project=, password=, email=, @@ -265,6 +269,7 @@ class TestUserCreate(TestUser): 'domain': None, 'email': None, 'enabled': True, + 'options': {}, 'password': None, } # UserManager.create(name=, domain=, project=, password=, email=, @@ -311,6 +316,7 @@ class TestUserCreate(TestUser): 'description': None, 'domain': None, 'email': None, + 'options': {}, 'enabled': True, 'password': None, } @@ -356,6 +362,7 @@ class TestUserCreate(TestUser): 'description': None, 'domain': self.domain.id, 'email': None, + 'options': {}, 'enabled': True, 'password': None, } @@ -392,6 +399,7 @@ class TestUserCreate(TestUser): 'description': None, 'domain': None, 'email': None, + 'options': {}, 'enabled': True, 'password': None, } @@ -428,6 +436,7 @@ class TestUserCreate(TestUser): 'description': None, 'domain': None, 'email': None, + 'options': {}, 'enabled': False, 'password': None, } @@ -438,6 +447,471 @@ class TestUserCreate(TestUser): self.assertEqual(self.columns, columns) self.assertEqual(self.datalist, data) + def test_user_create_ignore_lockout_failure_attempts(self): + arglist = [ + '--ignore-lockout-failure-attempts', + self.user.name, + ] + verifylist = [ + ('ignore_lockout_failure_attempts', True), + ('enable', False), + ('disable', False), + ('name', self.user.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # In base command class ShowOne in cliff, abstract method take_action() + # returns a two-part tuple with a tuple of column names and a tuple of + # data to be shown. + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'name': self.user.name, + 'default_project': None, + 'description': None, + 'domain': None, + 'email': None, + 'enabled': True, + 'options': {'ignore_lockout_failure_attempts': True}, + 'password': None, + } + # UserManager.create(name=, domain=, project=, password=, email=, + # description=, enabled=, default_project=) + self.users_mock.create.assert_called_with( + **kwargs + ) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.datalist, data) + + def test_user_create_no_ignore_lockout_failure_attempts(self): + arglist = [ + '--no-ignore-lockout-failure-attempts', + self.user.name, + ] + verifylist = [ + ('no_ignore_lockout_failure_attempts', True), + ('enable', False), + ('disable', False), + ('name', self.user.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # In base command class ShowOne in cliff, abstract method take_action() + # returns a two-part tuple with a tuple of column names and a tuple of + # data to be shown. + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'name': self.user.name, + 'default_project': None, + 'description': None, + 'domain': None, + 'email': None, + 'enabled': True, + 'options': {'ignore_lockout_failure_attempts': False}, + 'password': None, + } + # UserManager.create(name=, domain=, project=, password=, email=, + # description=, enabled=, default_project=) + self.users_mock.create.assert_called_with( + **kwargs + ) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.datalist, data) + + def test_user_create_ignore_password_expiry(self): + arglist = [ + '--ignore-password-expiry', + self.user.name, + ] + verifylist = [ + ('ignore_password_expiry', True), + ('enable', False), + ('disable', False), + ('name', self.user.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # In base command class ShowOne in cliff, abstract method take_action() + # returns a two-part tuple with a tuple of column names and a tuple of + # data to be shown. + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'name': self.user.name, + 'default_project': None, + 'description': None, + 'domain': None, + 'email': None, + 'enabled': True, + 'options': {'ignore_password_expiry': True}, + 'password': None, + } + # UserManager.create(name=, domain=, project=, password=, email=, + # description=, enabled=, default_project=) + self.users_mock.create.assert_called_with( + **kwargs + ) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.datalist, data) + + def test_user_create_no_ignore_password_expiry(self): + arglist = [ + '--no-ignore-password-expiry', + self.user.name, + ] + verifylist = [ + ('no_ignore_password_expiry', True), + ('enable', False), + ('disable', False), + ('name', self.user.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # In base command class ShowOne in cliff, abstract method take_action() + # returns a two-part tuple with a tuple of column names and a tuple of + # data to be shown. + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'name': self.user.name, + 'default_project': None, + 'description': None, + 'domain': None, + 'email': None, + 'enabled': True, + 'options': {'ignore_password_expiry': False}, + 'password': None, + } + # UserManager.create(name=, domain=, project=, password=, email=, + # description=, enabled=, default_project=) + self.users_mock.create.assert_called_with( + **kwargs + ) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.datalist, data) + + def test_user_create_ignore_change_password_upon_first_use(self): + arglist = [ + '--ignore-change-password-upon-first-use', + self.user.name, + ] + verifylist = [ + ('ignore_change_password_upon_first_use', True), + ('enable', False), + ('disable', False), + ('name', self.user.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # In base command class ShowOne in cliff, abstract method take_action() + # returns a two-part tuple with a tuple of column names and a tuple of + # data to be shown. + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'name': self.user.name, + 'default_project': None, + 'description': None, + 'domain': None, + 'email': None, + 'enabled': True, + 'options': {'ignore_change_password_upon_first_use': True}, + 'password': None, + } + # UserManager.create(name=, domain=, project=, password=, email=, + # description=, enabled=, default_project=) + self.users_mock.create.assert_called_with( + **kwargs + ) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.datalist, data) + + def test_user_create_no_ignore_change_password_upon_first_use(self): + arglist = [ + '--no-ignore-change-password-upon-first-use', + self.user.name, + ] + verifylist = [ + ('no_ignore_change_password_upon_first_use', True), + ('enable', False), + ('disable', False), + ('name', self.user.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # In base command class ShowOne in cliff, abstract method take_action() + # returns a two-part tuple with a tuple of column names and a tuple of + # data to be shown. + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'name': self.user.name, + 'default_project': None, + 'description': None, + 'domain': None, + 'email': None, + 'enabled': True, + 'options': {'ignore_change_password_upon_first_use': False}, + 'password': None, + } + # UserManager.create(name=, domain=, project=, password=, email=, + # description=, enabled=, default_project=) + self.users_mock.create.assert_called_with( + **kwargs + ) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.datalist, data) + + def test_user_create_enables_lock_password(self): + arglist = [ + '--enable-lock-password', + self.user.name, + ] + verifylist = [ + ('enable_lock_password', True), + ('enable', False), + ('disable', False), + ('name', self.user.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # In base command class ShowOne in cliff, abstract method take_action() + # returns a two-part tuple with a tuple of column names and a tuple of + # data to be shown. + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'name': self.user.name, + 'default_project': None, + 'description': None, + 'domain': None, + 'email': None, + 'enabled': True, + 'options': {'lock_password': True}, + 'password': None, + } + # UserManager.create(name=, domain=, project=, password=, email=, + # description=, enabled=, default_project=) + self.users_mock.create.assert_called_with( + **kwargs + ) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.datalist, data) + + def test_user_create_disables_lock_password(self): + arglist = [ + '--disable-lock-password', + self.user.name, + ] + verifylist = [ + ('disable_lock_password', True), + ('enable', False), + ('disable', False), + ('name', self.user.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # In base command class ShowOne in cliff, abstract method take_action() + # returns a two-part tuple with a tuple of column names and a tuple of + # data to be shown. + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'name': self.user.name, + 'default_project': None, + 'description': None, + 'domain': None, + 'email': None, + 'enabled': True, + 'options': {'lock_password': False}, + 'password': None, + } + # UserManager.create(name=, domain=, project=, password=, email=, + # description=, enabled=, default_project=) + self.users_mock.create.assert_called_with( + **kwargs + ) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.datalist, data) + + def test_user_create_enable_multi_factor_auth(self): + arglist = [ + '--enable-multi-factor-auth', + self.user.name, + ] + verifylist = [ + ('enable_multi_factor_auth', True), + ('enable', False), + ('disable', False), + ('name', self.user.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # In base command class ShowOne in cliff, abstract method take_action() + # returns a two-part tuple with a tuple of column names and a tuple of + # data to be shown. + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'name': self.user.name, + 'default_project': None, + 'description': None, + 'domain': None, + 'email': None, + 'enabled': True, + 'options': {'multi_factor_auth_enabled': True}, + 'password': None, + } + # UserManager.create(name=, domain=, project=, password=, email=, + # description=, enabled=, default_project=) + self.users_mock.create.assert_called_with( + **kwargs + ) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.datalist, data) + + def test_user_create_disable_multi_factor_auth(self): + arglist = [ + '--disable-multi-factor-auth', + self.user.name, + ] + verifylist = [ + ('disable_multi_factor_auth', True), + ('enable', False), + ('disable', False), + ('name', self.user.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # In base command class ShowOne in cliff, abstract method take_action() + # returns a two-part tuple with a tuple of column names and a tuple of + # data to be shown. + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'name': self.user.name, + 'default_project': None, + 'description': None, + 'domain': None, + 'email': None, + 'enabled': True, + 'options': {'multi_factor_auth_enabled': False}, + 'password': None, + } + # UserManager.create(name=, domain=, project=, password=, email=, + # description=, enabled=, default_project=) + self.users_mock.create.assert_called_with( + **kwargs + ) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.datalist, data) + + def test_user_create_option_with_multi_factor_auth_rule(self): + arglist = [ + '--multi-factor-auth-rule', identity_fakes.mfa_opt1, + '--multi-factor-auth-rule', identity_fakes.mfa_opt2, + self.user.name, + ] + verifylist = [ + ('multi_factor_auth_rule', [identity_fakes.mfa_opt1, + identity_fakes.mfa_opt2]), + ('enable', False), + ('disable', False), + ('name', self.user.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # In base command class ShowOne in cliff, abstract method take_action() + # returns a two-part tuple with a tuple of column names and a tuple of + # data to be shown. + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'name': self.user.name, + 'default_project': None, + 'description': None, + 'domain': None, + 'email': None, + 'enabled': True, + 'options': {'multi_factor_auth_rules': [["password", "totp"], + ["password"]]}, + 'password': None, + } + # UserManager.create(name=, domain=, project=, password=, email=, + # description=, enabled=, default_project=) + self.users_mock.create.assert_called_with( + **kwargs + ) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.datalist, data) + + def test_user_create_with_multiple_options(self): + arglist = [ + '--ignore-password-expiry', + '--disable-multi-factor-auth', + '--multi-factor-auth-rule', identity_fakes.mfa_opt1, + self.user.name, + ] + verifylist = [ + ('ignore_password_expiry', True), + ('disable_multi_factor_auth', True), + ('multi_factor_auth_rule', [identity_fakes.mfa_opt1]), + ('enable', False), + ('disable', False), + ('name', self.user.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # In base command class ShowOne in cliff, abstract method take_action() + # returns a two-part tuple with a tuple of column names and a tuple of + # data to be shown. + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'name': self.user.name, + 'default_project': None, + 'description': None, + 'domain': None, + 'email': None, + 'enabled': True, + 'options': {'ignore_password_expiry': True, + 'multi_factor_auth_enabled': False, + 'multi_factor_auth_rules': [["password", "totp"]]}, + 'password': None, + } + # UserManager.create(name=, domain=, project=, password=, email=, + # description=, enabled=, default_project=) + self.users_mock.create.assert_called_with( + **kwargs + ) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.datalist, data) + class TestUserDelete(TestUser): @@ -1007,6 +1481,384 @@ class TestUserSet(TestUser): ) self.assertIsNone(result) + def test_user_set_ignore_lockout_failure_attempts(self): + arglist = [ + '--ignore-lockout-failure-attempts', + self.user.name, + ] + verifylist = [ + ('name', None), + ('password', None), + ('email', None), + ('ignore_lockout_failure_attempts', True), + ('project', None), + ('enable', False), + ('disable', False), + ('user', self.user.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + # Set expected values + kwargs = { + 'enabled': True, + 'options': {'ignore_lockout_failure_attempts': True}, + } + # UserManager.update(user, name=, domain=, project=, password=, + # email=, description=, enabled=, default_project=) + self.users_mock.update.assert_called_with( + self.user.id, + **kwargs + ) + self.assertIsNone(result) + + def test_user_set_no_ignore_lockout_failure_attempts(self): + arglist = [ + '--no-ignore-lockout-failure-attempts', + self.user.name, + ] + verifylist = [ + ('name', None), + ('password', None), + ('email', None), + ('no_ignore_lockout_failure_attempts', True), + ('project', None), + ('enable', False), + ('disable', False), + ('user', self.user.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + # Set expected values + kwargs = { + 'enabled': True, + 'options': {'ignore_lockout_failure_attempts': False}, + } + # UserManager.update(user, name=, domain=, project=, password=, + # email=, description=, enabled=, default_project=) + self.users_mock.update.assert_called_with( + self.user.id, + **kwargs + ) + self.assertIsNone(result) + + def test_user_set_ignore_password_expiry(self): + arglist = [ + '--ignore-password-expiry', + self.user.name, + ] + verifylist = [ + ('name', None), + ('password', None), + ('email', None), + ('ignore_password_expiry', True), + ('project', None), + ('enable', False), + ('disable', False), + ('user', self.user.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + # Set expected values + kwargs = { + 'enabled': True, + 'options': {'ignore_password_expiry': True}, + } + # UserManager.update(user, name=, domain=, project=, password=, + # email=, description=, enabled=, default_project=) + self.users_mock.update.assert_called_with( + self.user.id, + **kwargs + ) + self.assertIsNone(result) + + def test_user_set_no_ignore_password_expiry(self): + arglist = [ + '--no-ignore-password-expiry', + self.user.name, + ] + verifylist = [ + ('name', None), + ('password', None), + ('email', None), + ('no_ignore_password_expiry', True), + ('project', None), + ('enable', False), + ('disable', False), + ('user', self.user.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + # Set expected values + kwargs = { + 'enabled': True, + 'options': {'ignore_password_expiry': False}, + } + # UserManager.update(user, name=, domain=, project=, password=, + # email=, description=, enabled=, default_project=) + self.users_mock.update.assert_called_with( + self.user.id, + **kwargs + ) + self.assertIsNone(result) + + def test_user_set_ignore_change_password_upon_first_use(self): + arglist = [ + '--ignore-change-password-upon-first-use', + self.user.name, + ] + verifylist = [ + ('name', None), + ('password', None), + ('email', None), + ('ignore_change_password_upon_first_use', True), + ('project', None), + ('enable', False), + ('disable', False), + ('user', self.user.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + # Set expected values + kwargs = { + 'enabled': True, + 'options': {'ignore_change_password_upon_first_use': True}, + } + # UserManager.update(user, name=, domain=, project=, password=, + # email=, description=, enabled=, default_project=) + self.users_mock.update.assert_called_with( + self.user.id, + **kwargs + ) + self.assertIsNone(result) + + def test_user_set_no_ignore_change_password_upon_first_use(self): + arglist = [ + '--no-ignore-change-password-upon-first-use', + self.user.name, + ] + verifylist = [ + ('name', None), + ('password', None), + ('email', None), + ('no_ignore_change_password_upon_first_use', True), + ('project', None), + ('enable', False), + ('disable', False), + ('user', self.user.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + # Set expected values + kwargs = { + 'enabled': True, + 'options': {'ignore_change_password_upon_first_use': False}, + } + # UserManager.update(user, name=, domain=, project=, password=, + # email=, description=, enabled=, default_project=) + self.users_mock.update.assert_called_with( + self.user.id, + **kwargs + ) + self.assertIsNone(result) + + def test_user_set_enable_lock_password(self): + arglist = [ + '--enable-lock-password', + self.user.name, + ] + verifylist = [ + ('name', None), + ('password', None), + ('email', None), + ('enable_lock_password', True), + ('project', None), + ('enable', False), + ('disable', False), + ('user', self.user.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + # Set expected values + kwargs = { + 'enabled': True, + 'options': {'lock_password': True}, + } + # UserManager.update(user, name=, domain=, project=, password=, + # email=, description=, enabled=, default_project=) + self.users_mock.update.assert_called_with( + self.user.id, + **kwargs + ) + self.assertIsNone(result) + + def test_user_set_disable_lock_password(self): + arglist = [ + '--disable-lock-password', + self.user.name, + ] + verifylist = [ + ('name', None), + ('password', None), + ('email', None), + ('disable_lock_password', True), + ('project', None), + ('enable', False), + ('disable', False), + ('user', self.user.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + # Set expected values + kwargs = { + 'enabled': True, + 'options': {'lock_password': False}, + } + # UserManager.update(user, name=, domain=, project=, password=, + # email=, description=, enabled=, default_project=) + self.users_mock.update.assert_called_with( + self.user.id, + **kwargs + ) + self.assertIsNone(result) + + def test_user_set_enable_multi_factor_auth(self): + arglist = [ + '--enable-multi-factor-auth', + self.user.name, + ] + verifylist = [ + ('name', None), + ('password', None), + ('email', None), + ('enable_multi_factor_auth', True), + ('project', None), + ('enable', False), + ('disable', False), + ('user', self.user.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + # Set expected values + kwargs = { + 'enabled': True, + 'options': {'multi_factor_auth_enabled': True}, + } + # UserManager.update(user, name=, domain=, project=, password=, + # email=, description=, enabled=, default_project=) + self.users_mock.update.assert_called_with( + self.user.id, + **kwargs + ) + self.assertIsNone(result) + + def test_user_set_disable_multi_factor_auth(self): + arglist = [ + '--disable-multi-factor-auth', + self.user.name, + ] + verifylist = [ + ('name', None), + ('password', None), + ('email', None), + ('disable_multi_factor_auth', True), + ('project', None), + ('enable', False), + ('disable', False), + ('user', self.user.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + # Set expected values + kwargs = { + 'enabled': True, + 'options': {'multi_factor_auth_enabled': False}, + } + # UserManager.update(user, name=, domain=, project=, password=, + # email=, description=, enabled=, default_project=) + self.users_mock.update.assert_called_with( + self.user.id, + **kwargs + ) + self.assertIsNone(result) + + def test_user_set_option_multi_factor_auth_rule(self): + arglist = [ + '--multi-factor-auth-rule', identity_fakes.mfa_opt1, + self.user.name, + ] + verifylist = [ + ('name', None), + ('password', None), + ('email', None), + ('multi_factor_auth_rule', [identity_fakes.mfa_opt1]), + ('project', None), + ('enable', False), + ('disable', False), + ('user', self.user.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + # Set expected values + kwargs = { + 'enabled': True, + 'options': {'multi_factor_auth_rules': [["password", "totp"]]}} + + # UserManager.update(user, name=, domain=, project=, password=, + # email=, description=, enabled=, default_project=) + self.users_mock.update.assert_called_with( + self.user.id, + **kwargs + ) + self.assertIsNone(result) + + def test_user_set_with_multiple_options(self): + arglist = [ + '--ignore-password-expiry', + '--enable-multi-factor-auth', + '--multi-factor-auth-rule', identity_fakes.mfa_opt1, + self.user.name, + ] + verifylist = [ + ('name', None), + ('password', None), + ('email', None), + ('ignore_password_expiry', True), + ('enable_multi_factor_auth', True), + ('multi_factor_auth_rule', [identity_fakes.mfa_opt1]), + ('project', None), + ('enable', False), + ('disable', False), + ('user', self.user.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + # Set expected values + kwargs = { + 'enabled': True, + 'options': {'ignore_password_expiry': True, + 'multi_factor_auth_enabled': True, + 'multi_factor_auth_rules': [["password", "totp"]]}} + + # UserManager.update(user, name=, domain=, project=, password=, + # email=, description=, enabled=, default_project=) + self.users_mock.update.assert_called_with( + self.user.id, + **kwargs + ) + self.assertIsNone(result) + class TestUserSetPassword(TestUser): diff --git a/openstackclient/tests/unit/image/v1/fakes.py b/openstackclient/tests/unit/image/v1/fakes.py index de232235..add3978d 100644 --- a/openstackclient/tests/unit/image/v1/fakes.py +++ b/openstackclient/tests/unit/image/v1/fakes.py @@ -13,10 +13,11 @@ # under the License. # -import copy from unittest import mock import uuid +from openstack.image.v1 import image + from openstackclient.tests.unit import fakes from openstackclient.tests.unit import utils from openstackclient.tests.unit.volume.v1 import fakes as volume_fakes @@ -111,13 +112,10 @@ class FakeImage(object): 'Alpha': 'a', 'Beta': 'b', 'Gamma': 'g'}, + 'status': 'status' + uuid.uuid4().hex } # Overwrite default attributes if there are some attributes set image_info.update(attrs) - image = fakes.FakeResource( - info=copy.deepcopy(image_info), - loaded=True) - - return image + return image.Image(**image_info) diff --git a/openstackclient/tests/unit/image/v1/test_image.py b/openstackclient/tests/unit/image/v1/test_image.py index 970b36c6..2f190a7a 100644 --- a/openstackclient/tests/unit/image/v1/test_image.py +++ b/openstackclient/tests/unit/image/v1/test_image.py @@ -17,7 +17,6 @@ import copy from unittest import mock from osc_lib.cli import format_columns -from osc_lib import exceptions from openstackclient.image.v1 import image from openstackclient.tests.unit import fakes @@ -29,9 +28,8 @@ class TestImage(image_fakes.TestImagev1): def setUp(self): super(TestImage, self).setUp() - # Get a shortcut to the ServerManager Mock - self.images_mock = self.app.client_manager.image.images - self.images_mock.reset_mock() + self.app.client_manager.image = mock.Mock() + self.client = self.app.client_manager.image class TestImageCreate(TestImage): @@ -48,6 +46,7 @@ class TestImageCreate(TestImage): 'owner', 'properties', 'protected', + 'size' ) data = ( new_image.container_format, @@ -57,28 +56,24 @@ class TestImageCreate(TestImage): new_image.min_disk, new_image.min_ram, new_image.name, - new_image.owner, + new_image.owner_id, format_columns.DictColumn(new_image.properties), - new_image.protected, + new_image.is_protected, + new_image.size ) def setUp(self): super(TestImageCreate, self).setUp() - self.images_mock.create.return_value = self.new_image - # This is the return value for utils.find_resource() - self.images_mock.get.return_value = self.new_image - self.images_mock.update.return_value = self.new_image + self.client.create_image = mock.Mock(return_value=self.new_image) + self.client.find_image = mock.Mock(return_value=self.new_image) + self.client.update_image = mock.Mock(return_image=self.new_image) # Get the command object to test self.cmd = image.CreateImage(self.app, None) - def test_image_reserve_no_options(self): - mock_exception = { - 'find.side_effect': exceptions.CommandError('x'), - 'get.side_effect': exceptions.CommandError('x'), - } - self.images_mock.configure_mock(**mock_exception) + @mock.patch('sys.stdin', side_effect=[None]) + def test_image_reserve_no_options(self, raw_input): arglist = [ self.new_image.name, ] @@ -95,25 +90,20 @@ class TestImageCreate(TestImage): columns, data = self.cmd.take_action(parsed_args) # ImageManager.create(name=, **) - self.images_mock.create.assert_called_with( + self.client.create_image.assert_called_with( name=self.new_image.name, container_format=image.DEFAULT_CONTAINER_FORMAT, - disk_format=image.DEFAULT_DISK_FORMAT, - data=mock.ANY, + disk_format=image.DEFAULT_DISK_FORMAT ) # Verify update() was not called, if it was show the args - self.assertEqual(self.images_mock.update.call_args_list, []) + self.assertEqual(self.client.update_image.call_args_list, []) self.assertEqual(self.columns, columns) self.assertItemEqual(self.data, data) - def test_image_reserve_options(self): - mock_exception = { - 'find.side_effect': exceptions.CommandError('x'), - 'get.side_effect': exceptions.CommandError('x'), - } - self.images_mock.configure_mock(**mock_exception) + @mock.patch('sys.stdin', side_effect=[None]) + def test_image_reserve_options(self, raw_input): arglist = [ '--container-format', 'ovf', '--disk-format', 'ami', @@ -144,20 +134,19 @@ class TestImageCreate(TestImage): columns, data = self.cmd.take_action(parsed_args) # ImageManager.create(name=, **) - self.images_mock.create.assert_called_with( + self.client.create_image.assert_called_with( name=self.new_image.name, container_format='ovf', disk_format='ami', min_disk=10, min_ram=4, - protected=True, + is_protected=True, is_public=False, - owner='q', - data=mock.ANY, + owner_id='q', ) # Verify update() was not called, if it was show the args - self.assertEqual(self.images_mock.update.call_args_list, []) + self.assertEqual(self.client.update_image.call_args_list, []) self.assertEqual(self.columns, columns) self.assertItemEqual(self.data, data) @@ -167,11 +156,6 @@ class TestImageCreate(TestImage): mock_file = mock.Mock(name='File') mock_open.return_value = mock_file mock_open.read.return_value = self.data - mock_exception = { - 'find.side_effect': exceptions.CommandError('x'), - 'get.side_effect': exceptions.CommandError('x'), - } - self.images_mock.configure_mock(**mock_exception) arglist = [ '--file', 'filer', @@ -203,15 +187,12 @@ class TestImageCreate(TestImage): # Ensure the input file is closed mock_file.close.assert_called_with() - # ImageManager.get(name) not to be called since update action exists - self.images_mock.get.assert_not_called() - # ImageManager.create(name=, **) - self.images_mock.create.assert_called_with( + self.client.create_image.assert_called_with( name=self.new_image.name, container_format=image.DEFAULT_CONTAINER_FORMAT, disk_format=image.DEFAULT_DISK_FORMAT, - protected=False, + is_protected=False, is_public=True, properties={ 'Alpha': '1', @@ -221,7 +202,7 @@ class TestImageCreate(TestImage): ) # Verify update() was not called, if it was show the args - self.assertEqual(self.images_mock.update.call_args_list, []) + self.assertEqual(self.client.update_image.call_args_list, []) self.assertEqual(self.columns, columns) self.assertItemEqual(self.data, data) @@ -235,8 +216,8 @@ class TestImageDelete(TestImage): super(TestImageDelete, self).setUp() # This is the return value for utils.find_resource() - self.images_mock.get.return_value = self._image - self.images_mock.delete.return_value = None + self.client.find_image = mock.Mock(return_value=self._image) + self.client.delete_image = mock.Mock(return_value=None) # Get the command object to test self.cmd = image.DeleteImage(self.app, None) @@ -252,7 +233,7 @@ class TestImageDelete(TestImage): result = self.cmd.take_action(parsed_args) - self.images_mock.delete.assert_called_with(self._image.id) + self.client.delete_image.assert_called_with(self._image.id) self.assertIsNone(result) @@ -269,7 +250,7 @@ class TestImageList(TestImage): ( _image.id, _image.name, - '', + _image.status ), ) @@ -277,13 +258,13 @@ class TestImageList(TestImage): info = { 'id': _image.id, 'name': _image.name, - 'owner': _image.owner, + 'owner': _image.owner_id, 'container_format': _image.container_format, 'disk_format': _image.disk_format, 'min_disk': _image.min_disk, 'min_ram': _image.min_ram, 'is_public': _image.is_public, - 'protected': _image.protected, + 'protected': _image.is_protected, 'properties': _image.properties, } image_info = copy.deepcopy(info) @@ -291,11 +272,10 @@ class TestImageList(TestImage): def setUp(self): super(TestImageList, self).setUp() - self.api_mock = mock.Mock() - self.api_mock.image_list.side_effect = [ - [self.image_info], [], + self.client.images = mock.Mock() + self.client.images.side_effect = [ + [self._image], [], ] - self.app.client_manager.image.api = self.api_mock # Get the command object to test self.cmd = image.ListImage(self.app, None) @@ -313,10 +293,7 @@ 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( - detailed=True, - marker=self._image.id, - ) + self.client.images.assert_called_with() self.assertEqual(self.columns, columns) self.assertEqual(self.datalist, tuple(data)) @@ -336,10 +313,8 @@ 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( - detailed=True, - public=True, - marker=self._image.id, + self.client.images.assert_called_with( + is_public=True, ) self.assertEqual(self.columns, columns) @@ -360,10 +335,8 @@ 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( - detailed=True, - private=True, - marker=self._image.id, + self.client.images.assert_called_with( + is_private=True, ) self.assertEqual(self.columns, columns) @@ -382,10 +355,7 @@ 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( - detailed=True, - marker=self._image.id, - ) + self.client.images.assert_called_with() collist = ( 'ID', @@ -405,14 +375,14 @@ class TestImageList(TestImage): datalist = (( self._image.id, self._image.name, - '', - '', - '', - '', - '', - image.VisibilityColumn(True), - False, - self._image.owner, + self._image.disk_format, + self._image.container_format, + self._image.size, + self._image.checksum, + self._image.status, + image.VisibilityColumn(self._image.is_public), + self._image.is_protected, + self._image.owner_id, format_columns.DictColumn( {'Alpha': 'a', 'Beta': 'b', 'Gamma': 'g'}), ), ) @@ -436,12 +406,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( - detailed=True, - marker=self._image.id, - ) + self.client.images.assert_called_with() sf_mock.assert_called_with( - [self.image_info], + [self._image], attr='a', value='1', property_field='properties', @@ -453,7 +420,7 @@ class TestImageList(TestImage): @mock.patch('osc_lib.utils.sort_items') def test_image_list_sort_option(self, si_mock): si_mock.side_effect = [ - [self.image_info], [], + [self._image], [], ] arglist = ['--sort', 'name:asc'] @@ -464,12 +431,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( - detailed=True, - marker=self._image.id, - ) + self.client.images.assert_called_with() si_mock.assert_called_with( - [self.image_info], + [self._image], 'name:asc' ) @@ -485,8 +449,8 @@ class TestImageSet(TestImage): super(TestImageSet, self).setUp() # This is the return value for utils.find_resource() - self.images_mock.get.return_value = self._image - self.images_mock.update.return_value = self._image + self.client.find_image = mock.Mock(return_value=self._image) + self.client.update_image = mock.Mock(return_value=self._image) # Get the command object to test self.cmd = image.SetImage(self.app, None) @@ -502,8 +466,7 @@ class TestImageSet(TestImage): result = self.cmd.take_action(parsed_args) - self.images_mock.update.assert_called_with(self._image.id, - **{}) + self.client.update_image.assert_called_with(self._image.id, **{}) self.assertIsNone(result) def test_image_set_options(self): @@ -541,7 +504,7 @@ class TestImageSet(TestImage): 'size': 35165824 } # ImageManager.update(image, **kwargs) - self.images_mock.update.assert_called_with( + self.client.update_image.assert_called_with( self._image.id, **kwargs ) @@ -565,11 +528,11 @@ class TestImageSet(TestImage): result = self.cmd.take_action(parsed_args) kwargs = { - 'protected': True, + 'is_protected': True, 'is_public': False, } # ImageManager.update(image, **kwargs) - self.images_mock.update.assert_called_with( + self.client.update_image.assert_called_with( self._image.id, **kwargs ) @@ -593,11 +556,11 @@ class TestImageSet(TestImage): result = self.cmd.take_action(parsed_args) kwargs = { - 'protected': False, + 'is_protected': False, 'is_public': True, } # ImageManager.update(image, **kwargs) - self.images_mock.update.assert_called_with( + self.client.update_image.assert_called_with( self._image.id, **kwargs ) @@ -625,7 +588,7 @@ class TestImageSet(TestImage): }, } # ImageManager.update(image, **kwargs) - self.images_mock.update.assert_called_with( + self.client.update_image.assert_called_with( self._image.id, **kwargs ) @@ -683,7 +646,7 @@ class TestImageSet(TestImage): '', ) # ImageManager.update(image_id, remove_props=, **) - self.images_mock.update.assert_called_with( + self.client.update_image.assert_called_with( self._image.id, name='updated_image', volume='volly', @@ -710,7 +673,7 @@ class TestImageSet(TestImage): 'min_ram': 0, } # ImageManager.update(image, **kwargs) - self.images_mock.update.assert_called_with( + self.client.update_image.assert_called_with( self._image.id, **kwargs ) @@ -742,16 +705,16 @@ class TestImageShow(TestImage): _image.min_disk, _image.min_ram, _image.name, - _image.owner, + _image.owner_id, format_columns.DictColumn(_image.properties), - _image.protected, + _image.is_protected, _image.size, ) def setUp(self): super(TestImageShow, self).setUp() - self.images_mock.get.return_value = self._image + self.client.find_image = mock.Mock(return_value=self._image) # Get the command object to test self.cmd = image.ShowImage(self.app, None) @@ -769,7 +732,7 @@ class TestImageShow(TestImage): # returns a two-part tuple with a tuple of column names and a tuple of # data to be shown. columns, data = self.cmd.take_action(parsed_args) - self.images_mock.get.assert_called_with( + self.client.find_image.assert_called_with( self._image.id, ) @@ -791,9 +754,9 @@ class TestImageShow(TestImage): # returns a two-part tuple with a tuple of column names and a tuple of # data to be shown. columns, data = self.cmd.take_action(parsed_args) - self.images_mock.get.assert_called_with( + self.client.find_image.assert_called_with( self._image.id, ) size_index = columns.index('size') - self.assertEqual(data[size_index], '2K') + self.assertEqual(data[size_index].human_readable(), '2K') diff --git a/openstackclient/tests/unit/image/v2/fakes.py b/openstackclient/tests/unit/image/v2/fakes.py index 655ae341..516d5630 100644 --- a/openstackclient/tests/unit/image/v2/fakes.py +++ b/openstackclient/tests/unit/image/v2/fakes.py @@ -18,9 +18,9 @@ import random from unittest import mock import uuid -from glanceclient.v2 import schemas +from openstack.image.v2 import image +from openstack.image.v2 import member from osc_lib.cli import format_columns -import warlock from openstackclient.tests.unit import fakes from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes @@ -154,6 +154,12 @@ class FakeImagev2Client(object): self.image_members.resource_class = fakes.FakeResource(None, {}) self.image_tags = mock.Mock() self.image_tags.resource_class = fakes.FakeResource(None, {}) + + self.find_image = mock.Mock() + self.find_image.resource_class = fakes.FakeResource(None, {}) + + self.get_image = mock.Mock() + self.get_image.resource_class = fakes.FakeResource(None, {}) self.auth_token = kwargs['token'] self.management_url = kwargs['endpoint'] self.version = 2.0 @@ -197,8 +203,8 @@ class FakeImage(object): image_info = { 'id': str(uuid.uuid4()), 'name': 'image-name' + uuid.uuid4().hex, - 'owner': 'image-owner' + uuid.uuid4().hex, - 'protected': bool(random.choice([0, 1])), + 'owner_id': 'image-owner' + uuid.uuid4().hex, + 'is_protected': bool(random.choice([0, 1])), 'visibility': random.choice(['public', 'private']), 'tags': [uuid.uuid4().hex for r in range(2)], } @@ -206,13 +212,7 @@ class FakeImage(object): # Overwrite default attributes if there are some attributes set image_info.update(attrs) - # Set up the schema - model = warlock.model_factory( - IMAGE_schema, - schemas.SchemaBasedModel, - ) - - return model(**image_info) + return image.Image(**image_info) @staticmethod def create_images(attrs=None, count=2): @@ -307,6 +307,8 @@ class FakeImage(object): # Overwrite default attributes if there are some attributes set image_member_info.update(attrs) + return member.Member(**image_member_info) + image_member = fakes.FakeModel( copy.deepcopy(image_member_info)) diff --git a/openstackclient/tests/unit/image/v2/test_image.py b/openstackclient/tests/unit/image/v2/test_image.py index 78d857e2..a021cfc7 100644 --- a/openstackclient/tests/unit/image/v2/test_image.py +++ b/openstackclient/tests/unit/image/v2/test_image.py @@ -14,13 +14,14 @@ # import copy +import io +import os +import tempfile from unittest import mock -from glanceclient.common import utils as glanceclient_utils -from glanceclient.v2 import schemas +from openstack import exceptions as sdk_exceptions from osc_lib.cli import format_columns from osc_lib import exceptions -import warlock from openstackclient.image.v2 import image from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes @@ -33,12 +34,16 @@ class TestImage(image_fakes.TestImagev2): super(TestImage, self).setUp() # Get shortcuts to the Mocks in image client - self.images_mock = self.app.client_manager.image.images - self.images_mock.reset_mock() + # SDK proxy mock + self.app.client_manager.image = mock.Mock() + self.client = self.app.client_manager.image + + self.client.remove_member = mock.Mock() + + self.client.create_image = mock.Mock() + self.client.update_image = mock.Mock() self.image_members_mock = self.app.client_manager.image.image_members - self.image_members_mock.reset_mock() self.image_tags_mock = self.app.client_manager.image.image_tags - self.image_tags_mock.reset_mock() # Get shortcut to the Mocks in identity client self.project_mock = self.app.client_manager.identity.projects @@ -49,9 +54,6 @@ class TestImage(image_fakes.TestImagev2): def setup_images_mock(self, count): images = image_fakes.FakeImage.create_images(count=count) - self.images_mock.get = image_fakes.FakeImage.get_images( - images, - 0) return images @@ -64,26 +66,22 @@ class TestImageCreate(TestImage): super(TestImageCreate, self).setUp() self.new_image = image_fakes.FakeImage.create_one_image() - self.images_mock.create.return_value = self.new_image + self.client.create_image.return_value = self.new_image self.project_mock.get.return_value = self.project self.domain_mock.get.return_value = self.domain - # This is the return value for utils.find_resource() - self.images_mock.get.return_value = copy.deepcopy( - self.new_image - ) - self.images_mock.update.return_value = self.new_image + self.client.update_image.return_value = self.new_image + + (self.expected_columns, self.expected_data) = zip( + *sorted(image._format_image(self.new_image).items())) # Get the command object to test self.cmd = image.CreateImage(self.app, None) - def test_image_reserve_no_options(self): - mock_exception = { - 'find.side_effect': exceptions.CommandError('x'), - } - self.images_mock.configure_mock(**mock_exception) + @mock.patch("sys.stdin", side_effect=[None]) + def test_image_reserve_no_options(self, raw_input): arglist = [ self.new_image.name ] @@ -100,45 +98,34 @@ class TestImageCreate(TestImage): columns, data = self.cmd.take_action(parsed_args) # ImageManager.create(name=, **) - self.images_mock.create.assert_called_with( + self.client.create_image.assert_called_with( name=self.new_image.name, container_format=image.DEFAULT_CONTAINER_FORMAT, disk_format=image.DEFAULT_DISK_FORMAT, ) # Verify update() was not called, if it was show the args - self.assertEqual(self.images_mock.update.call_args_list, []) - - self.images_mock.upload.assert_called_with( - mock.ANY, mock.ANY, - ) + self.assertEqual(self.client.update_image.call_args_list, []) self.assertEqual( - image_fakes.FakeImage.get_image_columns(self.new_image), + self.expected_columns, columns) self.assertItemEqual( - image_fakes.FakeImage.get_image_data(self.new_image), + self.expected_data, data) - @mock.patch('glanceclient.common.utils.get_data_file', name='Open') - def test_image_reserve_options(self, mock_open): - mock_file = mock.MagicMock(name='File') - mock_open.return_value = mock_file - mock_open.read.return_value = None - mock_exception = { - 'find.side_effect': exceptions.CommandError('x'), - } - self.images_mock.configure_mock(**mock_exception) + @mock.patch('sys.stdin', side_effect=[None]) + def test_image_reserve_options(self, raw_input): arglist = [ '--container-format', 'ovf', '--disk-format', 'ami', '--min-disk', '10', '--min-ram', '4', ('--protected' - if self.new_image.protected else '--unprotected'), + if self.new_image.is_protected else '--unprotected'), ('--private' if self.new_image.visibility == 'private' else '--public'), - '--project', self.new_image.owner, + '--project', self.new_image.owner_id, '--project-domain', self.domain.id, self.new_image.name, ] @@ -147,11 +134,11 @@ class TestImageCreate(TestImage): ('disk_format', 'ami'), ('min_disk', 10), ('min_ram', 4), - ('protected', self.new_image.protected), - ('unprotected', not self.new_image.protected), + ('protected', self.new_image.is_protected), + ('unprotected', not self.new_image.is_protected), ('public', self.new_image.visibility == 'public'), ('private', self.new_image.visibility == 'private'), - ('project', self.new_image.owner), + ('project', self.new_image.owner_id), ('project_domain', self.domain.id), ('name', self.new_image.name), ] @@ -163,29 +150,22 @@ class TestImageCreate(TestImage): columns, data = self.cmd.take_action(parsed_args) # ImageManager.create(name=, **) - self.images_mock.create.assert_called_with( + self.client.create_image.assert_called_with( name=self.new_image.name, container_format='ovf', disk_format='ami', min_disk=10, min_ram=4, - owner=self.project.id, - protected=self.new_image.protected, + owner_id=self.project.id, + is_protected=self.new_image.is_protected, visibility=self.new_image.visibility, ) - # Verify update() was not called, if it was show the args - self.assertEqual(self.images_mock.update.call_args_list, []) - - self.images_mock.upload.assert_called_with( - mock.ANY, mock.ANY, - ) - self.assertEqual( - image_fakes.FakeImage.get_image_columns(self.new_image), + self.expected_columns, columns) self.assertItemEqual( - image_fakes.FakeImage.get_image_data(self.new_image), + self.expected_data, data) def test_image_create_with_unexist_project(self): @@ -222,21 +202,15 @@ class TestImageCreate(TestImage): parsed_args, ) - @mock.patch('glanceclient.common.utils.get_data_file', name='Open') - def test_image_create_file(self, mock_open): - mock_file = mock.MagicMock(name='File') - mock_open.return_value = mock_file - mock_open.read.return_value = ( - image_fakes.FakeImage.get_image_data(self.new_image)) - mock_exception = { - 'find.side_effect': exceptions.CommandError('x'), - } - self.images_mock.configure_mock(**mock_exception) + def test_image_create_file(self): + imagefile = tempfile.NamedTemporaryFile(delete=False) + imagefile.write(b'\0') + imagefile.close() arglist = [ - '--file', 'filer', + '--file', imagefile.name, ('--unprotected' - if not self.new_image.protected else '--protected'), + if not self.new_image.is_protected else '--protected'), ('--public' if self.new_image.visibility == 'public' else '--private'), '--property', 'Alpha=1', @@ -246,9 +220,9 @@ class TestImageCreate(TestImage): self.new_image.name, ] verifylist = [ - ('file', 'filer'), - ('protected', self.new_image.protected), - ('unprotected', not self.new_image.protected), + ('file', imagefile.name), + ('protected', self.new_image.is_protected), + ('unprotected', not self.new_image.is_protected), ('public', self.new_image.visibility == 'public'), ('private', self.new_image.visibility == 'private'), ('properties', {'Alpha': '1', 'Beta': '2'}), @@ -263,29 +237,23 @@ class TestImageCreate(TestImage): columns, data = self.cmd.take_action(parsed_args) # ImageManager.create(name=, **) - self.images_mock.create.assert_called_with( + self.client.create_image.assert_called_with( name=self.new_image.name, container_format=image.DEFAULT_CONTAINER_FORMAT, disk_format=image.DEFAULT_DISK_FORMAT, - protected=self.new_image.protected, + is_protected=self.new_image.is_protected, visibility=self.new_image.visibility, Alpha='1', Beta='2', tags=self.new_image.tags, - ) - - # Verify update() was not called, if it was show the args - self.assertEqual(self.images_mock.update.call_args_list, []) - - self.images_mock.upload.assert_called_with( - mock.ANY, mock.ANY, + filename=imagefile.name ) self.assertEqual( - image_fakes.FakeImage.get_image_columns(self.new_image), + self.expected_columns, columns) self.assertItemEqual( - image_fakes.FakeImage.get_image_data(self.new_image), + self.expected_data, data) def test_image_create_dead_options(self): @@ -315,25 +283,31 @@ class TestAddProjectToImage(TestImage): ) columns = ( + 'created_at', 'image_id', 'member_id', + 'schema', 'status', + 'updated_at' ) datalist = ( + new_member.created_at, _image.id, new_member.member_id, + new_member.schema, new_member.status, + new_member.updated_at ) def setUp(self): super(TestAddProjectToImage, self).setUp() # This is the return value for utils.find_resource() - self.images_mock.get.return_value = self._image + self.client.find_image.return_value = self._image # Update the image_id in the MEMBER dict - self.image_members_mock.create.return_value = self.new_member + self.client.add_member.return_value = self.new_member self.project_mock.get.return_value = self.project self.domain_mock.get.return_value = self.domain # Get the command object to test @@ -354,9 +328,9 @@ class TestAddProjectToImage(TestImage): # returns a two-part tuple with a tuple of column names and a tuple of # data to be shown. columns, data = self.cmd.take_action(parsed_args) - self.image_members_mock.create.assert_called_with( - self._image.id, - self.project.id + self.client.add_member.assert_called_with( + image=self._image.id, + member_id=self.project.id ) self.assertEqual(self.columns, columns) @@ -379,10 +353,11 @@ class TestAddProjectToImage(TestImage): # returns a two-part tuple with a tuple of column names and a tuple of # data to be shown. columns, data = self.cmd.take_action(parsed_args) - self.image_members_mock.create.assert_called_with( - self._image.id, - self.project.id + self.client.add_member.assert_called_with( + image=self._image.id, + member_id=self.project.id ) + self.assertEqual(self.columns, columns) self.assertEqual(self.datalist, data) @@ -392,7 +367,7 @@ class TestImageDelete(TestImage): def setUp(self): super(TestImageDelete, self).setUp() - self.images_mock.delete.return_value = None + self.client.delete_image.return_value = None # Get the command object to test self.cmd = image.DeleteImage(self.app, None) @@ -408,9 +383,11 @@ class TestImageDelete(TestImage): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.client.find_image.side_effect = images + result = self.cmd.take_action(parsed_args) - self.images_mock.delete.assert_called_with(images[0].id) + self.client.delete_image.assert_called_with(images[0].id) self.assertIsNone(result) def test_image_delete_multi_images(self): @@ -422,10 +399,12 @@ class TestImageDelete(TestImage): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.client.find_image.side_effect = images + result = self.cmd.take_action(parsed_args) calls = [mock.call(i.id) for i in images] - self.images_mock.delete.assert_has_calls(calls) + self.client.delete_image.assert_has_calls(calls) self.assertIsNone(result) def test_image_delete_multi_images_exception(self): @@ -449,15 +428,15 @@ class TestImageDelete(TestImage): ret_find = [ images[0], images[1], - exceptions.NotFound('404'), + sdk_exceptions.ResourceNotFound() ] - self.images_mock.get = Exception() - self.images_mock.find.side_effect = ret_find + self.client.find_image.side_effect = ret_find + self.assertRaises(exceptions.CommandError, self.cmd.take_action, parsed_args) calls = [mock.call(i.id) for i in images] - self.images_mock.delete.assert_has_calls(calls) + self.client.delete_image.assert_has_calls(calls) class TestImageList(TestImage): @@ -473,17 +452,17 @@ class TestImageList(TestImage): datalist = ( _image.id, _image.name, - '', + None, ), def setUp(self): super(TestImageList, self).setUp() self.api_mock = mock.Mock() - self.api_mock.image_list.side_effect = [ + self.api_mock.side_effect = [ [self._image], [], ] - self.app.client_manager.image.api = self.api_mock + self.client.images = self.api_mock # Get the command object to test self.cmd = image.ListImage(self.app, None) @@ -503,8 +482,8 @@ 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( - marker=self._image.id, + self.client.images.assert_called_with( + # marker=self._image.id, ) self.assertEqual(self.columns, columns) @@ -527,9 +506,8 @@ 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( - public=True, - marker=self._image.id, + self.client.images.assert_called_with( + visibility='public', ) self.assertEqual(self.columns, columns) @@ -552,9 +530,8 @@ 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( - private=True, - marker=self._image.id, + self.client.images.assert_called_with( + visibility='private', ) self.assertEqual(self.columns, columns) @@ -577,9 +554,8 @@ 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( - community=True, - marker=self._image.id, + self.client.images.assert_called_with( + visibility='community', ) self.assertEqual(self.columns, columns) @@ -602,9 +578,8 @@ 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( - shared=True, - marker=self._image.id, + self.client.images.assert_called_with( + visibility='shared', ) self.assertEqual(self.columns, columns) @@ -629,10 +604,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( - shared=True, + self.client.images.assert_called_with( + visibility='shared', member_status='all', - marker=self._image.id, ) self.assertEqual(self.columns, columns) @@ -666,8 +640,7 @@ 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( - marker=self._image.id, + self.client.images.assert_called_with( ) collist = ( @@ -688,14 +661,14 @@ class TestImageList(TestImage): datalist = (( self._image.id, self._image.name, - '', - '', - '', - '', - '', + None, + None, + None, + None, + None, self._image.visibility, - self._image.protected, - self._image.owner, + self._image.is_protected, + self._image.owner_id, format_columns.ListColumn(self._image.tags), ), ) self.assertListItemEqual(datalist, tuple(data)) @@ -716,8 +689,7 @@ 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( - marker=self._image.id, + self.client.images.assert_called_with( ) sf_mock.assert_called_with( [self._image], @@ -741,8 +713,7 @@ 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( - marker=self._image.id, + self.client.images.assert_called_with( ) si_mock.assert_called_with( [self._image], @@ -763,8 +734,10 @@ class TestImageList(TestImage): parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.api_mock.image_list.assert_called_with( - limit=ret_limit, marker=None + self.client.images.assert_called_with( + limit=ret_limit, + paginated=False + # marker=None ) self.assertEqual(self.columns, columns) @@ -775,8 +748,9 @@ class TestImageList(TestImage): # tangchen: Since image_fakes.IMAGE is a dict, it cannot offer a .id # operation. Will fix this by using FakeImage class instead # of IMAGE dict. - fr_mock.return_value = mock.Mock() - fr_mock.return_value.id = image_fakes.image_id + self.client.find_image = mock.Mock(return_value=self._image) +# fr_mock.return_value = mock.Mock() +# fr_mock.return_value.id = image_fakes.image_id arglist = [ '--marker', image_fakes.image_name, @@ -787,10 +761,12 @@ class TestImageList(TestImage): parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.api_mock.image_list.assert_called_with( - marker=image_fakes.image_id, + self.client.images.assert_called_with( + marker=self._image.id, ) + self.client.find_image.assert_called_with(image_fakes.image_name) + def test_image_list_name_option(self): arglist = [ '--name', 'abc', @@ -801,8 +777,9 @@ class TestImageList(TestImage): parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.api_mock.image_list.assert_called_with( - name='abc', marker=self._image.id + self.client.images.assert_called_with( + name='abc', + # marker=self._image.id ) def test_image_list_status_option(self): @@ -815,8 +792,8 @@ class TestImageList(TestImage): parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.api_mock.image_list.assert_called_with( - status='active', marker=self._image.id + self.client.images.assert_called_with( + status='active' ) def test_image_list_tag_option(self): @@ -829,8 +806,8 @@ class TestImageList(TestImage): parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.api_mock.image_list.assert_called_with( - tag='abc', marker=self._image.id + self.client.images.assert_called_with( + tag='abc' ) @@ -849,17 +826,17 @@ class TestListImageProjects(TestImage): "Status" ) - datalist = (( + datalist = [( _image.id, member.member_id, member.status, - )) + )] def setUp(self): super(TestListImageProjects, self).setUp() - self.images_mock.get.return_value = self._image - self.image_members_mock.list.return_value = self.datalist + self.client.find_image.return_value = self._image + self.client.members.return_value = [self.member] self.cmd = image.ListImageProjects(self.app, None) @@ -874,10 +851,10 @@ class TestListImageProjects(TestImage): columns, data = self.cmd.take_action(parsed_args) - self.image_members_mock.list.assert_called_with(self._image.id) + self.client.members.assert_called_with(image=self._image.id) self.assertEqual(self.columns, columns) - self.assertEqual(len(self.datalist), len(tuple(data))) + self.assertEqual(self.datalist, list(data)) class TestRemoveProjectImage(TestImage): @@ -890,11 +867,11 @@ class TestRemoveProjectImage(TestImage): self._image = image_fakes.FakeImage.create_one_image() # This is the return value for utils.find_resource() - self.images_mock.get.return_value = self._image + self.client.find_image.return_value = self._image self.project_mock.get.return_value = self.project self.domain_mock.get.return_value = self.domain - self.image_members_mock.delete.return_value = None + self.client.remove_member.return_value = None # Get the command object to test self.cmd = image.RemoveProjectImage(self.app, None) @@ -911,9 +888,13 @@ class TestRemoveProjectImage(TestImage): result = self.cmd.take_action(parsed_args) - self.image_members_mock.delete.assert_called_with( + self.client.find_image.assert_called_with( self._image.id, - self.project.id, + ignore_missing=False) + + self.client.remove_member.assert_called_with( + member=self.project.id, + image=self._image.id, ) self.assertIsNone(result) @@ -932,9 +913,9 @@ class TestRemoveProjectImage(TestImage): result = self.cmd.take_action(parsed_args) - self.image_members_mock.delete.assert_called_with( - self._image.id, - self.project.id, + self.client.remove_member.assert_called_with( + member=self.project.id, + image=self._image.id, ) self.assertIsNone(result) @@ -943,21 +924,16 @@ class TestImageSet(TestImage): project = identity_fakes.FakeProject.create_one_project() domain = identity_fakes.FakeDomain.create_one_domain() + _image = image_fakes.FakeImage.create_one_image({'tags': []}) def setUp(self): super(TestImageSet, self).setUp() - # Set up the schema - self.model = warlock.model_factory( - image_fakes.IMAGE_schema, - schemas.SchemaBasedModel, - ) self.project_mock.get.return_value = self.project self.domain_mock.get.return_value = self.domain - self.images_mock.get.return_value = self.model(**image_fakes.IMAGE) - self.images_mock.update.return_value = self.model(**image_fakes.IMAGE) + self.client.find_image.return_value = self._image self.app.client_manager.auth_ref = mock.Mock( project_id=self.project.id, @@ -986,38 +962,38 @@ class TestImageSet(TestImage): attrs={'image_id': image_fakes.image_id, 'member_id': self.project.id} ) - self.image_members_mock.update.return_value = membership + self.client.update_member.return_value = membership arglist = [ '--accept', - image_fakes.image_id, + self._image.id, ] verifylist = [ ('accept', True), ('reject', False), ('pending', False), - ('image', image_fakes.image_id) + ('image', self._image.id) ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) self.cmd.take_action(parsed_args) - self.image_members_mock.update.assert_called_once_with( - image_fakes.image_id, - self.app.client_manager.auth_ref.project_id, - 'accepted', + self.client.update_member.assert_called_once_with( + image=self._image.id, + member=self.app.client_manager.auth_ref.project_id, + status='accepted', ) # Assert that the 'update image" route is also called, in addition to # the 'update membership' route. - self.images_mock.update.assert_called_with(image_fakes.image_id) + self.client.update_image.assert_called_with(self._image.id) def test_image_set_membership_option_reject(self): membership = image_fakes.FakeImage.create_one_image_member( attrs={'image_id': image_fakes.image_id, 'member_id': self.project.id} ) - self.image_members_mock.update.return_value = membership + self.client.update_member.return_value = membership arglist = [ '--reject', @@ -1033,22 +1009,22 @@ class TestImageSet(TestImage): parsed_args = self.check_parser(self.cmd, arglist, verifylist) self.cmd.take_action(parsed_args) - self.image_members_mock.update.assert_called_once_with( - image_fakes.image_id, - self.app.client_manager.auth_ref.project_id, - 'rejected', + self.client.update_member.assert_called_once_with( + image=self._image.id, + member=self.app.client_manager.auth_ref.project_id, + status='rejected', ) # Assert that the 'update image" route is also called, in addition to # the 'update membership' route. - self.images_mock.update.assert_called_with(image_fakes.image_id) + self.client.update_image.assert_called_with(self._image.id) def test_image_set_membership_option_pending(self): membership = image_fakes.FakeImage.create_one_image_member( attrs={'image_id': image_fakes.image_id, 'member_id': self.project.id} ) - self.image_members_mock.update.return_value = membership + self.client.update_member.return_value = membership arglist = [ '--pending', @@ -1064,15 +1040,15 @@ class TestImageSet(TestImage): parsed_args = self.check_parser(self.cmd, arglist, verifylist) self.cmd.take_action(parsed_args) - self.image_members_mock.update.assert_called_once_with( - image_fakes.image_id, - self.app.client_manager.auth_ref.project_id, - 'pending', + self.client.update_member.assert_called_once_with( + image=self._image.id, + member=self.app.client_manager.auth_ref.project_id, + status='pending', ) # Assert that the 'update image" route is also called, in addition to # the 'update membership' route. - self.images_mock.update.assert_called_with(image_fakes.image_id) + self.client.update_image.assert_called_with(self._image.id) def test_image_set_options(self): arglist = [ @@ -1083,7 +1059,7 @@ class TestImageSet(TestImage): '--disk-format', 'vmdk', '--project', self.project.name, '--project-domain', self.domain.id, - image_fakes.image_id, + self._image.id, ] verifylist = [ ('name', 'new-name'), @@ -1093,7 +1069,7 @@ class TestImageSet(TestImage): ('disk_format', 'vmdk'), ('project', self.project.name), ('project_domain', self.domain.id), - ('image', image_fakes.image_id), + ('image', self._image.id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -1101,15 +1077,15 @@ class TestImageSet(TestImage): kwargs = { 'name': 'new-name', - 'owner': self.project.id, + 'owner_id': self.project.id, 'min_disk': 2, 'min_ram': 4, 'container_format': 'ovf', 'disk_format': 'vmdk', } # ImageManager.update(image, **kwargs) - self.images_mock.update.assert_called_with( - image_fakes.image_id, **kwargs) + self.client.update_image.assert_called_with( + self._image.id, **kwargs) self.assertIsNone(result) def test_image_set_with_unexist_project(self): @@ -1148,12 +1124,12 @@ class TestImageSet(TestImage): result = self.cmd.take_action(parsed_args) kwargs = { - 'protected': True, + 'is_protected': True, 'visibility': 'private', } # ImageManager.update(image, **kwargs) - self.images_mock.update.assert_called_with( - image_fakes.image_id, + self.client.update_image.assert_called_with( + self._image.id, **kwargs ) self.assertIsNone(result) @@ -1176,12 +1152,12 @@ class TestImageSet(TestImage): result = self.cmd.take_action(parsed_args) kwargs = { - 'protected': False, + 'is_protected': False, 'visibility': 'public', } # ImageManager.update(image, **kwargs) - self.images_mock.update.assert_called_with( - image_fakes.image_id, + self.client.update_image.assert_called_with( + self._image.id, **kwargs ) self.assertIsNone(result) @@ -1205,8 +1181,8 @@ class TestImageSet(TestImage): 'Beta': '2', } # ImageManager.update(image, **kwargs) - self.images_mock.update.assert_called_with( - image_fakes.image_id, + self.client.update_image.assert_called_with( + self._image.id, **kwargs ) self.assertIsNone(result) @@ -1243,8 +1219,8 @@ class TestImageSet(TestImage): 'ramdisk_id': 'xyzpdq', } # ImageManager.update(image, **kwargs) - self.images_mock.update.assert_called_with( - image_fakes.image_id, + self.client.update_image.assert_called_with( + self._image.id, **kwargs ) self.assertIsNone(result) @@ -1266,8 +1242,8 @@ class TestImageSet(TestImage): 'tags': ['test-tag'], } # ImageManager.update(image, **kwargs) - self.images_mock.update.assert_called_with( - image_fakes.image_id, + self.client.update_image.assert_called_with( + self._image.id, **kwargs ) self.assertIsNone(result) @@ -1290,12 +1266,12 @@ class TestImageSet(TestImage): 'tags': ['test-tag'], } - self.images_mock.reactivate.assert_called_with( - image_fakes.image_id, + self.client.reactivate_image.assert_called_with( + self._image.id, ) # ImageManager.update(image, **kwargs) - self.images_mock.update.assert_called_with( - image_fakes.image_id, + self.client.update_image.assert_called_with( + self._image.id, **kwargs ) self.assertIsNone(result) @@ -1318,20 +1294,20 @@ class TestImageSet(TestImage): 'tags': ['test-tag'], } - self.images_mock.deactivate.assert_called_with( - image_fakes.image_id, + self.client.deactivate_image.assert_called_with( + self._image.id, ) # ImageManager.update(image, **kwargs) - self.images_mock.update.assert_called_with( - image_fakes.image_id, + self.client.update_image.assert_called_with( + self._image.id, **kwargs ) self.assertIsNone(result) def test_image_set_tag_merge(self): - old_image = copy.copy(image_fakes.IMAGE) + old_image = self._image old_image['tags'] = ['old1', 'new2'] - self.images_mock.get.return_value = self.model(**old_image) + self.client.find_image.return_value = old_image arglist = [ '--tag', 'test-tag', image_fakes.image_name, @@ -1348,16 +1324,16 @@ class TestImageSet(TestImage): 'tags': ['old1', 'new2', 'test-tag'], } # ImageManager.update(image, **kwargs) - a, k = self.images_mock.update.call_args - self.assertEqual(image_fakes.image_id, a[0]) + a, k = self.client.update_image.call_args + self.assertEqual(self._image.id, a[0]) self.assertIn('tags', k) self.assertEqual(set(kwargs['tags']), set(k['tags'])) self.assertIsNone(result) def test_image_set_tag_merge_dupe(self): - old_image = copy.copy(image_fakes.IMAGE) + old_image = self._image old_image['tags'] = ['old1', 'new2'] - self.images_mock.get.return_value = self.model(**old_image) + self.client.find_image.return_value = old_image arglist = [ '--tag', 'old1', image_fakes.image_name, @@ -1374,8 +1350,8 @@ class TestImageSet(TestImage): 'tags': ['new2', 'old1'], } # ImageManager.update(image, **kwargs) - a, k = self.images_mock.update.call_args - self.assertEqual(image_fakes.image_id, a[0]) + a, k = self.client.update_image.call_args + self.assertEqual(self._image.id, a[0]) self.assertIn('tags', k) self.assertEqual(set(kwargs['tags']), set(k['tags'])) self.assertIsNone(result) @@ -1416,8 +1392,8 @@ class TestImageSet(TestImage): 'min_ram': 0, } # ImageManager.update(image, **kwargs) - self.images_mock.update.assert_called_with( - image_fakes.image_id, + self.client.update_image.assert_called_with( + self._image.id, **kwargs ) self.assertIsNone(result) @@ -1428,16 +1404,25 @@ class TestImageShow(TestImage): new_image = image_fakes.FakeImage.create_one_image( attrs={'size': 1000}) + _data = image_fakes.FakeImage.create_one_image() + + columns = ( + 'id', 'name', 'owner', 'protected', 'tags', 'visibility' + ) + + data = ( + _data.id, + _data.name, + _data.owner_id, + _data.is_protected, + format_columns.ListColumn(_data.tags), + _data.visibility + ) + def setUp(self): super(TestImageShow, self).setUp() - # Set up the schema - self.model = warlock.model_factory( - image_fakes.IMAGE_schema, - schemas.SchemaBasedModel, - ) - - self.images_mock.get.return_value = self.model(**image_fakes.IMAGE) + self.client.find_image = mock.Mock(return_value=self._data) # Get the command object to test self.cmd = image.ShowImage(self.app, None) @@ -1455,15 +1440,16 @@ class TestImageShow(TestImage): # returns a two-part tuple with a tuple of column names and a tuple of # data to be shown. columns, data = self.cmd.take_action(parsed_args) - self.images_mock.get.assert_called_with( + self.client.find_image.assert_called_with( image_fakes.image_id, + ignore_missing=False ) - self.assertEqual(image_fakes.IMAGE_columns, columns) - self.assertItemEqual(image_fakes.IMAGE_SHOW_data, data) + self.assertEqual(self.columns, columns) + self.assertItemEqual(self.data, data) def test_image_show_human_readable(self): - self.images_mock.get.return_value = self.new_image + self.client.find_image.return_value = self.new_image arglist = [ '--human-readable', self.new_image.id, @@ -1478,8 +1464,9 @@ class TestImageShow(TestImage): # returns a two-part tuple with a tuple of column names and a tuple of # data to be shown. columns, data = self.cmd.take_action(parsed_args) - self.images_mock.get.assert_called_with( + self.client.find_image.assert_called_with( self.new_image.id, + ignore_missing=False ) size_index = columns.index('size') @@ -1491,19 +1478,15 @@ class TestImageUnset(TestImage): attrs = {} attrs['tags'] = ['test'] attrs['prop'] = 'test' + attrs['prop2'] = 'fake' image = image_fakes.FakeImage.create_one_image(attrs) def setUp(self): super(TestImageUnset, self).setUp() - # Set up the schema - self.model = warlock.model_factory( - image_fakes.IMAGE_schema, - schemas.SchemaBasedModel, - ) - - self.images_mock.get.return_value = self.image - self.image_tags_mock.delete.return_value = self.image + self.client.find_image.return_value = self.image + self.client.remove_tag.return_value = self.image + self.client.update_image.return_value = self.image # Get the command object to test self.cmd = image.UnsetImage(self.app, None) @@ -1535,7 +1518,7 @@ class TestImageUnset(TestImage): parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) - self.image_tags_mock.delete.assert_called_with( + self.client.remove_tag.assert_called_with( self.image.id, 'test' ) self.assertIsNone(result) @@ -1555,9 +1538,9 @@ class TestImageUnset(TestImage): result = self.cmd.take_action(parsed_args) kwargs = {} - self.images_mock.update.assert_called_with( - self.image.id, - parsed_args.properties, + self.client.update_image.assert_called_with( + self.image, + properties={'prop2': 'fake'}, **kwargs) self.assertIsNone(result) @@ -1579,12 +1562,12 @@ class TestImageUnset(TestImage): result = self.cmd.take_action(parsed_args) kwargs = {} - self.images_mock.update.assert_called_with( - self.image.id, - parsed_args.properties, + self.client.update_image.assert_called_with( + self.image, + properties={'prop2': 'fake'}, **kwargs) - self.image_tags_mock.delete.assert_called_with( + self.client.remove_tag.assert_called_with( self.image.id, 'test' ) self.assertIsNone(result) @@ -1597,18 +1580,13 @@ class TestImageSave(TestImage): def setUp(self): super(TestImageSave, self).setUp() - # Generate a request id - self.resp = mock.MagicMock() - self.resp.headers['x-openstack-request-id'] = 'req_id' + self.client.find_image.return_value = self.image + self.client.download_image.return_value = self.image # Get the command object to test self.cmd = image.SaveImage(self.app, None) def test_save_data(self): - req_id_proxy = glanceclient_utils.RequestIdProxy( - ['some_data', self.resp] - ) - self.images_mock.data.return_value = req_id_proxy arglist = ['--file', '/path/to/file', self.image.id] @@ -1618,23 +1596,58 @@ class TestImageSave(TestImage): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - with mock.patch('glanceclient.common.utils.save_image') as mocked_save: - self.cmd.take_action(parsed_args) - mocked_save.assert_called_once_with(req_id_proxy, '/path/to/file') + self.cmd.take_action(parsed_args) - def test_save_no_data(self): - req_id_proxy = glanceclient_utils.RequestIdProxy( - [None, self.resp] - ) - self.images_mock.data.return_value = req_id_proxy + self.client.download_image.assert_called_once_with( + self.image.id, + output='/path/to/file') - arglist = ['--file', '/path/to/file', self.image.id] - verifylist = [ - ('file', '/path/to/file'), - ('image', self.image.id) - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) +class TestImageGetData(TestImage): + + def setUp(self): + super(TestImageGetData, self).setUp() + self.args = mock.Mock() + + def test_get_data_file_file(self): + (fd, fname) = tempfile.mkstemp(prefix='osc_test_image') + self.args.file = fname + + (test_fd, test_name) = image.get_data_file(self.args) + + self.assertEqual(fname, test_name) + test_fd.close() + + os.unlink(fname) + + def test_get_data_file_2(self): + + self.args.file = None + + f = io.BytesIO(b"some initial binary data: \x00\x01") + + with mock.patch('sys.stdin') as stdin: + stdin.return_value = f + stdin.isatty.return_value = False + stdin.buffer = f + + (test_fd, test_name) = image.get_data_file(self.args) + + # Ensure data written to temp file is correct + self.assertEqual(f, test_fd) + self.assertIsNone(test_name) + + def test_get_data_file_3(self): + + self.args.file = None + + f = io.BytesIO(b"some initial binary data: \x00\x01") + + with mock.patch('sys.stdin') as stdin: + # There is stdin, but interactive + stdin.return_value = f + + (test_fd, test_fname) = image.get_data_file(self.args) - # Raise SystemExit if no data was provided. - self.assertRaises(SystemExit, self.cmd.take_action, parsed_args) + self.assertIsNone(test_fd) + self.assertIsNone(test_fname) diff --git a/openstackclient/tests/unit/integ/cli/test_shell.py b/openstackclient/tests/unit/integ/cli/test_shell.py index 0c98a129..5788b473 100644 --- a/openstackclient/tests/unit/integ/cli/test_shell.py +++ b/openstackclient/tests/unit/integ/cli/test_shell.py @@ -20,16 +20,6 @@ from openstackclient import shell from openstackclient.tests.unit.integ import base as test_base from openstackclient.tests.unit import test_shell -# NOTE(dtroyer): Attempt the import to detect if the SDK installed is new -# enough to contain the os_client_config code. If so, use -# that path for mocks. -CONFIG_MOCK_BASE = "openstack.config.loader" -try: - from openstack.config import defaults # noqa -except ImportError: - # Fall back to os-client-config - CONFIG_MOCK_BASE = "os_client_config.config" - class TestIntegShellCliNoAuth(test_base.TestInteg): @@ -455,8 +445,8 @@ class TestIntegShellCliPrecedenceOCC(test_base.TestInteg): temp_dir = self.useFixture(fixtures.TempDir()) return temp_dir.join(filename) - @mock.patch(CONFIG_MOCK_BASE + ".OpenStackConfig._load_vendor_file") - @mock.patch(CONFIG_MOCK_BASE + ".OpenStackConfig._load_config_file") + @mock.patch("openstack.config.loader.OpenStackConfig._load_vendor_file") + @mock.patch("openstack.config.loader.OpenStackConfig._load_config_file") def test_shell_args_precedence_1(self, config_mock, vendor_mock): """Precedence run 1 @@ -473,7 +463,6 @@ class TestIntegShellCliPrecedenceOCC(test_base.TestInteg): return ('file.yaml', copy.deepcopy(test_shell.PUBLIC_1)) vendor_mock.side_effect = vendor_mock_return - print("CONFIG_MOCK_BASE=%s" % CONFIG_MOCK_BASE) _shell = shell.OpenStackShell() _shell.run( "--os-password qaz extension list".split(), @@ -527,8 +516,8 @@ class TestIntegShellCliPrecedenceOCC(test_base.TestInteg): # +env, +cli, +occ # see test_shell_args_precedence_2() - @mock.patch(CONFIG_MOCK_BASE + ".OpenStackConfig._load_vendor_file") - @mock.patch(CONFIG_MOCK_BASE + ".OpenStackConfig._load_config_file") + @mock.patch("openstack.config.loader.OpenStackConfig._load_vendor_file") + @mock.patch("openstack.config.loader.OpenStackConfig._load_config_file") def test_shell_args_precedence_2(self, config_mock, vendor_mock): """Precedence run 2 @@ -545,7 +534,6 @@ class TestIntegShellCliPrecedenceOCC(test_base.TestInteg): return ('file.yaml', copy.deepcopy(test_shell.PUBLIC_1)) vendor_mock.side_effect = vendor_mock_return - print("CONFIG_MOCK_BASE=%s" % CONFIG_MOCK_BASE) _shell = shell.OpenStackShell() _shell.run( "--os-username zarquon --os-password qaz " diff --git a/openstackclient/tests/unit/network/v2/fakes.py b/openstackclient/tests/unit/network/v2/fakes.py index a553f501..cef0a11c 100644 --- a/openstackclient/tests/unit/network/v2/fakes.py +++ b/openstackclient/tests/unit/network/v2/fakes.py @@ -1227,6 +1227,7 @@ class FakeSecurityGroup(object): 'id': 'security-group-id-' + uuid.uuid4().hex, 'name': 'security-group-name-' + uuid.uuid4().hex, 'description': 'security-group-description-' + uuid.uuid4().hex, + 'stateful': True, 'project_id': 'project-id-' + uuid.uuid4().hex, 'security_group_rules': [], 'tags': [] @@ -1832,7 +1833,7 @@ class FakeFloatingIPPortForwarding(object): """ attrs = attrs or {} floatingip_id = ( - attrs.get('floatingip_id') or'floating-ip-id-' + uuid.uuid4().hex + attrs.get('floatingip_id') or 'floating-ip-id-' + uuid.uuid4().hex ) # Set default attributes. port_forwarding_attrs = { @@ -1843,6 +1844,7 @@ class FakeFloatingIPPortForwarding(object): 'internal_port': randint(1, 65535), 'external_port': randint(1, 65535), 'protocol': 'tcp', + 'description': 'some description', } # Overwrite default attributes. diff --git a/openstackclient/tests/unit/network/v2/test_floating_ip_network.py b/openstackclient/tests/unit/network/v2/test_floating_ip_network.py index a98051e7..dbcd5c97 100644 --- a/openstackclient/tests/unit/network/v2/test_floating_ip_network.py +++ b/openstackclient/tests/unit/network/v2/test_floating_ip_network.py @@ -776,6 +776,32 @@ class TestSetFloatingIP(TestFloatingIPNetwork): self.network.update_ip.assert_called_once_with( self.floating_ip, **attrs) + def test_description_option(self): + arglist = [ + self.floating_ip.id, + '--port', self.floating_ip.port_id, + '--description', self.floating_ip.description, + ] + verifylist = [ + ('floating_ip', self.floating_ip.id), + ('port', self.floating_ip.port_id), + ('description', self.floating_ip.description), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + attrs = { + 'port_id': self.floating_ip.port_id, + 'description': self.floating_ip.description, + } + self.network.find_ip.assert_called_once_with( + self.floating_ip.id, + ignore_missing=False, + ) + self.network.update_ip.assert_called_once_with( + self.floating_ip, **attrs) + def test_qos_policy_option(self): qos_policy = network_fakes.FakeNetworkQosPolicy.create_one_qos_policy() self.network.find_qos_policy = mock.Mock(return_value=qos_policy) diff --git a/openstackclient/tests/unit/network/v2/test_floating_ip_port_forwarding.py b/openstackclient/tests/unit/network/v2/test_floating_ip_port_forwarding.py index ea6cdd26..1028c18a 100644 --- a/openstackclient/tests/unit/network/v2/test_floating_ip_port_forwarding.py +++ b/openstackclient/tests/unit/network/v2/test_floating_ip_port_forwarding.py @@ -62,6 +62,7 @@ class TestCreateFloatingIPPortForwarding(TestFloatingIPPortForwarding): self.app, self.namespace) self.columns = ( + 'description', 'external_port', 'floatingip_id', 'id', @@ -73,6 +74,7 @@ class TestCreateFloatingIPPortForwarding(TestFloatingIPPortForwarding): ) self.data = ( + self.new_port_forwarding.description, self.new_port_forwarding.external_port, self.new_port_forwarding.floatingip_id, self.new_port_forwarding.id, @@ -102,6 +104,8 @@ class TestCreateFloatingIPPortForwarding(TestFloatingIPPortForwarding): self.new_port_forwarding.floatingip_id, '--internal-ip-address', self.new_port_forwarding.internal_ip_address, + '--description', + self.new_port_forwarding.description, ] verifylist = [ ('port', self.new_port_forwarding.internal_port_id), @@ -111,6 +115,7 @@ class TestCreateFloatingIPPortForwarding(TestFloatingIPPortForwarding): ('floating_ip', self.new_port_forwarding.floatingip_id), ('internal_ip_address', self.new_port_forwarding. internal_ip_address), + ('description', self.new_port_forwarding.description), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) @@ -126,6 +131,7 @@ class TestCreateFloatingIPPortForwarding(TestFloatingIPPortForwarding): 'internal_port_id': self.new_port_forwarding. internal_port_id, 'protocol': self.new_port_forwarding.protocol, + 'description': self.new_port_forwarding.description, }) self.assertEqual(self.columns, columns) self.assertEqual(self.data, data) @@ -251,7 +257,8 @@ class TestListFloatingIPPortForwarding(TestFloatingIPPortForwarding): 'Internal IP Address', 'Internal Port', 'External Port', - 'Protocol' + 'Protocol', + 'Description', ) def setUp(self): @@ -273,6 +280,7 @@ class TestListFloatingIPPortForwarding(TestFloatingIPPortForwarding): port_forwarding.internal_port, port_forwarding.external_port, port_forwarding.protocol, + port_forwarding.description, )) self.network.floating_ip_port_forwardings = mock.Mock( return_value=self.port_forwardings @@ -393,6 +401,7 @@ class TestSetFloatingIPPortForwarding(TestFloatingIPPortForwarding): '--internal-protocol-port', '100', '--external-protocol-port', '200', '--protocol', 'tcp', + '--description', 'some description', self._port_forwarding.floatingip_id, self._port_forwarding.id, ] @@ -402,6 +411,7 @@ class TestSetFloatingIPPortForwarding(TestFloatingIPPortForwarding): ('internal_protocol_port', 100), ('external_protocol_port', 200), ('protocol', 'tcp'), + ('description', 'some description'), ('floating_ip', self._port_forwarding.floatingip_id), ('port_forwarding_id', self._port_forwarding.id), ] @@ -415,6 +425,7 @@ class TestSetFloatingIPPortForwarding(TestFloatingIPPortForwarding): 'internal_port': 100, 'external_port': 200, 'protocol': 'tcp', + 'description': 'some description', } self.network.update_floating_ip_port_forwarding.assert_called_with( self._port_forwarding.floatingip_id, @@ -428,6 +439,7 @@ class TestShowFloatingIPPortForwarding(TestFloatingIPPortForwarding): # The port forwarding to show. columns = ( + 'description', 'external_port', 'floatingip_id', 'id', @@ -450,6 +462,7 @@ class TestShowFloatingIPPortForwarding(TestFloatingIPPortForwarding): ) ) self.data = ( + self._port_forwarding.description, self._port_forwarding.external_port, self._port_forwarding.floatingip_id, self._port_forwarding.id, diff --git a/openstackclient/tests/unit/network/v2/test_network.py b/openstackclient/tests/unit/network/v2/test_network.py index 45d6008b..5f8eed67 100644 --- a/openstackclient/tests/unit/network/v2/test_network.py +++ b/openstackclient/tests/unit/network/v2/test_network.py @@ -278,24 +278,6 @@ class TestCreateNetworkIdentityV3(TestNetwork): def test_create_with_no_tag(self): self._test_create_with_tag(add_tags=False) - def test_create_default_internal(self): - arglist = [ - self._network.name, - "--default", - ] - verifylist = [ - ('name', self._network.name), - ('enable', True), - ('share', None), - ('project', None), - ('external', False), - ('default', True), - ] - - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.assertRaises(exceptions.CommandError, self.cmd.take_action, - parsed_args) - class TestCreateNetworkIdentityV2(TestNetwork): @@ -1043,21 +1025,6 @@ class TestSetNetwork(TestNetwork): def test_set_with_no_tag(self): self._test_set_tags(with_tags=False) - def test_set_default_internal(self): - arglist = [ - self._network.name, - '--internal', - '--default', - ] - verifylist = [ - ('internal', True), - ('default', True), - ] - - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.assertRaises(exceptions.CommandError, self.cmd.take_action, - parsed_args) - class TestShowNetwork(TestNetwork): diff --git a/openstackclient/tests/unit/network/v2/test_network_rbac.py b/openstackclient/tests/unit/network/v2/test_network_rbac.py index 078188ce..d7c71ea7 100644 --- a/openstackclient/tests/unit/network/v2/test_network_rbac.py +++ b/openstackclient/tests/unit/network/v2/test_network_rbac.py @@ -14,6 +14,7 @@ from unittest import mock from unittest.mock import call +import ddt from osc_lib import exceptions from openstackclient.network.v2 import network_rbac @@ -33,11 +34,14 @@ class TestNetworkRBAC(network_fakes.TestNetworkV2): self.projects_mock = self.app.client_manager.identity.projects +@ddt.ddt class TestCreateNetworkRBAC(TestNetworkRBAC): network_object = network_fakes.FakeNetwork.create_one_network() qos_object = network_fakes.FakeNetworkQosPolicy.create_one_qos_policy() sg_object = network_fakes.FakeNetworkSecGroup.create_one_security_group() + as_object = network_fakes.FakeAddressScope.create_one_address_scope() + snp_object = network_fakes.FakeSubnetPool.create_one_subnet_pool() project = identity_fakes_v3.FakeProject.create_one_project() rbac_policy = network_fakes.FakeNetworkRBAC.create_one_network_rbac( attrs={'tenant_id': project.id, @@ -77,6 +81,10 @@ class TestCreateNetworkRBAC(TestNetworkRBAC): return_value=self.qos_object) self.network.find_security_group = mock.Mock( return_value=self.sg_object) + self.network.find_address_scope = mock.Mock( + return_value=self.as_object) + self.network.find_subnet_pool = mock.Mock( + return_value=self.snp_object) self.projects_mock.get.return_value = self.project def test_network_rbac_create_no_type(self): @@ -224,57 +232,29 @@ class TestCreateNetworkRBAC(TestNetworkRBAC): self.assertEqual(self.columns, columns) self.assertEqual(self.data, list(data)) - def test_network_rbac_create_qos_object(self): - self.rbac_policy.object_type = 'qos_policy' - self.rbac_policy.object_id = self.qos_object.id - arglist = [ - '--type', 'qos_policy', - '--action', self.rbac_policy.action, - '--target-project', self.rbac_policy.target_tenant, - self.qos_object.name, - ] - verifylist = [ - ('type', 'qos_policy'), - ('action', self.rbac_policy.action), - ('target_project', self.rbac_policy.target_tenant), - ('rbac_object', self.qos_object.name), - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - # DisplayCommandBase.take_action() returns two tuples - columns, data = self.cmd.take_action(parsed_args) - - self.network.create_rbac_policy.assert_called_with(**{ - 'object_id': self.qos_object.id, - 'object_type': 'qos_policy', - 'action': self.rbac_policy.action, - 'target_tenant': self.rbac_policy.target_tenant, - }) - self.data = [ - self.rbac_policy.action, - self.rbac_policy.id, - self.qos_object.id, - 'qos_policy', - self.rbac_policy.tenant_id, - self.rbac_policy.target_tenant, - ] - self.assertEqual(self.columns, columns) - self.assertEqual(self.data, list(data)) + @ddt.data( + ('qos_policy', "qos_object"), + ('security_group', "sg_object"), + ('subnetpool', "snp_object"), + ('address_scope', "as_object") + ) + @ddt.unpack + def test_network_rbac_create_object(self, obj_type, obj_fake_attr): + obj_fake = getattr(self, obj_fake_attr) - def test_network_rbac_create_security_group_object(self): - self.rbac_policy.object_type = 'security_group' - self.rbac_policy.object_id = self.sg_object.id + self.rbac_policy.object_type = obj_type + self.rbac_policy.object_id = obj_fake.id arglist = [ - '--type', 'security_group', + '--type', obj_type, '--action', self.rbac_policy.action, '--target-project', self.rbac_policy.target_tenant, - self.sg_object.name, + obj_fake.name, ] verifylist = [ - ('type', 'security_group'), + ('type', obj_type), ('action', self.rbac_policy.action), ('target_project', self.rbac_policy.target_tenant), - ('rbac_object', self.sg_object.name), + ('rbac_object', obj_fake.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -282,16 +262,16 @@ class TestCreateNetworkRBAC(TestNetworkRBAC): columns, data = self.cmd.take_action(parsed_args) self.network.create_rbac_policy.assert_called_with(**{ - 'object_id': self.sg_object.id, - 'object_type': 'security_group', + 'object_id': obj_fake.id, + 'object_type': obj_type, 'action': self.rbac_policy.action, 'target_tenant': self.rbac_policy.target_tenant, }) self.data = [ self.rbac_policy.action, self.rbac_policy.id, - self.sg_object.id, - 'security_group', + obj_fake.id, + obj_type, self.rbac_policy.tenant_id, self.rbac_policy.target_tenant, ] diff --git a/openstackclient/tests/unit/network/v2/test_network_segment_range.py b/openstackclient/tests/unit/network/v2/test_network_segment_range.py index 89a0c223..b60f1710 100644 --- a/openstackclient/tests/unit/network/v2/test_network_segment_range.py +++ b/openstackclient/tests/unit/network/v2/test_network_segment_range.py @@ -24,6 +24,20 @@ from openstackclient.tests.unit.network.v2 import fakes as network_fakes from openstackclient.tests.unit import utils as tests_utils +class TestAuxiliaryFunctions(tests_utils.TestCase): + + def test__get_ranges(self): + input_reference = [ + ([1, 2, 3, 4, 5, 6, 7], ['1-7']), + ([1, 2, 5, 4, 3, 6, 7], ['1-7']), + ([1, 2, 4, 3, 7, 6], ['1-4', '6-7']), + ([1, 2, 4, 3, '13', 12, '7', '6'], ['1-4', '6-7', '12-13']) + ] + for input, reference in input_reference: + self.assertEqual(reference, + list(network_segment_range._get_ranges(input))) + + class TestNetworkSegmentRange(network_fakes.TestNetworkV2): def setUp(self): diff --git a/openstackclient/tests/unit/network/v2/test_port.py b/openstackclient/tests/unit/network/v2/test_port.py index b1a18da6..87aea61f 100644 --- a/openstackclient/tests/unit/network/v2/test_port.py +++ b/openstackclient/tests/unit/network/v2/test_port.py @@ -1054,6 +1054,22 @@ class TestListPort(TestPort): self.assertEqual(self.columns_long, columns) self.assertListItemEqual(self.data_long, list(data)) + def test_port_list_host(self): + arglist = [ + '--host', 'foobar', + ] + verifylist = [ + ('host', 'foobar'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + filters = {'binding:host_id': 'foobar'} + + self.network.ports.assert_called_once_with(**filters) + self.assertEqual(self.columns, columns) + self.assertListItemEqual(self.data, list(data)) + def test_port_list_project(self): project = identity_fakes.FakeProject.create_one_project() self.projects_mock.get.return_value = project diff --git a/openstackclient/tests/unit/network/v2/test_router.py b/openstackclient/tests/unit/network/v2/test_router.py index 38861b0a..09b4957c 100644 --- a/openstackclient/tests/unit/network/v2/test_router.py +++ b/openstackclient/tests/unit/network/v2/test_router.py @@ -776,6 +776,146 @@ class TestRemoveSubnetFromRouter(TestRouter): self.assertIsNone(result) +class TestAddExtraRoutesToRouter(TestRouter): + + _router = network_fakes.FakeRouter.create_one_router() + + def setUp(self): + super(TestAddExtraRoutesToRouter, self).setUp() + self.network.add_extra_routes_to_router = mock.Mock( + return_value=self._router) + self.cmd = router.AddExtraRoutesToRouter(self.app, self.namespace) + self.network.find_router = mock.Mock(return_value=self._router) + + def test_add_no_extra_route(self): + arglist = [ + self._router.id, + ] + verifylist = [ + ('router', self._router.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.network.add_extra_routes_to_router.assert_called_with( + self._router, body={'router': {'routes': []}}) + self.assertEqual(2, len(result)) + + def test_add_one_extra_route(self): + arglist = [ + self._router.id, + '--route', 'destination=dst1,gateway=gw1', + ] + verifylist = [ + ('router', self._router.id), + ('routes', [{'destination': 'dst1', 'gateway': 'gw1'}]), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.network.add_extra_routes_to_router.assert_called_with( + self._router, body={'router': {'routes': [ + {'destination': 'dst1', 'nexthop': 'gw1'}, + ]}}) + self.assertEqual(2, len(result)) + + def test_add_multiple_extra_routes(self): + arglist = [ + self._router.id, + '--route', 'destination=dst1,gateway=gw1', + '--route', 'destination=dst2,gateway=gw2', + ] + verifylist = [ + ('router', self._router.id), + ('routes', [ + {'destination': 'dst1', 'gateway': 'gw1'}, + {'destination': 'dst2', 'gateway': 'gw2'}, + ]), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.network.add_extra_routes_to_router.assert_called_with( + self._router, body={'router': {'routes': [ + {'destination': 'dst1', 'nexthop': 'gw1'}, + {'destination': 'dst2', 'nexthop': 'gw2'}, + ]}}) + self.assertEqual(2, len(result)) + + +class TestRemoveExtraRoutesFromRouter(TestRouter): + + _router = network_fakes.FakeRouter.create_one_router() + + def setUp(self): + super(TestRemoveExtraRoutesFromRouter, self).setUp() + self.network.remove_extra_routes_from_router = mock.Mock( + return_value=self._router) + self.cmd = router.RemoveExtraRoutesFromRouter(self.app, self.namespace) + self.network.find_router = mock.Mock(return_value=self._router) + + def test_remove_no_extra_route(self): + arglist = [ + self._router.id, + ] + verifylist = [ + ('router', self._router.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.network.remove_extra_routes_from_router.assert_called_with( + self._router, body={'router': {'routes': []}}) + self.assertEqual(2, len(result)) + + def test_remove_one_extra_route(self): + arglist = [ + self._router.id, + '--route', 'destination=dst1,gateway=gw1', + ] + verifylist = [ + ('router', self._router.id), + ('routes', [{'destination': 'dst1', 'gateway': 'gw1'}]), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.network.remove_extra_routes_from_router.assert_called_with( + self._router, body={'router': {'routes': [ + {'destination': 'dst1', 'nexthop': 'gw1'}, + ]}}) + self.assertEqual(2, len(result)) + + def test_remove_multiple_extra_routes(self): + arglist = [ + self._router.id, + '--route', 'destination=dst1,gateway=gw1', + '--route', 'destination=dst2,gateway=gw2', + ] + verifylist = [ + ('router', self._router.id), + ('routes', [ + {'destination': 'dst1', 'gateway': 'gw1'}, + {'destination': 'dst2', 'gateway': 'gw2'}, + ]), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.network.remove_extra_routes_from_router.assert_called_with( + self._router, body={'router': {'routes': [ + {'destination': 'dst1', 'nexthop': 'gw1'}, + {'destination': 'dst2', 'nexthop': 'gw2'}, + ]}}) + self.assertEqual(2, len(result)) + + class TestSetRouter(TestRouter): # The router to set. diff --git a/openstackclient/tests/unit/network/v2/test_security_group_network.py b/openstackclient/tests/unit/network/v2/test_security_group_network.py index 14d57514..7c1d7fb6 100644 --- a/openstackclient/tests/unit/network/v2/test_security_group_network.py +++ b/openstackclient/tests/unit/network/v2/test_security_group_network.py @@ -49,6 +49,7 @@ class TestCreateSecurityGroupNetwork(TestSecurityGroupNetwork): 'name', 'project_id', 'rules', + 'stateful', 'tags', ) @@ -58,6 +59,7 @@ class TestCreateSecurityGroupNetwork(TestSecurityGroupNetwork): _security_group.name, _security_group.project_id, security_group.NetworkSecurityGroupRulesColumn([]), + _security_group.stateful, _security_group.tags, ) @@ -101,6 +103,7 @@ class TestCreateSecurityGroupNetwork(TestSecurityGroupNetwork): '--description', self._security_group.description, '--project', self.project.name, '--project-domain', self.domain.name, + '--stateful', self._security_group.name, ] verifylist = [ @@ -108,6 +111,7 @@ class TestCreateSecurityGroupNetwork(TestSecurityGroupNetwork): ('name', self._security_group.name), ('project', self.project.name), ('project_domain', self.domain.name), + ('stateful', self._security_group.stateful), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -115,6 +119,7 @@ class TestCreateSecurityGroupNetwork(TestSecurityGroupNetwork): self.network.create_security_group.assert_called_once_with(**{ 'description': self._security_group.description, + 'stateful': self._security_group.stateful, 'name': self._security_group.name, 'tenant_id': self.project.id, }) @@ -285,7 +290,8 @@ class TestListSecurityGroupNetwork(TestSecurityGroupNetwork): columns, data = self.cmd.take_action(parsed_args) - self.network.security_groups.assert_called_once_with() + self.network.security_groups.assert_called_once_with( + fields=security_group.ListSecurityGroup.FIELDS_TO_RETRIEVE) self.assertEqual(self.columns, columns) self.assertListItemEqual(self.data, list(data)) @@ -300,7 +306,8 @@ class TestListSecurityGroupNetwork(TestSecurityGroupNetwork): columns, data = self.cmd.take_action(parsed_args) - self.network.security_groups.assert_called_once_with() + self.network.security_groups.assert_called_once_with( + fields=security_group.ListSecurityGroup.FIELDS_TO_RETRIEVE) self.assertEqual(self.columns, columns) self.assertListItemEqual(self.data, list(data)) @@ -316,7 +323,9 @@ class TestListSecurityGroupNetwork(TestSecurityGroupNetwork): parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - filters = {'tenant_id': project.id, 'project_id': project.id} + filters = { + 'tenant_id': project.id, 'project_id': project.id, + 'fields': security_group.ListSecurityGroup.FIELDS_TO_RETRIEVE} self.network.security_groups.assert_called_once_with(**filters) self.assertEqual(self.columns, columns) @@ -336,7 +345,9 @@ class TestListSecurityGroupNetwork(TestSecurityGroupNetwork): parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - filters = {'tenant_id': project.id, 'project_id': project.id} + filters = { + 'tenant_id': project.id, 'project_id': project.id, + 'fields': security_group.ListSecurityGroup.FIELDS_TO_RETRIEVE} self.network.security_groups.assert_called_once_with(**filters) self.assertEqual(self.columns, columns) @@ -362,7 +373,8 @@ class TestListSecurityGroupNetwork(TestSecurityGroupNetwork): **{'tags': 'red,blue', 'any_tags': 'red,green', 'not_tags': 'orange,yellow', - 'not_any_tags': 'black,white'} + 'not_any_tags': 'black,white', + 'fields': security_group.ListSecurityGroup.FIELDS_TO_RETRIEVE} ) self.assertEqual(self.columns, columns) self.assertEqual(self.data, list(data)) @@ -414,11 +426,13 @@ class TestSetSecurityGroupNetwork(TestSecurityGroupNetwork): arglist = [ '--name', new_name, '--description', new_description, + '--stateful', self._security_group.name, ] verifylist = [ ('description', new_description), ('group', self._security_group.name), + ('stateful', self._security_group.stateful), ('name', new_name), ] @@ -428,6 +442,7 @@ class TestSetSecurityGroupNetwork(TestSecurityGroupNetwork): attrs = { 'description': new_description, 'name': new_name, + 'stateful': True, } self.network.update_security_group.assert_called_once_with( self._security_group, @@ -482,6 +497,7 @@ class TestShowSecurityGroupNetwork(TestSecurityGroupNetwork): 'name', 'project_id', 'rules', + 'stateful', 'tags', ) @@ -492,6 +508,7 @@ class TestShowSecurityGroupNetwork(TestSecurityGroupNetwork): _security_group.project_id, security_group.NetworkSecurityGroupRulesColumn( [_security_group_rule._info]), + _security_group.stateful, _security_group.tags, ) diff --git a/openstackclient/tests/unit/network/v2/test_subnet.py b/openstackclient/tests/unit/network/v2/test_subnet.py index e71e1dd6..47d0c6b4 100644 --- a/openstackclient/tests/unit/network/v2/test_subnet.py +++ b/openstackclient/tests/unit/network/v2/test_subnet.py @@ -460,6 +460,44 @@ class TestCreateSubnet(TestSubnet): self.assertEqual(self.columns, columns) self.assertItemEqual(self.data, data) + def _test_create_with_dns(self, publish_dns=True): + arglist = [ + "--subnet-range", self._subnet.cidr, + "--network", self._subnet.network_id, + self._subnet.name, + ] + if publish_dns: + arglist += ['--dns-publish-fixed-ip'] + else: + arglist += ['--no-dns-publish-fixed-ip'] + verifylist = [ + ('name', self._subnet.name), + ('subnet_range', self._subnet.cidr), + ('network', self._subnet.network_id), + ('ip_version', self._subnet.ip_version), + ('gateway', 'auto'), + ] + verifylist.append(('dns_publish_fixed_ip', publish_dns)) + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = (self.cmd.take_action(parsed_args)) + + self.network.create_subnet.assert_called_once_with( + cidr=self._subnet.cidr, + ip_version=self._subnet.ip_version, + name=self._subnet.name, + network_id=self._subnet.network_id, + dns_publish_fixed_ip=publish_dns, + ) + self.assertEqual(self.columns, columns) + self.assertItemEqual(self.data, data) + + def test_create_with_dns(self): + self._test_create_with_dns(publish_dns=True) + + def test_create_with_no_dns(self): + self._test_create_with_dns(publish_dns=False) + def _test_create_with_tag(self, add_tags=True): arglist = [ "--subnet-range", self._subnet.cidr, diff --git a/openstackclient/tests/unit/volume/v2/test_volume.py b/openstackclient/tests/unit/volume/v2/test_volume.py index 5d41b3a1..4e204ad1 100644 --- a/openstackclient/tests/unit/volume/v2/test_volume.py +++ b/openstackclient/tests/unit/volume/v2/test_volume.py @@ -41,8 +41,8 @@ class TestVolume(volume_fakes.TestVolume): self.users_mock = self.app.client_manager.identity.users self.users_mock.reset_mock() - self.images_mock = self.app.client_manager.image.images - self.images_mock.reset_mock() + self.find_image_mock = self.app.client_manager.image.find_image + self.find_image_mock.reset_mock() self.snapshots_mock = self.app.client_manager.volume.volume_snapshots self.snapshots_mock.reset_mock() @@ -222,7 +222,7 @@ class TestVolumeCreate(TestVolume): def test_volume_create_image_id(self): image = image_fakes.FakeImage.create_one_image() - self.images_mock.get.return_value = image + self.find_image_mock.return_value = image arglist = [ '--image', image.id, @@ -260,7 +260,7 @@ class TestVolumeCreate(TestVolume): def test_volume_create_image_name(self): image = image_fakes.FakeImage.create_one_image() - self.images_mock.get.return_value = image + self.find_image_mock.return_value = image arglist = [ '--image', image.name, diff --git a/openstackclient/volume/client.py b/openstackclient/volume/client.py index fdd1794b..1fbfaaee 100644 --- a/openstackclient/volume/client.py +++ b/openstackclient/volume/client.py @@ -67,11 +67,15 @@ def make_client(instance): # Remember interface only if it is set kwargs = utils.build_kwargs_dict('endpoint_type', instance.interface) + endpoint_override = instance.sdk_connection.config.get_endpoint( + 'block-storage') + client = volume_client( session=instance.session, extensions=extensions, http_log_debug=http_log_debug, region_name=instance.region_name, + endpoint_override=endpoint_override, **kwargs ) diff --git a/openstackclient/volume/v1/volume_snapshot.py b/openstackclient/volume/v1/volume_snapshot.py index 966db48f..2d1f0359 100644 --- a/openstackclient/volume/v1/volume_snapshot.py +++ b/openstackclient/volume/v1/volume_snapshot.py @@ -174,10 +174,10 @@ class ListVolumeSnapshot(command.Lister): '--status', metavar='<status>', choices=['available', 'error', 'creating', 'deleting', - 'error-deleting'], + 'error_deleting'], help=_("Filters results by a status. " "('available', 'error', 'creating', 'deleting'" - " or 'error-deleting')") + " or 'error_deleting')") ) parser.add_argument( '--volume', diff --git a/openstackclient/volume/v2/consistency_group_snapshot.py b/openstackclient/volume/v2/consistency_group_snapshot.py index 3df66e69..7d5ba82f 100644 --- a/openstackclient/volume/v2/consistency_group_snapshot.py +++ b/openstackclient/volume/v2/consistency_group_snapshot.py @@ -128,7 +128,7 @@ class ListConsistencyGroupSnapshot(command.Lister): '--status', metavar="<status>", choices=['available', 'error', 'creating', 'deleting', - 'error-deleting'], + 'error_deleting'], help=_('Filters results by a status ("available", "error", ' '"creating", "deleting" or "error_deleting")') ) diff --git a/openstackclient/volume/v2/volume.py b/openstackclient/volume/v2/volume.py index 4dde1340..1e0cb183 100644 --- a/openstackclient/volume/v2/volume.py +++ b/openstackclient/volume/v2/volume.py @@ -193,9 +193,8 @@ class CreateVolume(command.ShowOne): image = None if parsed_args.image: - image = utils.find_resource( - image_client.images, - parsed_args.image).id + image = image_client.find_image(parsed_args.image, + ignore_missing=False).id size = parsed_args.size diff --git a/openstackclient/volume/v2/volume_snapshot.py b/openstackclient/volume/v2/volume_snapshot.py index edacf683..656f59d4 100644 --- a/openstackclient/volume/v2/volume_snapshot.py +++ b/openstackclient/volume/v2/volume_snapshot.py @@ -229,10 +229,10 @@ class ListVolumeSnapshot(command.Lister): '--status', metavar='<status>', choices=['available', 'error', 'creating', 'deleting', - 'error-deleting'], + 'error_deleting'], help=_("Filters results by a status. " "('available', 'error', 'creating', 'deleting'" - " or 'error-deleting')") + " or 'error_deleting')") ) parser.add_argument( '--volume', @@ -344,7 +344,7 @@ class SetVolumeSnapshot(command.Command): '--state', metavar='<state>', choices=['available', 'error', 'creating', 'deleting', - 'error-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 ' |
