diff options
64 files changed, 3462 insertions, 315 deletions
diff --git a/doc/source/command-objects/consistency-group.rst b/doc/source/command-objects/consistency-group.rst index 658d9425..e54ac65f 100644 --- a/doc/source/command-objects/consistency-group.rst +++ b/doc/source/command-objects/consistency-group.rst @@ -4,6 +4,28 @@ consistency group Block Storage v2 +consistency group add volume +---------------------------- + +Add volume(s) to consistency group. + +.. program:: consistency group add volume +.. code:: bash + + os consistency group add volume + <consistency-group> + <volume> [<volume> ...] + +.. _consistency_group_add_volume: +.. describe:: <consistency-group> + + Consistency group to contain <volume> (name or ID) + +.. describe:: <volume> + + Volume(s) to add to <consistency-group> (name or ID) + (repeat option to add multiple volumes) + consistency group create ------------------------ @@ -86,13 +108,35 @@ List consistency groups. List additional fields in output +consistency group remove volume +------------------------------- + +Remove volume(s) from consistency group. + +.. program:: consistency group remove volume +.. code:: bash + + os consistency group remove volume + <consistency-group> + <volume> [<volume> ...] + +.. _consistency_group_remove_volume: +.. describe:: <consistency-group> + + Consistency group containing <volume> (name or ID) + +.. describe:: <volume> + + Volume(s) to remove from <consistency-group> (name or ID) + (repeat option to remove multiple volumes) + consistency group set --------------------- Set consistency group properties. .. program:: consistency group set - .. code:: bash +.. code:: bash openstack consistency group set [--name <name>] diff --git a/doc/source/command-objects/flavor.rst b/doc/source/command-objects/flavor.rst index 0900f2ed..971628d7 100644 --- a/doc/source/command-objects/flavor.rst +++ b/doc/source/command-objects/flavor.rst @@ -42,9 +42,9 @@ Create new flavor Ephemeral disk size in GB (default 0G) -.. option:: --swap <size-gb> +.. option:: --swap <size-mb> - Swap space size in GB (default 0G) + Swap space size in MB (default 0M) .. option:: --vcpus <num-cpu> diff --git a/doc/source/command-objects/image.rst b/doc/source/command-objects/image.rst index 7ebcb54a..999842af 100644 --- a/doc/source/command-objects/image.rst +++ b/doc/source/command-objects/image.rst @@ -325,6 +325,7 @@ Set image properties [--ramdisk-id <ramdisk-id>] [--activate|--deactivate] [--project <project> [--project-domain <project-domain>]] + [--accept | --reject | --pending] <image> .. option:: --name <name> @@ -490,6 +491,36 @@ Set image properties .. versionadded:: 2 +.. option:: --accept + + Accept the image membership. + + If `--project` is passed, this will update the membership status for the + given project, otherwise `--project` will default to the project the user + is authenticated to. + + .. versionadded:: 2 + +.. option:: --reject + + Reject the image membership. + + If `--project` is passed, this will update the membership status for the + given project, otherwise `--project` will default to the project the user + is authenticated to. + + .. versionadded:: 2 + +.. option:: --pending + + Reset the image membership to 'pending'. + + If `--project` is passed, this will update the membership status for the + given project, otherwise `--project` will default to the project the user + is authenticated to. + + .. versionadded:: 2 + .. _image_set-image: .. describe:: <image> diff --git a/doc/source/command-objects/network-qos-rule.rst b/doc/source/command-objects/network-qos-rule.rst new file mode 100644 index 00000000..b98244b1 --- /dev/null +++ b/doc/source/command-objects/network-qos-rule.rst @@ -0,0 +1,165 @@ +================ +network qos rule +================ + +A **Network QoS rule** specifies a rule defined in a Network QoS policy; its +type is defined by the parameter 'type'. Can be assigned, within a Network QoS +policy, to a port or a network. Each Network QoS policy can contain several +rules, each of them + +Network v2 + +network qos rule create +----------------------- + +Create new Network QoS rule + +.. program:: network qos rule create +.. code:: bash + + os network qos rule create + --type <type> + [--max-kbps <max-kbps>] + [--max-burst-kbits <max-burst-kbits>] + [--dscp-marks <dscp-marks>] + [--min-kbps <min-kbps>] + [--ingress | --egress] + <qos-policy> + +.. option:: --type <type> + + QoS rule type (minimum-bandwidth, dscp-marking, bandwidth-limit) + +.. option:: --max-kbps <min-kbps> + + Maximum bandwidth in kbps + +.. option:: --max-burst-kbits <max-burst-kbits> + + Maximum burst in kilobits, 0 means automatic + +.. option:: --dscp-mark <dscp-mark> + + DSCP mark: value can be 0, even numbers from 8-56, excluding 42, 44, 50, + 52, and 54 + +.. option:: --min-kbps <min-kbps> + + Minimum guaranteed bandwidth in kbps + +.. option:: --ingress + + Ingress traffic direction from the project point of view + +.. option:: --egress + + Egress traffic direction from the project point of view + +.. describe:: <qos-policy> + + QoS policy that contains the rule (name or ID) + +network qos rule delete +----------------------- + +Delete Network QoS rule + +.. program:: network qos rule delete +.. code:: bash + + os network qos rule delete + <qos-policy> + <rule-id> + +.. describe:: <qos-policy> + + QoS policy that contains the rule (name or ID) + +.. describe:: <rule-id> + + Network QoS rule to delete (ID) + +network qos rule list +--------------------- + +List Network QoS rules + +.. program:: network qos rule list +.. code:: bash + + os network qos rule list + <qos-policy> + +.. describe:: <qos-policy> + + QoS policy that contains the rule (name or ID) + +network qos rule set +-------------------- + +Set Network QoS rule properties + +.. program:: network qos rule set +.. code:: bash + + os network qos rule set + [--max-kbps <max-kbps>] + [--max-burst-kbits <max-burst-kbits>] + [--dscp-marks <dscp-marks>] + [--min-kbps <min-kbps>] + [--ingress | --egress] + <qos-policy> + <rule-id> + +.. option:: --max-kbps <min-kbps> + + Maximum bandwidth in kbps + +.. option:: --max-burst-kbits <max-burst-kbits> + + Maximum burst in kilobits, 0 means automatic + +.. option:: --dscp-mark <dscp-mark> + + DSCP mark: value can be 0, even numbers from 8-56, excluding 42, 44, 50, + 52, and 54 + +.. option:: --min-kbps <min-kbps> + + Minimum guaranteed bandwidth in kbps + +.. option:: --ingress + + Ingress traffic direction from the project point of view + +.. option:: --egress + + Egress traffic direction from the project point of view + +.. describe:: <qos-policy> + + QoS policy that contains the rule (name or ID) + +.. describe:: <rule-id> + + Network QoS rule to delete (ID) + +network qos rule show +--------------------- + +Display Network QoS rule details + +.. program:: network qos rule show +.. code:: bash + + os network qos rule show + <qos-policy> + <rule-id> + +.. describe:: <qos-policy> + + QoS policy that contains the rule (name or ID) + +.. describe:: <rule-id> + + Network QoS rule to delete (ID) diff --git a/doc/source/commands.rst b/doc/source/commands.rst index b81c121b..78668a0f 100644 --- a/doc/source/commands.rst +++ b/doc/source/commands.rst @@ -113,6 +113,7 @@ referring to both Compute and Volume quotas. * ``network agent``: (**Network**) - A network agent is an agent that handles various tasks used to implement virtual networks * ``network meter``: (**Network**) - allow traffic metering in a network * ``network rbac``: (**Network**) - an RBAC policy for network resources +* ``network qos rule``: (**Network**) - a QoS rule for network resources * ``network qos policy``: (**Network**) - a QoS policy for network resources * ``network qos rule type``: (**Network**) - list of QoS available rule types * ``network segment``: (**Network**) - a segment of a virtual network diff --git a/openstackclient/common/quota.py b/openstackclient/common/quota.py index 58368c56..fa6c5765 100644 --- a/openstackclient/common/quota.py +++ b/openstackclient/common/quota.py @@ -18,6 +18,8 @@ import itertools import sys +from openstack import exceptions as sdk_exceptions +from openstack.network.v2 import quota as _quota from osc_lib.command import command from osc_lib import utils import six @@ -251,7 +253,39 @@ class ShowQuota(command.ShowOne): project = self._get_project(parsed_args) client = self.app.client_manager.network if parsed_args.default: - network_quota = client.get_quota_default(project) + # TODO(dtroyer): Remove the top of this if block once the + # fixed SDK QuotaDefault class is the minimum + # required version. This is expected to be + # SDK release 0.9.13 + if hasattr(_quota.QuotaDefault, 'project'): + # hack 0.9.11+ + quotadef_obj = client._get_resource( + _quota.QuotaDefault, + project, + ) + quotadef_obj.base_path = quotadef_obj.base_path % { + 'project': project, + } + try: + network_quota = quotadef_obj.get( + client.session, + requires_id=False, + ) + except sdk_exceptions.NotFoundException as e: + raise sdk_exceptions.ResourceNotFound( + message="No %s found for %s" % + (_quota.QuotaDefault.__name__, project), + details=e.details, + response=e.response, + request_id=e.request_id, + url=e.url, + method=e.method, + http_status=e.http_status, + cause=e.cause, + ) + # end hack-around + else: + network_quota = client.get_quota_default(project) else: network_quota = client.get_quota(project) return network_quota @@ -273,6 +307,10 @@ class ShowQuota(command.ShowOne): volume_quota_info = self.get_compute_volume_quota(volume_client, parsed_args) network_quota_info = self.get_network_quota(parsed_args) + # NOTE(reedip): Remove the below check once requirement for + # Openstack SDK is fixed to version 0.9.12 and above + if type(network_quota_info) is not dict: + network_quota_info = network_quota_info.to_dict() info = {} info.update(compute_quota_info) diff --git a/openstackclient/compute/v2/flavor.py b/openstackclient/compute/v2/flavor.py index e562cd40..7cd22ed7 100644 --- a/openstackclient/compute/v2/flavor.py +++ b/openstackclient/compute/v2/flavor.py @@ -89,9 +89,9 @@ class CreateFlavor(command.ShowOne): parser.add_argument( "--swap", type=int, - metavar="<size-gb>", + metavar="<size-mb>", default=0, - help=_("Swap space size in GB (default 0G)") + help=_("Swap space size in MB (default 0M)") ) parser.add_argument( "--vcpus", diff --git a/openstackclient/identity/v2_0/project.py b/openstackclient/identity/v2_0/project.py index 8526d6bd..ca565d4d 100644 --- a/openstackclient/identity/v2_0/project.py +++ b/openstackclient/identity/v2_0/project.py @@ -20,6 +20,7 @@ import logging from keystoneauth1 import exceptions as ks_exc from osc_lib.cli import parseractions from osc_lib.command import command +from osc_lib import exceptions from osc_lib import utils import six @@ -117,12 +118,25 @@ class DeleteProject(command.Command): def take_action(self, parsed_args): identity_client = self.app.client_manager.identity + errors = 0 for project in parsed_args.projects: - project_obj = utils.find_resource( - identity_client.tenants, - project, - ) - identity_client.tenants.delete(project_obj.id) + try: + project_obj = utils.find_resource( + identity_client.tenants, + project, + ) + identity_client.tenants.delete(project_obj.id) + except Exception as e: + errors += 1 + LOG.error(_("Failed to delete project with " + "name or ID '%(project)s': %(e)s"), + {'project': project, 'e': e}) + + if errors > 0: + total = len(parsed_args.projects) + msg = (_("%(errors)s of %(total)s projects failed " + "to delete.") % {'errors': errors, 'total': total}) + raise exceptions.CommandError(msg) class ListProject(command.Lister): diff --git a/openstackclient/identity/v2_0/role.py b/openstackclient/identity/v2_0/role.py index 0a28a70a..e254e05f 100644 --- a/openstackclient/identity/v2_0/role.py +++ b/openstackclient/identity/v2_0/role.py @@ -124,12 +124,25 @@ class DeleteRole(command.Command): def take_action(self, parsed_args): identity_client = self.app.client_manager.identity + errors = 0 for role in parsed_args.roles: - role_obj = utils.find_resource( - identity_client.roles, - role, - ) - identity_client.roles.delete(role_obj.id) + try: + role_obj = utils.find_resource( + identity_client.roles, + role, + ) + identity_client.roles.delete(role_obj.id) + except Exception as e: + errors += 1 + LOG.error(_("Failed to delete role with " + "name or ID '%(role)s': %(e)s"), + {'role': role, 'e': e}) + + if errors > 0: + total = len(parsed_args.roles) + msg = (_("%(errors)s of %(total)s roles failed " + "to delete.") % {'errors': errors, 'total': total}) + raise exceptions.CommandError(msg) class ListRole(command.Lister): diff --git a/openstackclient/identity/v2_0/user.py b/openstackclient/identity/v2_0/user.py index ddd5b981..2a3dde6b 100644 --- a/openstackclient/identity/v2_0/user.py +++ b/openstackclient/identity/v2_0/user.py @@ -19,6 +19,7 @@ import logging from keystoneauth1 import exceptions as ks_exc from osc_lib.command import command +from osc_lib import exceptions from osc_lib import utils import six @@ -145,12 +146,25 @@ class DeleteUser(command.Command): def take_action(self, parsed_args): identity_client = self.app.client_manager.identity + errors = 0 for user in parsed_args.users: - user_obj = utils.find_resource( - identity_client.users, - user, - ) - identity_client.users.delete(user_obj.id) + try: + user_obj = utils.find_resource( + identity_client.users, + user, + ) + identity_client.users.delete(user_obj.id) + except Exception as e: + errors += 1 + LOG.error(_("Failed to delete user with " + "name or ID '%(user)s': %(e)s"), + {'user': user, 'e': e}) + + if errors > 0: + total = len(parsed_args.users) + msg = (_("%(errors)s of %(total)s users failed " + "to delete.") % {'errors': errors, 'total': total}) + raise exceptions.CommandError(msg) class ListUser(command.Lister): diff --git a/openstackclient/identity/v3/group.py b/openstackclient/identity/v3/group.py index df684c12..a03a86eb 100644 --- a/openstackclient/identity/v3/group.py +++ b/openstackclient/identity/v3/group.py @@ -20,6 +20,7 @@ import sys from keystoneauth1 import exceptions as ks_exc from osc_lib.command import command +from osc_lib import exceptions from osc_lib import utils import six @@ -194,11 +195,24 @@ class DeleteGroup(command.Command): def take_action(self, parsed_args): identity_client = self.app.client_manager.identity + errors = 0 for group in parsed_args.groups: - group_obj = common.find_group(identity_client, - group, - parsed_args.domain) - identity_client.groups.delete(group_obj.id) + try: + group_obj = common.find_group(identity_client, + group, + parsed_args.domain) + identity_client.groups.delete(group_obj.id) + except Exception as e: + errors += 1 + LOG.error(_("Failed to delete group with " + "name or ID '%(group)s': %(e)s"), + {'group': group, 'e': e}) + + if errors > 0: + total = len(parsed_args.groups) + msg = (_("%(errors)s of %(total)s groups failed " + "to delete.") % {'errors': errors, 'total': total}) + raise exceptions.CommandError(msg) class ListGroup(command.Lister): diff --git a/openstackclient/identity/v3/project.py b/openstackclient/identity/v3/project.py index a6348659..12197cdd 100644 --- a/openstackclient/identity/v3/project.py +++ b/openstackclient/identity/v3/project.py @@ -20,6 +20,7 @@ import logging from keystoneauth1 import exceptions as ks_exc from osc_lib.cli import parseractions from osc_lib.command import command +from osc_lib import exceptions from osc_lib import utils import six @@ -148,15 +149,28 @@ class DeleteProject(command.Command): domain = None if parsed_args.domain: domain = common.find_domain(identity_client, parsed_args.domain) + errors = 0 for project in parsed_args.projects: - if domain is not None: - project_obj = utils.find_resource(identity_client.projects, - project, - domain_id=domain.id) - else: - project_obj = utils.find_resource(identity_client.projects, - project) - identity_client.projects.delete(project_obj.id) + try: + if domain is not None: + project_obj = utils.find_resource(identity_client.projects, + project, + domain_id=domain.id) + else: + project_obj = utils.find_resource(identity_client.projects, + project) + identity_client.projects.delete(project_obj.id) + except Exception as e: + errors += 1 + LOG.error(_("Failed to delete project with " + "name or ID '%(project)s': %(e)s"), + {'project': project, 'e': e}) + + if errors > 0: + total = len(parsed_args.projects) + msg = (_("%(errors)s of %(total)s projects failed " + "to delete.") % {'errors': errors, 'total': total}) + raise exceptions.CommandError(msg) class ListProject(command.Lister): diff --git a/openstackclient/identity/v3/role.py b/openstackclient/identity/v3/role.py index c9d0fbf3..994ecc9c 100644 --- a/openstackclient/identity/v3/role.py +++ b/openstackclient/identity/v3/role.py @@ -20,6 +20,7 @@ import sys from keystoneauth1 import exceptions as ks_exc from osc_lib.command import command +from osc_lib import exceptions from osc_lib import utils import six @@ -223,14 +224,26 @@ class DeleteRole(command.Command): if parsed_args.domain: domain_id = common.find_domain(identity_client, parsed_args.domain).id - + errors = 0 for role in parsed_args.roles: - role_obj = utils.find_resource( - identity_client.roles, - role, - domain_id=domain_id - ) - identity_client.roles.delete(role_obj.id) + try: + role_obj = utils.find_resource( + identity_client.roles, + role, + domain_id=domain_id + ) + identity_client.roles.delete(role_obj.id) + except Exception as e: + errors += 1 + LOG.error(_("Failed to delete role with " + "name or ID '%(role)s': %(e)s"), + {'role': role, 'e': e}) + + if errors > 0: + total = len(parsed_args.roles) + msg = (_("%(errors)s of %(total)s roles failed " + "to delete.") % {'errors': errors, 'total': total}) + raise exceptions.CommandError(msg) class ListRole(command.Lister): diff --git a/openstackclient/identity/v3/trust.py b/openstackclient/identity/v3/trust.py index 62d72ea1..04ee4dce 100644 --- a/openstackclient/identity/v3/trust.py +++ b/openstackclient/identity/v3/trust.py @@ -14,8 +14,10 @@ """Identity v3 Trust action implementations""" import datetime +import logging from osc_lib.command import command +from osc_lib import exceptions from osc_lib import utils import six @@ -23,6 +25,9 @@ from openstackclient.i18n import _ from openstackclient.identity import common +LOG = logging.getLogger(__name__) + + class CreateTrust(command.ShowOne): _description = _("Create new trust") @@ -145,9 +150,24 @@ class DeleteTrust(command.Command): def take_action(self, parsed_args): identity_client = self.app.client_manager.identity - for t in parsed_args.trust: - trust_obj = utils.find_resource(identity_client.trusts, t) - identity_client.trusts.delete(trust_obj.id) + + errors = 0 + for trust in parsed_args.trust: + try: + trust_obj = utils.find_resource(identity_client.trusts, + trust) + identity_client.trusts.delete(trust_obj.id) + except Exception as e: + errors += 1 + LOG.error(_("Failed to delete trust with " + "name or ID '%(trust)s': %(e)s"), + {'trust': trust, 'e': e}) + + if errors > 0: + total = len(parsed_args.trust) + msg = (_("%(errors)s of %(total)s trusts failed " + "to delete.") % {'errors': errors, 'total': total}) + raise exceptions.CommandError(msg) class ListTrust(command.Lister): diff --git a/openstackclient/identity/v3/user.py b/openstackclient/identity/v3/user.py index 796cf28c..19a4c298 100644 --- a/openstackclient/identity/v3/user.py +++ b/openstackclient/identity/v3/user.py @@ -20,6 +20,7 @@ import logging from keystoneauth1 import exceptions as ks_exc from osc_lib.command import command +from osc_lib import exceptions from osc_lib import utils import six @@ -161,15 +162,28 @@ class DeleteUser(command.Command): domain = None if parsed_args.domain: domain = common.find_domain(identity_client, parsed_args.domain) + errors = 0 for user in parsed_args.users: - if domain is not None: - user_obj = utils.find_resource(identity_client.users, - user, - domain_id=domain.id) - else: - user_obj = utils.find_resource(identity_client.users, - user) - identity_client.users.delete(user_obj.id) + try: + if domain is not None: + user_obj = utils.find_resource(identity_client.users, + user, + domain_id=domain.id) + else: + user_obj = utils.find_resource(identity_client.users, + user) + identity_client.users.delete(user_obj.id) + except Exception as e: + errors += 1 + LOG.error(_("Failed to delete user with " + "name or ID '%(user)s': %(e)s"), + {'user': user, 'e': e}) + + if errors > 0: + total = len(parsed_args.users) + msg = (_("%(errors)s of %(total)s users failed " + "to delete.") % {'errors': errors, 'total': total}) + raise exceptions.CommandError(msg) class ListUser(command.Lister): diff --git a/openstackclient/image/v2/image.py b/openstackclient/image/v2/image.py index 55eb7eb1..418cc397 100644 --- a/openstackclient/image/v2/image.py +++ b/openstackclient/image/v2/image.py @@ -777,6 +777,23 @@ class SetImage(command.Command): dest=deadopt.replace('-', '_'), help=argparse.SUPPRESS, ) + + membership_group = parser.add_mutually_exclusive_group() + membership_group.add_argument( + "--accept", + action="store_true", + help=_("Accept the image membership"), + ) + membership_group.add_argument( + "--reject", + action="store_true", + help=_("Reject the image membership"), + ) + membership_group.add_argument( + "--pending", + action="store_true", + help=_("Reset the image membership to 'pending'"), + ) return parser def take_action(self, parsed_args): @@ -828,12 +845,14 @@ class SetImage(command.Command): project_arg = parsed_args.owner LOG.warning(_('The --owner option is deprecated, ' 'please use --project instead.')) + project_id = None if project_arg: - kwargs['owner'] = common.find_project( + project_id = common.find_project( identity_client, project_arg, parsed_args.project_domain, ).id + kwargs['owner'] = project_id image = utils.find_resource( image_client.images, parsed_args.image) @@ -846,6 +865,21 @@ class SetImage(command.Command): image_client.images.reactivate(image.id) activation_status = "activated" + membership_group_args = ('accept', 'reject', 'pending') + membership_status = [status for status in membership_group_args + if getattr(parsed_args, status)] + if membership_status: + # If a specific project is not passed, assume we want to update + # our own membership + if not project_id: + project_id = self.app.client_manager.auth_ref.project_id + # The mutually exclusive group of the arg parser ensure we have at + # 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]) + if parsed_args.tags: # Tags should be extended, but duplicates removed kwargs['tags'] = list(set(image.tags).union(set(parsed_args.tags))) diff --git a/openstackclient/network/v2/floating_ip.py b/openstackclient/network/v2/floating_ip.py index 8202b3fa..980c41c7 100644 --- a/openstackclient/network/v2/floating_ip.py +++ b/openstackclient/network/v2/floating_ip.py @@ -15,6 +15,8 @@ import logging +from openstack import exceptions as sdk_exceptions +from openstack.network.v2 import floating_ip as _floating_ip from osc_lib import utils from openstackclient.i18n import _ @@ -79,6 +81,58 @@ def _get_attrs(client_manager, parsed_args): return attrs +def _find_floating_ip( + session, + ip_cache, + name_or_id, + ignore_missing=True, + **params +): + """Find a floating IP by IP or ID + + The SDK's find_ip() can only locate a floating IP by ID so we have + to do this ourselves. + """ + + def _get_one_match(name_or_id): + """Given a list of results, return the match""" + the_result = None + for maybe_result in ip_cache: + id_value = maybe_result.id + ip_value = maybe_result.floating_ip_address + + if (id_value == name_or_id) or (ip_value == name_or_id): + # Only allow one resource to be found. If we already + # found a match, raise an exception to show it. + if the_result is None: + the_result = maybe_result + else: + msg = "More than one %s exists with the name '%s'." + msg = (msg % (_floating_ip.FloatingIP, name_or_id)) + raise sdk_exceptions.DuplicateResource(msg) + + return the_result + + # Try to short-circuit by looking directly for a matching ID. + try: + match = _floating_ip.FloatingIP.existing(id=name_or_id, **params) + return (match.get(session), ip_cache) + except sdk_exceptions.NotFoundException: + pass + + if len(ip_cache) == 0: + ip_cache = list(_floating_ip.FloatingIP.list(session, **params)) + + result = _get_one_match(name_or_id) + if result is not None: + return (result, ip_cache) + + if ignore_missing: + return (None, ip_cache) + raise sdk_exceptions.ResourceNotFound( + "No %s found for %s" % (_floating_ip.FloatingIP.__name__, name_or_id)) + + class CreateFloatingIP(common.NetworkAndComputeShowOne): _description = _("Create floating IP") @@ -186,13 +240,28 @@ class DeleteFloatingIP(common.NetworkAndComputeDelete): return parser def take_action_network(self, client, parsed_args): - obj = client.find_ip(self.r, ignore_missing=False) + (obj, self.ip_cache) = _find_floating_ip( + client.session, + self.ip_cache, + self.r, + ignore_missing=False, + ) client.delete_ip(obj) def take_action_compute(self, client, parsed_args): obj = utils.find_resource(client.floating_ips, self.r) client.floating_ips.delete(obj.id) + def take_action(self, parsed_args): + """Implements a naive cache for the list of floating IPs""" + + # NOTE(dtroyer): This really only prevents multiple list() + # calls when performing multiple resource deletes + # in a single command. In an interactive session + # each delete command will call list(). + self.ip_cache = [] + super(DeleteFloatingIP, self).take_action(parsed_args) + class DeleteIPFloating(DeleteFloatingIP): _description = _("Delete floating IP(s)") @@ -390,6 +459,9 @@ class ListIPFloating(ListFloatingIP): class ShowFloatingIP(common.NetworkAndComputeShowOne): _description = _("Display floating IP details") + # ip_cache is unused here but is a side effect of _find_floating_ip() + ip_cache = [] + def update_parser_common(self, parser): parser.add_argument( 'floating_ip', @@ -399,7 +471,12 @@ class ShowFloatingIP(common.NetworkAndComputeShowOne): return parser def take_action_network(self, client, parsed_args): - obj = client.find_ip(parsed_args.floating_ip, ignore_missing=False) + (obj, self.ip_cache) = _find_floating_ip( + client.session, + [], + parsed_args.floating_ip, + ignore_missing=False, + ) display_columns, columns = _get_network_columns(obj) data = utils.get_item_properties(obj, columns) return (display_columns, data) diff --git a/openstackclient/network/v2/network_agent.py b/openstackclient/network/v2/network_agent.py index b3411166..d429fa08 100644 --- a/openstackclient/network/v2/network_agent.py +++ b/openstackclient/network/v2/network_agent.py @@ -20,6 +20,7 @@ from osc_lib import exceptions from osc_lib import utils from openstackclient.i18n import _ +from openstackclient.network import sdk_utils LOG = logging.getLogger(__name__) @@ -31,10 +32,19 @@ def _format_admin_state(state): _formatters = { 'admin_state_up': _format_admin_state, + 'is_admin_state_up': _format_admin_state, 'configurations': utils.format_dict, } +def _get_network_columns(item): + column_map = { + 'is_admin_state_up': 'admin_state_up', + 'is_alive': 'alive', + } + return sdk_utils.get_osc_show_columns_for_sdk_resource(item, column_map) + + class DeleteNetworkAgent(command.Command): _description = _("Delete network agent(s)") @@ -69,6 +79,8 @@ class DeleteNetworkAgent(command.Command): raise exceptions.CommandError(msg) +# TODO(huanxuan): Use the SDK resource mapped attribute names once the +# OSC minimum requirements include SDK 1.0. class ListNetworkAgent(command.Lister): _description = _("List network agents") @@ -98,8 +110,8 @@ class ListNetworkAgent(command.Lister): 'agent_type', 'host', 'availability_zone', - 'alive', - 'admin_state_up', + 'is_alive', + 'is_admin_state_up', 'binary' ) column_headers = ( @@ -138,6 +150,8 @@ class ListNetworkAgent(command.Lister): ) for s in data)) +# TODO(huanxuan): Use the SDK resource mapped attribute names once the +# OSC minimum requirements include SDK 1.0. class SetNetworkAgent(command.Command): _description = _("Set network agent properties") @@ -168,10 +182,12 @@ class SetNetworkAgent(command.Command): def take_action(self, parsed_args): client = self.app.client_manager.network - obj = client.get_agent(parsed_args.network_agent, ignore_missing=False) + obj = client.get_agent(parsed_args.network_agent) attrs = {} if parsed_args.description is not None: attrs['description'] = str(parsed_args.description) + # TODO(huanxuan): Also update by the new attribute name + # "is_admin_state_up" after sdk 0.9.12 if parsed_args.enable: attrs['admin_state_up'] = True if parsed_args.disable: @@ -193,7 +209,7 @@ class ShowNetworkAgent(command.ShowOne): def take_action(self, parsed_args): client = self.app.client_manager.network - obj = client.get_agent(parsed_args.network_agent, ignore_missing=False) - columns = tuple(sorted(list(obj.keys()))) + obj = client.get_agent(parsed_args.network_agent) + display_columns, columns = _get_network_columns(obj) data = utils.get_item_properties(obj, columns, formatters=_formatters,) - return columns, data + return display_columns, data diff --git a/openstackclient/network/v2/network_qos_rule.py b/openstackclient/network/v2/network_qos_rule.py new file mode 100644 index 00000000..a662ca18 --- /dev/null +++ b/openstackclient/network/v2/network_qos_rule.py @@ -0,0 +1,356 @@ +# Copyright (c) 2016, Intel Corporation. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import itertools +import logging +import six + +from osc_lib.command import command +from osc_lib import exceptions +from osc_lib import utils + +from openstackclient.i18n import _ +from openstackclient.network import sdk_utils + + +LOG = logging.getLogger(__name__) + +RULE_TYPE_BANDWIDTH_LIMIT = 'bandwidth-limit' +RULE_TYPE_DSCP_MARKING = 'dscp-marking' +RULE_TYPE_MINIMUM_BANDWIDTH = 'minimum-bandwidth' +REQUIRED_PARAMETERS = { + RULE_TYPE_MINIMUM_BANDWIDTH: ['min_kbps', 'direction'], + RULE_TYPE_DSCP_MARKING: ['dscp_mark'], + RULE_TYPE_BANDWIDTH_LIMIT: ['max_kbps', 'max_burst_kbps']} +DIRECTION_EGRESS = 'egress' +DIRECTION_INGRESS = 'ingress' +DSCP_VALID_MARKS = [0, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, + 34, 36, 38, 40, 46, 48, 56] + +ACTION_CREATE = 'create' +ACTION_DELETE = 'delete' +ACTION_FIND = 'find' +ACTION_SET = 'update' +ACTION_SHOW = 'get' + + +def _get_columns(item): + column_map = { + 'tenant_id': 'project_id', + } + return sdk_utils.get_osc_show_columns_for_sdk_resource(item, column_map) + + +def _check_type_parameters(attrs, type, is_create): + req_params = REQUIRED_PARAMETERS[type] + notreq_params = list(itertools.chain( + *[v for k, v in six.iteritems(REQUIRED_PARAMETERS) if k != type])) + if is_create and None in map(attrs.get, req_params): + msg = (_('"Create" rule command for type "%(rule_type)s" requires ' + 'arguments %(args)s') % {'rule_type': type, + 'args': ", ".join(req_params)}) + raise exceptions.CommandError(msg) + if set(six.iterkeys(attrs)) & set(notreq_params): + msg = (_('Rule type "%(rule_type)s" only requires arguments %(args)s') + % {'rule_type': type, 'args': ", ".join(req_params)}) + raise exceptions.CommandError(msg) + + +def _get_attrs(network_client, parsed_args, is_create=False): + attrs = {} + qos = network_client.find_qos_policy(parsed_args.qos_policy, + ignore_missing=False) + attrs['qos_policy_id'] = qos.id + if not is_create: + attrs['id'] = parsed_args.id + rule_type = _find_rule_type(qos, parsed_args.id) + if not rule_type: + msg = (_('Rule ID %(rule_id)s not found') % + {'rule_id': parsed_args.id}) + raise exceptions.CommandError(msg) + else: + if not parsed_args.type: + msg = _('"Create" rule command requires argument "type"') + raise exceptions.CommandError(msg) + rule_type = parsed_args.type + if parsed_args.max_kbps is not None: + attrs['max_kbps'] = parsed_args.max_kbps + if parsed_args.max_burst_kbits is not None: + # NOTE(ralonsoh): this parameter must be changed in SDK and then in + # Neutron API, from 'max_burst_kbps' to + # 'max_burst_kbits' + attrs['max_burst_kbps'] = parsed_args.max_burst_kbits + if parsed_args.dscp_mark is not None: + attrs['dscp_mark'] = parsed_args.dscp_mark + if parsed_args.min_kbps is not None: + attrs['min_kbps'] = parsed_args.min_kbps + if parsed_args.ingress: + attrs['direction'] = 'ingress' + if parsed_args.egress: + attrs['direction'] = 'egress' + _check_type_parameters(attrs, rule_type, is_create) + return attrs + + +def _get_item_properties(item, fields): + """Return a tuple containing the item properties.""" + row = [] + for field in fields: + row.append(item.get(field, '')) + return tuple(row) + + +def _rule_action_call(client, action, rule_type): + rule_type = rule_type.replace('-', '_') + func_name = '%(action)s_qos_%(rule_type)s_rule' % {'action': action, + 'rule_type': rule_type} + return getattr(client, func_name) + + +def _find_rule_type(qos, rule_id): + for rule in (r for r in qos.rules if r['id'] == rule_id): + return rule['type'].replace('_', '-') + return None + + +def _add_rule_arguments(parser): + parser.add_argument( + '--max-kbps', + dest='max_kbps', + metavar='<max-kbps>', + type=int, + help=_('Maximum bandwidth in kbps') + ) + parser.add_argument( + '--max-burst-kbits', + dest='max_burst_kbits', + metavar='<max-burst-kbits>', + type=int, + help=_('Maximum burst in kilobits, 0 means automatic') + ) + parser.add_argument( + '--dscp-mark', + dest='dscp_mark', + metavar='<dscp-mark>', + type=int, + help=_('DSCP mark: value can be 0, even numbers from 8-56, ' + 'excluding 42, 44, 50, 52, and 54') + ) + parser.add_argument( + '--min-kbps', + dest='min_kbps', + metavar='<min-kbps>', + type=int, + help=_('Minimum guaranteed bandwidth in kbps') + ) + direction_group = parser.add_mutually_exclusive_group() + direction_group.add_argument( + '--ingress', + action='store_true', + help=_("Ingress traffic direction from the project point of view") + ) + direction_group.add_argument( + '--egress', + action='store_true', + help=_("Egress traffic direction from the project point of view") + ) + + +class CreateNetworkQosRule(command.ShowOne): + _description = _("Create new Network QoS rule") + + def get_parser(self, prog_name): + parser = super(CreateNetworkQosRule, self).get_parser( + prog_name) + parser.add_argument( + 'qos_policy', + metavar='<qos-policy>', + help=_('QoS policy that contains the rule (name or ID)') + ) + parser.add_argument( + '--type', + metavar='<type>', + choices=[RULE_TYPE_MINIMUM_BANDWIDTH, + RULE_TYPE_DSCP_MARKING, + RULE_TYPE_BANDWIDTH_LIMIT], + help=(_('QoS rule type (%s)') % + ", ".join(six.iterkeys(REQUIRED_PARAMETERS))) + ) + _add_rule_arguments(parser) + return parser + + def take_action(self, parsed_args): + network_client = self.app.client_manager.network + attrs = _get_attrs(network_client, parsed_args, is_create=True) + try: + obj = _rule_action_call( + network_client, ACTION_CREATE, parsed_args.type)( + attrs.pop('qos_policy_id'), **attrs) + except Exception as e: + 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) + return display_columns, data + + +class DeleteNetworkQosRule(command.Command): + _description = _("Delete Network QoS rule") + + def get_parser(self, prog_name): + parser = super(DeleteNetworkQosRule, self).get_parser(prog_name) + parser.add_argument( + 'qos_policy', + metavar='<qos-policy>', + help=_('QoS policy that contains the rule (name or ID)') + ) + parser.add_argument( + 'id', + metavar='<rule-id>', + help=_('Network QoS rule to delete (ID)') + ) + return parser + + def take_action(self, parsed_args): + network_client = self.app.client_manager.network + rule_id = parsed_args.id + try: + qos = network_client.find_qos_policy(parsed_args.qos_policy, + ignore_missing=False) + rule_type = _find_rule_type(qos, rule_id) + if not rule_type: + raise Exception('Rule %s not found' % rule_id) + _rule_action_call(network_client, ACTION_DELETE, rule_type)( + rule_id, qos.id) + except Exception as e: + msg = (_('Failed to delete Network QoS rule ID "%(rule)s": %(e)s') + % {'rule': rule_id, 'e': e}) + raise exceptions.CommandError(msg) + + +class ListNetworkQosRule(command.Lister): + _description = _("List Network QoS rules") + + def get_parser(self, prog_name): + parser = super(ListNetworkQosRule, self).get_parser(prog_name) + parser.add_argument( + 'qos_policy', + metavar='<qos-policy>', + help=_('QoS policy that contains the rule (name or ID)') + ) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + columns = ( + 'id', + 'qos_policy_id', + 'type', + 'max_kbps', + 'max_burst_kbps', + 'min_kbps', + 'dscp_mark', + 'direction', + ) + column_headers = ( + 'ID', + 'QoS Policy ID', + 'Type', + 'Max Kbps', + 'Max Burst Kbits', + 'Min Kbps', + 'DSCP mark', + 'Direction', + ) + qos = client.find_qos_policy(parsed_args.qos_policy, + ignore_missing=False) + data = qos.rules + return (column_headers, + (_get_item_properties(s, columns) for s in data)) + + +class SetNetworkQosRule(command.Command): + _description = _("Set Network QoS rule properties") + + def get_parser(self, prog_name): + parser = super(SetNetworkQosRule, self).get_parser(prog_name) + parser.add_argument( + 'qos_policy', + metavar='<qos-policy>', + help=_('QoS policy that contains the rule (name or ID)') + ) + parser.add_argument( + 'id', + metavar='<rule-id>', + help=_('Network QoS rule to delete (ID)') + ) + _add_rule_arguments(parser) + return parser + + def take_action(self, parsed_args): + network_client = self.app.client_manager.network + try: + qos = network_client.find_qos_policy(parsed_args.qos_policy, + ignore_missing=False) + rule_type = _find_rule_type(qos, parsed_args.id) + if not rule_type: + raise Exception('Rule not found') + attrs = _get_attrs(network_client, parsed_args) + qos_id = attrs.pop('qos_policy_id') + qos_rule = _rule_action_call(network_client, ACTION_FIND, + rule_type)(attrs.pop('id'), qos_id) + _rule_action_call(network_client, ACTION_SET, rule_type)( + qos_rule, qos_id, **attrs) + except Exception as e: + msg = (_('Failed to set Network QoS rule ID "%(rule)s": %(e)s') % + {'rule': parsed_args.id, 'e': e}) + raise exceptions.CommandError(msg) + + +class ShowNetworkQosRule(command.ShowOne): + _description = _("Display Network QoS rule details") + + def get_parser(self, prog_name): + parser = super(ShowNetworkQosRule, self).get_parser(prog_name) + parser.add_argument( + 'qos_policy', + metavar='<qos-policy>', + help=_('QoS policy that contains the rule (name or ID)') + ) + parser.add_argument( + 'id', + metavar='<rule-id>', + help=_('Network QoS rule to delete (ID)') + ) + return parser + + def take_action(self, parsed_args): + network_client = self.app.client_manager.network + rule_id = parsed_args.id + try: + qos = network_client.find_qos_policy(parsed_args.qos_policy, + ignore_missing=False) + rule_type = _find_rule_type(qos, rule_id) + if not rule_type: + raise Exception('Rule not found') + obj = _rule_action_call(network_client, ACTION_SHOW, rule_type)( + rule_id, qos.id) + except Exception as e: + msg = (_('Failed to set Network QoS rule ID "%(rule)s": %(e)s') % + {'rule': rule_id, 'e': e}) + raise exceptions.CommandError(msg) + display_columns, columns = _get_columns(obj) + data = utils.get_item_properties(obj, columns) + return display_columns, data diff --git a/openstackclient/network/v2/security_group_rule.py b/openstackclient/network/v2/security_group_rule.py index b878d875..4fb62c7b 100644 --- a/openstackclient/network/v2/security_group_rule.py +++ b/openstackclient/network/v2/security_group_rule.py @@ -51,17 +51,17 @@ def _format_network_port_range(rule): # - Single port: '80:80' # - No port range: '' port_range = '' - if _is_icmp_protocol(rule.protocol): - if rule.port_range_min: - port_range += 'type=' + str(rule.port_range_min) - if rule.port_range_max: - port_range += ':code=' + str(rule.port_range_max) - elif rule.port_range_min or rule.port_range_max: - port_range_min = str(rule.port_range_min) - port_range_max = str(rule.port_range_max) - if rule.port_range_min is None: + if _is_icmp_protocol(rule['protocol']): + if rule['port_range_min']: + port_range += 'type=' + str(rule['port_range_min']) + if rule['port_range_max']: + port_range += ':code=' + str(rule['port_range_max']) + elif rule['port_range_min'] or rule['port_range_max']: + port_range_min = str(rule['port_range_min']) + port_range_max = str(rule['port_range_max']) + if rule['port_range_min'] is None: port_range_min = port_range_max - if rule.port_range_max is None: + if rule['port_range_max'] is None: port_range_max = port_range_min port_range = port_range_min + ':' + port_range_max return port_range @@ -423,6 +423,16 @@ class DeleteSecurityGroupRule(common.NetworkAndComputeDelete): class ListSecurityGroupRule(common.NetworkAndComputeLister): _description = _("List security group rules") + def _format_network_security_group_rule(self, rule): + """Transform the SDK SecurityGroupRule object to a dict + + The SDK object gets in the way of reformatting columns... + Create port_range column from port_range_min and port_range_max + """ + rule = rule.to_dict() + rule['port_range'] = _format_network_port_range(rule) + return rule + def update_parser_common(self, parser): parser.add_argument( 'group', @@ -508,7 +518,7 @@ class ListSecurityGroupRule(common.NetworkAndComputeLister): 'id', 'protocol', 'remote_ip_prefix', - 'port_range_min', + 'port_range', ) if parsed_args.long: columns = columns + ('direction', 'ethertype',) @@ -535,16 +545,13 @@ class ListSecurityGroupRule(common.NetworkAndComputeLister): if parsed_args.protocol is not None: query['protocol'] = parsed_args.protocol - rules = list(client.security_group_rules(**query)) - - # Reformat the rules to display a port range instead - # of just the port range minimum. This maintains - # output compatibility with compute. - for rule in rules: - rule.port_range_min = _format_network_port_range(rule) + rules = [ + self._format_network_security_group_rule(r) + for r in client.security_group_rules(**query) + ] return (column_headers, - (utils.get_item_properties( + (utils.get_dict_properties( s, columns, ) for s in rules)) diff --git a/openstackclient/tests/functional/base.py b/openstackclient/tests/functional/base.py index 885abc02..fb78ea62 100644 --- a/openstackclient/tests/functional/base.py +++ b/openstackclient/tests/functional/base.py @@ -77,6 +77,11 @@ class TestCase(testtools.TestCase): if expected not in actual: raise Exception(expected + ' not in ' + actual) + @classmethod + def assertsOutputNotNone(cls, observed): + if observed is None: + raise Exception('No output observed') + def assert_table_structure(self, items, field_names): """Verify that all items have keys listed in field_names.""" for item in items: diff --git a/openstackclient/tests/functional/common/test_quota.py b/openstackclient/tests/functional/common/test_quota.py index fbb8e563..b2b198af 100644 --- a/openstackclient/tests/functional/common/test_quota.py +++ b/openstackclient/tests/functional/common/test_quota.py @@ -35,19 +35,16 @@ class QuotaTests(base.TestCase): raw_output = self.openstack('quota show ' + self.PROJECT_NAME + opts) self.assertEqual("11\n11\n11\n", raw_output) - @testtools.skip('broken SDK testing') def test_quota_show(self): raw_output = self.openstack('quota show ' + self.PROJECT_NAME) for expected_field in self.EXPECTED_FIELDS: self.assertIn(expected_field, raw_output) - @testtools.skip('broken SDK testing') def test_quota_show_default_project(self): raw_output = self.openstack('quota show') for expected_field in self.EXPECTED_FIELDS: self.assertIn(expected_field, raw_output) - @testtools.skip('broken SDK testing') def test_quota_show_with_default_option(self): raw_output = self.openstack('quota show --default') for expected_field in self.EXPECTED_FIELDS: diff --git a/openstackclient/tests/functional/image/v2/test_image.py b/openstackclient/tests/functional/image/v2/test_image.py index 3f432b02..6faff94a 100644 --- a/openstackclient/tests/functional/image/v2/test_image.py +++ b/openstackclient/tests/functional/image/v2/test_image.py @@ -74,3 +74,25 @@ class ImageTests(base.TestCase): self.openstack('image unset --property a --property c ' + self.NAME) raw_output = self.openstack('image show ' + self.NAME + opts) self.assertEqual(self.NAME + "\n\n", raw_output) + + def test_image_members(self): + opts = self.get_opts(['project_id']) + my_project_id = self.openstack('token issue' + opts).strip() + self.openstack( + 'image add project {} {}'.format(self.NAME, my_project_id)) + + self.openstack( + 'image set --accept ' + self.NAME) + shared_img_list = self.parse_listing( + self.openstack('image list --shared', self.get_opts(['name'])) + ) + self.assertIn(self.NAME, [img['Name'] for img in shared_img_list]) + + self.openstack( + 'image set --reject ' + self.NAME) + shared_img_list = self.parse_listing( + self.openstack('image list --shared', self.get_opts(['name'])) + ) + + self.openstack( + 'image remove project {} {}'.format(self.NAME, my_project_id)) diff --git a/openstackclient/tests/functional/network/v2/test_floating_ip.py b/openstackclient/tests/functional/network/v2/test_floating_ip.py index 5f642f04..fa9607a0 100644 --- a/openstackclient/tests/functional/network/v2/test_floating_ip.py +++ b/openstackclient/tests/functional/network/v2/test_floating_ip.py @@ -14,8 +14,6 @@ import random import re import uuid -import testtools - from openstackclient.tests.functional import base @@ -25,7 +23,6 @@ class FloatingIpTests(base.TestCase): NETWORK_NAME = uuid.uuid4().hex @classmethod - @testtools.skip('broken SDK testing') def setUpClass(cls): # Set up some regex for matching below cls.re_id = re.compile("id\s+\|\s+(\S+)") @@ -37,7 +34,7 @@ class FloatingIpTests(base.TestCase): # Make a random subnet cls.subnet = ".".join(map( str, - (random.randint(0, 255) for _ in range(3)) + (random.randint(0, 223) for _ in range(3)) )) + ".0/26" # Create a network for the floating ip @@ -62,7 +59,6 @@ class FloatingIpTests(base.TestCase): raw_output = cls.openstack('network delete ' + cls.NETWORK_NAME) cls.assertOutput('', raw_output) - @testtools.skip('broken SDK testing') def test_floating_ip_delete(self): """Test create, delete multiple""" raw_output = self.openstack( @@ -93,7 +89,6 @@ class FloatingIpTests(base.TestCase): raw_output = self.openstack('floating ip delete ' + ip1 + ' ' + ip2) self.assertOutput('', raw_output) - @testtools.skip('broken SDK testing') def test_floating_ip_list(self): """Test create defaults, list filters, delete""" raw_output = self.openstack( @@ -135,7 +130,6 @@ class FloatingIpTests(base.TestCase): # TODO(dtroyer): add more filter tests - @testtools.skip('broken SDK testing') def test_floating_ip_show(self): """Test show""" raw_output = self.openstack( diff --git a/openstackclient/tests/functional/network/v2/test_network.py b/openstackclient/tests/functional/network/v2/test_network.py index ef42dcce..c55d70f9 100644 --- a/openstackclient/tests/functional/network/v2/test_network.py +++ b/openstackclient/tests/functional/network/v2/test_network.py @@ -10,164 +10,173 @@ # License for the specific language governing permissions and limitations # under the License. -import re +import json import uuid -import testtools - from openstackclient.tests.functional import base class NetworkTests(base.TestCase): """Functional tests for network""" - @classmethod - def setUpClass(cls): - # Set up some regex for matching below - cls.re_id = re.compile("id\s+\|\s+(\S+)") - cls.re_description = re.compile("description\s+\|\s+([^|]+?)\s+\|") - cls.re_enabled = re.compile("admin_state_up\s+\|\s+(\S+)") - cls.re_shared = re.compile("shared\s+\|\s+(\S+)") - cls.re_external = re.compile("router:external\s+\|\s+(\S+)") - cls.re_default = re.compile("is_default\s+\|\s+(\S+)") - cls.re_port_security = re.compile( - "port_security_enabled\s+\|\s+(\S+)" - ) - def test_network_delete(self): """Test create, delete multiple""" name1 = uuid.uuid4().hex - raw_output = self.openstack( - 'network create ' + + cmd_output = json.loads(self.openstack( + 'network create -f json ' + '--description aaaa ' + name1 - ) + )) + self.assertIsNotNone(cmd_output["id"]) self.assertEqual( 'aaaa', - re.search(self.re_description, raw_output).group(1), + cmd_output["description"], ) + name2 = uuid.uuid4().hex - raw_output = self.openstack( - 'network create ' + + cmd_output = json.loads(self.openstack( + 'network create -f json ' + '--description bbbb ' + name2 - ) + )) + self.assertIsNotNone(cmd_output["id"]) self.assertEqual( 'bbbb', - re.search(self.re_description, raw_output).group(1), + cmd_output["description"], ) del_output = self.openstack('network delete ' + name1 + ' ' + name2) self.assertOutput('', del_output) - @testtools.skip('broken SDK testing') def test_network_list(self): """Test create defaults, list filters, delete""" name1 = uuid.uuid4().hex - raw_output = self.openstack( - 'network create ' + + cmd_output = json.loads(self.openstack( + 'network create -f json ' + '--description aaaa ' + name1 - ) + )) self.addCleanup(self.openstack, 'network delete ' + name1) + self.assertIsNotNone(cmd_output["id"]) self.assertEqual( 'aaaa', - re.search(self.re_description, raw_output).group(1), + cmd_output["description"], ) # Check the default values self.assertEqual( 'UP', - re.search(self.re_enabled, raw_output).group(1), + cmd_output["admin_state_up"], ) self.assertEqual( - 'False', - re.search(self.re_shared, raw_output).group(1), + False, + cmd_output["shared"], ) self.assertEqual( 'Internal', - re.search(self.re_external, raw_output).group(1), + cmd_output["router:external"], ) # NOTE(dtroyer): is_default is not present in the create output # so make sure it stays that way. - self.assertIsNone(re.search(self.re_default, raw_output)) + # NOTE(stevemar): is_default *is* present in SDK 0.9.11 and newer, + # but the value seems to always be None, regardless + # of the --default or --no-default value. + # self.assertEqual('x', cmd_output) + if ('is_default' in cmd_output): + self.assertEqual( + None, + cmd_output["is_default"], + ) self.assertEqual( - 'True', - re.search(self.re_port_security, raw_output).group(1), + True, + cmd_output["port_security_enabled"], ) name2 = uuid.uuid4().hex - raw_output = self.openstack( - 'network create ' + + cmd_output = json.loads(self.openstack( + 'network create -f json ' + '--description bbbb ' + '--disable ' + '--share ' + name2 - ) + )) self.addCleanup(self.openstack, 'network delete ' + name2) + self.assertIsNotNone(cmd_output["id"]) self.assertEqual( 'bbbb', - re.search(self.re_description, raw_output).group(1), + cmd_output["description"], ) self.assertEqual( 'DOWN', - re.search(self.re_enabled, raw_output).group(1), + cmd_output["admin_state_up"], ) self.assertEqual( - 'True', - re.search(self.re_shared, raw_output).group(1), + True, + cmd_output["shared"], + ) + if ('is_default' in cmd_output): + self.assertEqual( + None, + cmd_output["is_default"], + ) + self.assertEqual( + True, + cmd_output["port_security_enabled"], ) # Test list --long - raw_output = self.openstack('network list --long') - self.assertIsNotNone( - re.search("\|\s+" + name1 + "\s+\|\s+ACTIVE", raw_output) - ) - self.assertIsNotNone( - re.search("\|\s+" + name2 + "\s+\|\s+ACTIVE", raw_output) - ) + cmd_output = json.loads(self.openstack( + "network list -f json " + + "--long" + )) + col_name = [x["Name"] for x in cmd_output] + self.assertIn(name1, col_name) + self.assertIn(name2, col_name) # Test list --long --enable - raw_output = self.openstack('network list --long --enable') - self.assertIsNotNone( - re.search("\|\s+" + name1 + "\s+\|\s+ACTIVE", raw_output) - ) - self.assertIsNone( - re.search("\|\s+" + name2 + "\s+\|\s+ACTIVE", raw_output) - ) + cmd_output = json.loads(self.openstack( + "network list -f json " + + "--enable " + + "--long" + )) + col_name = [x["Name"] for x in cmd_output] + self.assertIn(name1, col_name) + self.assertNotIn(name2, col_name) # Test list --long --disable - raw_output = self.openstack('network list --long --disable') - self.assertIsNone( - re.search("\|\s+" + name1 + "\s+\|\s+ACTIVE", raw_output) - ) - self.assertIsNotNone( - re.search("\|\s+" + name2 + "\s+\|\s+ACTIVE", raw_output) - ) + cmd_output = json.loads(self.openstack( + "network list -f json " + + "--disable " + + "--long" + )) + col_name = [x["Name"] for x in cmd_output] + self.assertNotIn(name1, col_name) + self.assertIn(name2, col_name) # Test list --long --share - raw_output = self.openstack('network list --long --share') - self.assertIsNone( - re.search("\|\s+" + name1 + "\s+\|\s+ACTIVE", raw_output) - ) - self.assertIsNotNone( - re.search("\|\s+" + name2 + "\s+\|\s+ACTIVE", raw_output) - ) + cmd_output = json.loads(self.openstack( + "network list -f json " + + "--share " + + "--long" + )) + col_name = [x["Name"] for x in cmd_output] + self.assertNotIn(name1, col_name) + self.assertIn(name2, col_name) # Test list --long --no-share - raw_output = self.openstack('network list --long --no-share') - self.assertIsNotNone( - re.search("\|\s+" + name1 + "\s+\|\s+ACTIVE", raw_output) - ) - self.assertIsNone( - re.search("\|\s+" + name2 + "\s+\|\s+ACTIVE", raw_output) - ) + cmd_output = json.loads(self.openstack( + "network list -f json " + + "--no-share " + + "--long" + )) + col_name = [x["Name"] for x in cmd_output] + self.assertIn(name1, col_name) + self.assertNotIn(name2, col_name) - @testtools.skip('broken SDK testing') def test_network_set(self): """Tests create options, set, show, delete""" name = uuid.uuid4().hex - raw_output = self.openstack( - 'network create ' + + cmd_output = json.loads(self.openstack( + 'network create -f json ' + '--description aaaa ' + '--enable ' + '--no-share ' + @@ -175,30 +184,38 @@ class NetworkTests(base.TestCase): '--no-default ' + '--enable-port-security ' + name - ) + )) self.addCleanup(self.openstack, 'network delete ' + name) + self.assertIsNotNone(cmd_output["id"]) self.assertEqual( 'aaaa', - re.search(self.re_description, raw_output).group(1), + cmd_output["description"], ) self.assertEqual( 'UP', - re.search(self.re_enabled, raw_output).group(1), + cmd_output["admin_state_up"], ) self.assertEqual( - 'False', - re.search(self.re_shared, raw_output).group(1), + False, + cmd_output["shared"], ) self.assertEqual( 'Internal', - re.search(self.re_external, raw_output).group(1), + cmd_output["router:external"], ) # NOTE(dtroyer): is_default is not present in the create output # so make sure it stays that way. - self.assertIsNone(re.search(self.re_default, raw_output)) + # NOTE(stevemar): is_default *is* present in SDK 0.9.11 and newer, + # but the value seems to always be None, regardless + # of the --default or --no-default value. + if ('is_default' in cmd_output): + self.assertEqual( + None, + cmd_output["is_default"], + ) self.assertEqual( - 'True', - re.search(self.re_port_security, raw_output).group(1), + True, + cmd_output["port_security_enabled"], ) raw_output = self.openstack( @@ -212,32 +229,34 @@ class NetworkTests(base.TestCase): ) self.assertOutput('', raw_output) - raw_output = self.openstack('network show ' + name) + cmd_output = json.loads(self.openstack( + 'network show -f json ' + name + )) self.assertEqual( 'cccc', - re.search(self.re_description, raw_output).group(1), + cmd_output["description"], ) self.assertEqual( 'DOWN', - re.search(self.re_enabled, raw_output).group(1), + cmd_output["admin_state_up"], ) self.assertEqual( - 'True', - re.search(self.re_shared, raw_output).group(1), + True, + cmd_output["shared"], ) self.assertEqual( 'External', - re.search(self.re_external, raw_output).group(1), + cmd_output["router:external"], ) # why not 'None' like above?? self.assertEqual( - 'False', - re.search(self.re_default, raw_output).group(1), + False, + cmd_output["is_default"], ) self.assertEqual( - 'False', - re.search(self.re_port_security, raw_output).group(1), + False, + cmd_output["port_security_enabled"], ) # NOTE(dtroyer): There is ambiguity around is_default in that @@ -252,14 +271,16 @@ class NetworkTests(base.TestCase): ) self.assertOutput('', raw_output) - raw_output = self.openstack('network show ' + name) + cmd_output = json.loads(self.openstack( + 'network show -f json ' + name + )) self.assertEqual( 'cccc', - re.search(self.re_description, raw_output).group(1), + cmd_output["description"], ) # NOTE(dtroyer): This should be 'True' self.assertEqual( - 'False', - re.search(self.re_default, raw_output).group(1), + False, + cmd_output["port_security_enabled"], ) diff --git a/openstackclient/tests/functional/network/v2/test_network_agent.py b/openstackclient/tests/functional/network/v2/test_network_agent.py index e99dcef6..dd6112e7 100644 --- a/openstackclient/tests/functional/network/v2/test_network_agent.py +++ b/openstackclient/tests/functional/network/v2/test_network_agent.py @@ -10,8 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools - from openstackclient.tests.functional import base @@ -28,13 +26,11 @@ class NetworkAgentTests(base.TestCase): # get the list of network agent IDs. cls.IDs = raw_output.split('\n') - @testtools.skip('broken SDK testing') def test_network_agent_show(self): opts = self.get_opts(self.FIELDS) raw_output = self.openstack('network agent show ' + self.IDs[0] + opts) self.assertEqual(self.IDs[0] + "\n", raw_output) - @testtools.skip('broken SDK testing') def test_network_agent_set(self): opts = self.get_opts(['admin_state_up']) self.openstack('network agent set --disable ' + self.IDs[0]) diff --git a/openstackclient/tests/functional/network/v2/test_network_qos_rule.py b/openstackclient/tests/functional/network/v2/test_network_qos_rule.py new file mode 100644 index 00000000..af0c9bac --- /dev/null +++ b/openstackclient/tests/functional/network/v2/test_network_qos_rule.py @@ -0,0 +1,181 @@ +# Copyright (c) 2016, Intel Corporation. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import uuid + +from openstackclient.tests.functional import base + + +class NetworkQosRuleTestsMinimumBandwidth(base.TestCase): + """Functional tests for QoS minimum bandwidth rule.""" + RULE_ID = None + QOS_POLICY_NAME = 'qos_policy_' + uuid.uuid4().hex + MIN_KBPS = 2800 + MIN_KBPS_MODIFIED = 7500 + DIRECTION = '--egress' + HEADERS = ['ID'] + FIELDS = ['id'] + TYPE = 'minimum-bandwidth' + + @classmethod + def setUpClass(cls): + opts = cls.get_opts(cls.FIELDS) + cls.openstack('network qos policy create ' + cls.QOS_POLICY_NAME) + cls.RULE_ID = cls.openstack('network qos rule create --type ' + + cls.TYPE + ' --min-kbps ' + + str(cls.MIN_KBPS) + ' ' + cls.DIRECTION + + ' ' + cls.QOS_POLICY_NAME + opts) + cls.assertsOutputNotNone(cls.RULE_ID) + + @classmethod + def tearDownClass(cls): + raw_output = cls.openstack('network qos rule delete ' + + cls.QOS_POLICY_NAME + ' ' + cls.RULE_ID) + cls.openstack('network qos policy delete ' + cls.QOS_POLICY_NAME) + cls.assertOutput('', raw_output) + + def test_qos_policy_list(self): + opts = self.get_opts(self.HEADERS) + raw_output = self.openstack('network qos rule list ' + + self.QOS_POLICY_NAME + opts) + self.assertIn(self.RULE_ID, raw_output) + + def test_qos_policy_show(self): + opts = self.get_opts(self.FIELDS) + raw_output = self.openstack('network qos rule show ' + + self.QOS_POLICY_NAME + ' ' + self.RULE_ID + + opts) + self.assertEqual(self.RULE_ID, raw_output) + + def test_qos_policy_set(self): + self.openstack('network qos rule set --min-kbps ' + + str(self.MIN_KBPS_MODIFIED) + ' ' + + self.QOS_POLICY_NAME + ' ' + self.RULE_ID) + opts = self.get_opts(['min_kbps']) + raw_output = self.openstack('network qos rule show ' + + self.QOS_POLICY_NAME + ' ' + self.RULE_ID + + opts) + self.assertEqual(str(self.MIN_KBPS_MODIFIED) + "\n", raw_output) + + +class NetworkQosRuleTestsDSCPMarking(base.TestCase): + """Functional tests for QoS DSCP marking rule.""" + RULE_ID = None + QOS_POLICY_NAME = 'qos_policy_' + uuid.uuid4().hex + DSCP_MARK = 8 + DSCP_MARK_MODIFIED = 32 + HEADERS = ['ID'] + FIELDS = ['id'] + TYPE = 'dscp-marking' + + @classmethod + def setUpClass(cls): + opts = cls.get_opts(cls.FIELDS) + cls.openstack('network qos policy create ' + cls.QOS_POLICY_NAME) + cls.RULE_ID = cls.openstack('network qos rule create --type ' + + cls.TYPE + ' --dscp-mark ' + + str(cls.DSCP_MARK) + ' ' + + cls.QOS_POLICY_NAME + opts) + cls.assertsOutputNotNone(cls.RULE_ID) + + @classmethod + def tearDownClass(cls): + raw_output = cls.openstack('network qos rule delete ' + + cls.QOS_POLICY_NAME + ' ' + cls.RULE_ID) + cls.openstack('network qos policy delete ' + cls.QOS_POLICY_NAME) + cls.assertOutput('', raw_output) + + def test_qos_policy_list(self): + opts = self.get_opts(self.HEADERS) + raw_output = self.openstack('network qos rule list ' + + self.QOS_POLICY_NAME + opts) + self.assertIn(self.RULE_ID, raw_output) + + def test_qos_policy_show(self): + opts = self.get_opts(self.FIELDS) + raw_output = self.openstack('network qos rule show ' + + self.QOS_POLICY_NAME + ' ' + self.RULE_ID + + opts) + self.assertEqual(self.RULE_ID, raw_output) + + def test_qos_policy_set(self): + self.openstack('network qos rule set --dscp-mark ' + + str(self.DSCP_MARK_MODIFIED) + ' ' + + self.QOS_POLICY_NAME + ' ' + self.RULE_ID) + opts = self.get_opts(['dscp_mark']) + raw_output = self.openstack('network qos rule show ' + + self.QOS_POLICY_NAME + ' ' + self.RULE_ID + + opts) + self.assertEqual(str(self.DSCP_MARK_MODIFIED) + "\n", raw_output) + + +class NetworkQosRuleTestsBandwidthLimit(base.TestCase): + """Functional tests for QoS bandwidth limit rule.""" + RULE_ID = None + QOS_POLICY_NAME = 'qos_policy_' + uuid.uuid4().hex + MAX_KBPS = 10000 + MAX_KBPS_MODIFIED = 15000 + MAX_BURST_KBITS = 1400 + MAX_BURST_KBITS_MODIFIED = 1800 + HEADERS = ['ID'] + FIELDS = ['id'] + TYPE = 'bandwidth-limit' + + @classmethod + def setUpClass(cls): + opts = cls.get_opts(cls.FIELDS) + cls.openstack('network qos policy create ' + cls.QOS_POLICY_NAME) + cls.RULE_ID = cls.openstack('network qos rule create --type ' + + cls.TYPE + ' --max-kbps ' + + str(cls.MAX_KBPS) + ' --max-burst-kbits ' + + str(cls.MAX_BURST_KBITS) + ' ' + + cls.QOS_POLICY_NAME + opts) + cls.assertsOutputNotNone(cls.RULE_ID) + + @classmethod + def tearDownClass(cls): + raw_output = cls.openstack('network qos rule delete ' + + cls.QOS_POLICY_NAME + ' ' + cls.RULE_ID) + cls.openstack('network qos policy delete ' + cls.QOS_POLICY_NAME) + cls.assertOutput('', raw_output) + + def test_qos_policy_list(self): + opts = self.get_opts(self.HEADERS) + raw_output = self.openstack('network qos rule list ' + + self.QOS_POLICY_NAME + opts) + self.assertIn(self.RULE_ID, raw_output) + + def test_qos_policy_show(self): + opts = self.get_opts(self.FIELDS) + raw_output = self.openstack('network qos rule show ' + + self.QOS_POLICY_NAME + ' ' + self.RULE_ID + + opts) + self.assertEqual(self.RULE_ID, raw_output) + + def test_qos_policy_set(self): + self.openstack('network qos rule set --max-kbps ' + + str(self.MAX_KBPS_MODIFIED) + ' --max-burst-kbits ' + + str(self.MAX_BURST_KBITS_MODIFIED) + ' ' + + self.QOS_POLICY_NAME + ' ' + self.RULE_ID) + opts = self.get_opts(['max_kbps']) + raw_output = self.openstack('network qos rule show ' + + self.QOS_POLICY_NAME + ' ' + self.RULE_ID + + opts) + self.assertEqual(str(self.MAX_KBPS_MODIFIED) + "\n", raw_output) + opts = self.get_opts(['max_burst_kbps']) + raw_output = self.openstack('network qos rule show ' + + self.QOS_POLICY_NAME + ' ' + self.RULE_ID + + opts) + self.assertEqual(str(self.MAX_BURST_KBITS_MODIFIED) + "\n", raw_output) diff --git a/openstackclient/tests/functional/network/v2/test_network_qos_rule_type.py b/openstackclient/tests/functional/network/v2/test_network_qos_rule_type.py index 2bb04a9d..7dff0cbd 100644 --- a/openstackclient/tests/functional/network/v2/test_network_qos_rule_type.py +++ b/openstackclient/tests/functional/network/v2/test_network_qos_rule_type.py @@ -13,8 +13,6 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools - from openstackclient.tests.functional import base @@ -22,10 +20,8 @@ class NetworkQosRuleTypeTests(base.TestCase): """Functional tests for Network QoS rule type. """ AVAILABLE_RULE_TYPES = ['dscp_marking', - 'bandwidth_limit', - 'minimum_bandwidth'] + 'bandwidth_limit'] - @testtools.skip('broken SDK testing') def test_qos_rule_type_list(self): raw_output = self.openstack('network qos rule type list') for rule_type in self.AVAILABLE_RULE_TYPES: diff --git a/openstackclient/tests/functional/network/v2/test_port.py b/openstackclient/tests/functional/network/v2/test_port.py index 976fbedb..decd9553 100644 --- a/openstackclient/tests/functional/network/v2/test_port.py +++ b/openstackclient/tests/functional/network/v2/test_port.py @@ -12,8 +12,6 @@ import uuid -import testtools - from openstackclient.tests.functional import base @@ -25,7 +23,6 @@ class PortTests(base.TestCase): FIELDS = ['name'] @classmethod - @testtools.skip('broken SDK testing') def setUpClass(cls): # Create a network for the subnet. cls.openstack('network create ' + cls.NETWORK_NAME) diff --git a/openstackclient/tests/functional/network/v2/test_security_group_rule.py b/openstackclient/tests/functional/network/v2/test_security_group_rule.py index ec3731eb..c91de1a5 100644 --- a/openstackclient/tests/functional/network/v2/test_security_group_rule.py +++ b/openstackclient/tests/functional/network/v2/test_security_group_rule.py @@ -12,8 +12,6 @@ import uuid -import testtools - from openstackclient.tests.functional import base @@ -54,7 +52,6 @@ class SecurityGroupRuleTests(base.TestCase): cls.SECURITY_GROUP_NAME) cls.assertOutput('', raw_output) - @testtools.skip('broken SDK testing') def test_security_group_rule_list(self): opts = self.get_opts(self.ID_HEADER) raw_output = self.openstack('security group rule list ' + diff --git a/openstackclient/tests/functional/post_test_hook_tips.sh b/openstackclient/tests/functional/post_test_hook_tips.sh new file mode 100755 index 00000000..28ab9580 --- /dev/null +++ b/openstackclient/tests/functional/post_test_hook_tips.sh @@ -0,0 +1,47 @@ +#!/bin/bash + +# This is a script that kicks off a series of functional tests against an +# OpenStack cloud. It will attempt to create an instance if one is not +# available. Do not run this script unless you know what you're doing. +# For more information refer to: +# http://docs.openstack.org/developer/python-openstackclient/ + +# This particular script differs from the normal post_test_hook because +# it installs the master (tip) version of osc-lib, os-client-config +# and openstacksdk, OSCs most important dependencies. + +function generate_testr_results { + if [ -f .testrepository/0 ]; then + sudo .tox/functional-tips/bin/testr last --subunit > $WORKSPACE/testrepository.subunit + sudo mv $WORKSPACE/testrepository.subunit $BASE/logs/testrepository.subunit + sudo .tox/functional-tips/bin/subunit2html $BASE/logs/testrepository.subunit $BASE/logs/testr_results.html + sudo gzip -9 $BASE/logs/testrepository.subunit + sudo gzip -9 $BASE/logs/testr_results.html + sudo chown jenkins:jenkins $BASE/logs/testrepository.subunit.gz $BASE/logs/testr_results.html.gz + sudo chmod a+r $BASE/logs/testrepository.subunit.gz $BASE/logs/testr_results.html.gz + fi +} + +export OPENSTACKCLIENT_DIR="$BASE/new/python-openstackclient" +sudo chown -R jenkins:stack $OPENSTACKCLIENT_DIR + +# Go to the openstackclient dir +cd $OPENSTACKCLIENT_DIR + +# Run tests +echo "Running openstackclient functional-tips test suite" +set +e + +# Source environment variables to kick things off +source ~stack/devstack/openrc admin admin +echo 'Running tests with:' +env | grep OS + +# Preserve env for OS_ credentials +sudo -E -H -u jenkins tox -e functional-tips +EXIT_CODE=$? +set -e + +# Collect and parse result +generate_testr_results +exit $EXIT_CODE diff --git a/openstackclient/tests/unit/fakes.py b/openstackclient/tests/unit/fakes.py index ca6b1d31..626b466d 100644 --- a/openstackclient/tests/unit/fakes.py +++ b/openstackclient/tests/unit/fakes.py @@ -219,6 +219,12 @@ class FakeResource(object): def info(self): return self._info + def __getitem__(self, item): + return self._info.get(item) + + def get(self, item, default=None): + return self._info.get(item, default) + class FakeResponse(requests.Response): diff --git a/openstackclient/tests/unit/identity/v2_0/test_project.py b/openstackclient/tests/unit/identity/v2_0/test_project.py index c1f00762..4e1077db 100644 --- a/openstackclient/tests/unit/identity/v2_0/test_project.py +++ b/openstackclient/tests/unit/identity/v2_0/test_project.py @@ -13,8 +13,11 @@ # under the License. # +import mock + from keystoneauth1 import exceptions as ks_exc from osc_lib import exceptions +from osc_lib import utils from openstackclient.identity.v2_0 import project from openstackclient.tests.unit.identity.v2_0 import fakes as identity_fakes @@ -302,6 +305,32 @@ class TestProjectDelete(TestProject): ) self.assertIsNone(result) + @mock.patch.object(utils, 'find_resource') + def test_delete_multi_projects_with_exception(self, find_mock): + find_mock.side_effect = [self.fake_project, + exceptions.CommandError] + arglist = [ + self.fake_project.id, + 'unexist_project', + ] + verifylist = [ + ('projects', arglist), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + try: + self.cmd.take_action(parsed_args) + self.fail('CommandError should be raised.') + except exceptions.CommandError as e: + self.assertEqual('1 of 2 projects failed to delete.', + str(e)) + + find_mock.assert_any_call(self.projects_mock, self.fake_project.id) + find_mock.assert_any_call(self.projects_mock, 'unexist_project') + + self.assertEqual(2, find_mock.call_count) + self.projects_mock.delete.assert_called_once_with(self.fake_project.id) + class TestProjectList(TestProject): diff --git a/openstackclient/tests/unit/identity/v2_0/test_role.py b/openstackclient/tests/unit/identity/v2_0/test_role.py index 68ebf141..684ce803 100644 --- a/openstackclient/tests/unit/identity/v2_0/test_role.py +++ b/openstackclient/tests/unit/identity/v2_0/test_role.py @@ -17,6 +17,7 @@ import mock from keystoneauth1 import exceptions as ks_exc from osc_lib import exceptions +from osc_lib import utils from openstackclient.identity.v2_0 import role from openstackclient.tests.unit.identity.v2_0 import fakes as identity_fakes @@ -240,6 +241,32 @@ class TestRoleDelete(TestRole): ) self.assertIsNone(result) + @mock.patch.object(utils, 'find_resource') + def test_delete_multi_roles_with_exception(self, find_mock): + find_mock.side_effect = [self.fake_role, + exceptions.CommandError] + arglist = [ + self.fake_role.id, + 'unexist_role', + ] + verifylist = [ + ('roles', arglist), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + try: + self.cmd.take_action(parsed_args) + self.fail('CommandError should be raised.') + except exceptions.CommandError as e: + self.assertEqual('1 of 2 roles failed to delete.', + str(e)) + + find_mock.assert_any_call(self.roles_mock, self.fake_role.id) + find_mock.assert_any_call(self.roles_mock, 'unexist_role') + + self.assertEqual(2, find_mock.call_count) + self.roles_mock.delete.assert_called_once_with(self.fake_role.id) + class TestRoleList(TestRole): diff --git a/openstackclient/tests/unit/identity/v2_0/test_user.py b/openstackclient/tests/unit/identity/v2_0/test_user.py index 765f8559..a8b9497e 100644 --- a/openstackclient/tests/unit/identity/v2_0/test_user.py +++ b/openstackclient/tests/unit/identity/v2_0/test_user.py @@ -17,6 +17,7 @@ import mock from keystoneauth1 import exceptions as ks_exc from osc_lib import exceptions +from osc_lib import utils from openstackclient.identity.v2_0 import user from openstackclient.tests.unit.identity.v2_0 import fakes as identity_fakes @@ -411,6 +412,32 @@ class TestUserDelete(TestUser): ) self.assertIsNone(result) + @mock.patch.object(utils, 'find_resource') + def test_delete_multi_users_with_exception(self, find_mock): + find_mock.side_effect = [self.fake_user, + exceptions.CommandError] + arglist = [ + self.fake_user.id, + 'unexist_user', + ] + verifylist = [ + ('users', arglist), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + try: + self.cmd.take_action(parsed_args) + self.fail('CommandError should be raised.') + except exceptions.CommandError as e: + self.assertEqual('1 of 2 users failed to delete.', + str(e)) + + find_mock.assert_any_call(self.users_mock, self.fake_user.id) + find_mock.assert_any_call(self.users_mock, 'unexist_user') + + self.assertEqual(2, find_mock.call_count) + self.users_mock.delete.assert_called_once_with(self.fake_user.id) + class TestUserList(TestUser): diff --git a/openstackclient/tests/unit/identity/v3/test_group.py b/openstackclient/tests/unit/identity/v3/test_group.py index eb50adb5..8558de95 100644 --- a/openstackclient/tests/unit/identity/v3/test_group.py +++ b/openstackclient/tests/unit/identity/v3/test_group.py @@ -16,6 +16,7 @@ from mock import call from keystoneauth1 import exceptions as ks_exc from osc_lib import exceptions +from osc_lib import utils from openstackclient.identity.v3 import group from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes @@ -257,6 +258,32 @@ class TestGroupDelete(TestGroup): self.groups_mock.delete.assert_called_once_with(self.groups[0].id) self.assertIsNone(result) + @mock.patch.object(utils, 'find_resource') + def test_delete_multi_groups_with_exception(self, find_mock): + find_mock.side_effect = [self.groups[0], + exceptions.CommandError] + arglist = [ + self.groups[0].id, + 'unexist_group', + ] + verifylist = [ + ('groups', arglist), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + try: + self.cmd.take_action(parsed_args) + self.fail('CommandError should be raised.') + except exceptions.CommandError as e: + self.assertEqual('1 of 2 groups failed to delete.', + str(e)) + + find_mock.assert_any_call(self.groups_mock, self.groups[0].id) + find_mock.assert_any_call(self.groups_mock, 'unexist_group') + + self.assertEqual(2, find_mock.call_count) + self.groups_mock.delete.assert_called_once_with(self.groups[0].id) + class TestGroupList(TestGroup): diff --git a/openstackclient/tests/unit/identity/v3/test_project.py b/openstackclient/tests/unit/identity/v3/test_project.py index 702d9209..2b898090 100644 --- a/openstackclient/tests/unit/identity/v3/test_project.py +++ b/openstackclient/tests/unit/identity/v3/test_project.py @@ -16,6 +16,7 @@ import mock from osc_lib import exceptions +from osc_lib import utils from openstackclient.identity.v3 import project from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes @@ -445,6 +446,32 @@ class TestProjectDelete(TestProject): ) self.assertIsNone(result) + @mock.patch.object(utils, 'find_resource') + def test_delete_multi_projects_with_exception(self, find_mock): + find_mock.side_effect = [self.project, + exceptions.CommandError] + arglist = [ + self.project.id, + 'unexist_project', + ] + verifylist = [ + ('projects', arglist), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + try: + self.cmd.take_action(parsed_args) + self.fail('CommandError should be raised.') + except exceptions.CommandError as e: + self.assertEqual('1 of 2 projects failed to delete.', + str(e)) + + find_mock.assert_any_call(self.projects_mock, self.project.id) + find_mock.assert_any_call(self.projects_mock, 'unexist_project') + + self.assertEqual(2, find_mock.call_count) + self.projects_mock.delete.assert_called_once_with(self.project.id) + class TestProjectList(TestProject): diff --git a/openstackclient/tests/unit/identity/v3/test_role.py b/openstackclient/tests/unit/identity/v3/test_role.py index 448e18d3..c0b68bdf 100644 --- a/openstackclient/tests/unit/identity/v3/test_role.py +++ b/openstackclient/tests/unit/identity/v3/test_role.py @@ -14,6 +14,10 @@ # import copy +import mock + +from osc_lib import exceptions +from osc_lib import utils from openstackclient.identity.v3 import role from openstackclient.tests.unit import fakes @@ -428,6 +432,36 @@ class TestRoleDelete(TestRole): ) self.assertIsNone(result) + @mock.patch.object(utils, 'find_resource') + def test_delete_multi_roles_with_exception(self, find_mock): + find_mock.side_effect = [self.roles_mock.get.return_value, + exceptions.CommandError] + arglist = [ + identity_fakes.role_name, + 'unexist_role', + ] + verifylist = [ + ('roles', arglist), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + try: + self.cmd.take_action(parsed_args) + self.fail('CommandError should be raised.') + except exceptions.CommandError as e: + self.assertEqual('1 of 2 roles failed to delete.', + str(e)) + + find_mock.assert_any_call(self.roles_mock, + identity_fakes.role_name, + domain_id=None) + find_mock.assert_any_call(self.roles_mock, + 'unexist_role', + domain_id=None) + + self.assertEqual(2, find_mock.call_count) + self.roles_mock.delete.assert_called_once_with(identity_fakes.role_id) + class TestRoleList(TestRole): diff --git a/openstackclient/tests/unit/identity/v3/test_trust.py b/openstackclient/tests/unit/identity/v3/test_trust.py index 4eeb8bfe..93e8f63d 100644 --- a/openstackclient/tests/unit/identity/v3/test_trust.py +++ b/openstackclient/tests/unit/identity/v3/test_trust.py @@ -12,6 +12,10 @@ # import copy +import mock + +from osc_lib import exceptions +from osc_lib import utils from openstackclient.identity.v3 import trust from openstackclient.tests.unit import fakes @@ -148,6 +152,33 @@ class TestTrustDelete(TestTrust): ) self.assertIsNone(result) + @mock.patch.object(utils, 'find_resource') + def test_delete_multi_trusts_with_exception(self, find_mock): + find_mock.side_effect = [self.trusts_mock.get.return_value, + exceptions.CommandError] + arglist = [ + identity_fakes.trust_id, + 'unexist_trust', + ] + verifylist = [ + ('trust', arglist), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + try: + self.cmd.take_action(parsed_args) + self.fail('CommandError should be raised.') + except exceptions.CommandError as e: + self.assertEqual('1 of 2 trusts failed to delete.', + str(e)) + + find_mock.assert_any_call(self.trusts_mock, identity_fakes.trust_id) + find_mock.assert_any_call(self.trusts_mock, 'unexist_trust') + + self.assertEqual(2, find_mock.call_count) + self.trusts_mock.delete.assert_called_once_with( + identity_fakes.trust_id) + class TestTrustList(TestTrust): diff --git a/openstackclient/tests/unit/identity/v3/test_user.py b/openstackclient/tests/unit/identity/v3/test_user.py index 6150a5f3..3c1f49a6 100644 --- a/openstackclient/tests/unit/identity/v3/test_user.py +++ b/openstackclient/tests/unit/identity/v3/test_user.py @@ -16,6 +16,9 @@ import contextlib import mock +from osc_lib import exceptions +from osc_lib import utils + from openstackclient.identity.v3 import user from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes @@ -465,6 +468,32 @@ class TestUserDelete(TestUser): ) self.assertIsNone(result) + @mock.patch.object(utils, 'find_resource') + def test_delete_multi_users_with_exception(self, find_mock): + find_mock.side_effect = [self.user, + exceptions.CommandError] + arglist = [ + self.user.id, + 'unexist_user', + ] + verifylist = [ + ('users', arglist), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + try: + self.cmd.take_action(parsed_args) + self.fail('CommandError should be raised.') + except exceptions.CommandError as e: + self.assertEqual('1 of 2 users failed to delete.', + str(e)) + + find_mock.assert_any_call(self.users_mock, self.user.id) + find_mock.assert_any_call(self.users_mock, 'unexist_user') + + self.assertEqual(2, find_mock.call_count) + self.users_mock.delete.assert_called_once_with(self.user.id) + class TestUserList(TestUser): diff --git a/openstackclient/tests/unit/image/v2/test_image.py b/openstackclient/tests/unit/image/v2/test_image.py index a054e513..164185df 100644 --- a/openstackclient/tests/unit/image/v2/test_image.py +++ b/openstackclient/tests/unit/image/v2/test_image.py @@ -829,6 +829,11 @@ class TestImageSet(TestImage): self.images_mock.get.return_value = self.model(**image_fakes.IMAGE) self.images_mock.update.return_value = self.model(**image_fakes.IMAGE) + + self.app.client_manager.auth_ref = mock.Mock( + project_id=self.project.id, + ) + # Get the command object to test self.cmd = image.SetImage(self.app, None) @@ -845,6 +850,101 @@ class TestImageSet(TestImage): self.assertIsNone(result) + self.image_members_mock.update.assert_not_called() + + def test_image_set_membership_option_accept(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 + + arglist = [ + '--accept', + image_fakes.image_id, + ] + verifylist = [ + ('accept', True), + ('reject', False), + ('pending', False), + ('image', image_fakes.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', + ) + + # 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) + + 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 + + arglist = [ + '--reject', + image_fakes.image_id, + ] + verifylist = [ + ('accept', False), + ('reject', True), + ('pending', False), + ('image', image_fakes.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, + '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) + + 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 + + arglist = [ + '--pending', + image_fakes.image_id, + ] + verifylist = [ + ('accept', False), + ('reject', False), + ('pending', True), + ('image', image_fakes.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, + '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) + def test_image_set_options(self): arglist = [ '--name', 'new-name', diff --git a/openstackclient/tests/unit/network/v2/fakes.py b/openstackclient/tests/unit/network/v2/fakes.py index b931cb55..524285ab 100644 --- a/openstackclient/tests/unit/network/v2/fakes.py +++ b/openstackclient/tests/unit/network/v2/fakes.py @@ -14,6 +14,8 @@ import argparse import copy import mock +from random import choice +from random import randint import uuid from openstackclient.tests.unit import fakes @@ -37,10 +39,20 @@ QUOTA = { "l7policy": 5, } +RULE_TYPE_BANDWIDTH_LIMIT = 'bandwidth-limit' +RULE_TYPE_DSCP_MARKING = 'dscp-marking' +RULE_TYPE_MINIMUM_BANDWIDTH = 'minimum-bandwidth' +VALID_QOS_RULES = [RULE_TYPE_BANDWIDTH_LIMIT, + RULE_TYPE_DSCP_MARKING, + RULE_TYPE_MINIMUM_BANDWIDTH] +VALID_DSCP_MARKS = [0, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, + 34, 36, 38, 40, 46, 48, 56] + class FakeNetworkV2Client(object): def __init__(self, **kwargs): + self.session = mock.Mock() self.extensions = mock.Mock() self.extensions.resource_class = fakes.FakeResource(None, {}) @@ -553,6 +565,8 @@ class FakeNetworkAgent(object): agent_attrs.update(attrs) agent = fakes.FakeResource(info=copy.deepcopy(agent_attrs), loaded=True) + agent.is_admin_state_up = agent_attrs['admin_state_up'] + agent.is_alive = agent_attrs['alive'] return agent @staticmethod @@ -662,83 +676,90 @@ class FakeNetworkRBAC(object): return mock.Mock(side_effect=rbac_policies) -class FakeNetworkQosBandwidthLimitRule(object): - """Fake one or more QoS bandwidth limit rules.""" +class FakeNetworkQosPolicy(object): + """Fake one or more QoS policies.""" @staticmethod - def create_one_qos_bandwidth_limit_rule(attrs=None): - """Create a fake QoS bandwidth limit rule. + def create_one_qos_policy(attrs=None): + """Create a fake QoS policy. :param Dictionary attrs: A dictionary with all attributes :return: - A FakeResource object with id, qos_policy_id, max_kbps and - max_burst_kbps attributes. + A FakeResource object with name, id, etc. """ attrs = attrs or {} + qos_id = attrs.get('id') or 'qos-policy-id-' + uuid.uuid4().hex + rule_attrs = {'qos_policy_id': qos_id} + rules = [FakeNetworkQosRule.create_one_qos_rule(rule_attrs)] # Set default attributes. - qos_bandwidth_limit_rule_attrs = { - 'id': 'qos-bandwidth-limit-rule-id-' + uuid.uuid4().hex, - 'qos_policy_id': 'qos-policy-id-' + uuid.uuid4().hex, - 'max_kbps': 1500, - 'max_burst_kbps': 1200, + qos_policy_attrs = { + 'name': 'qos-policy-name-' + uuid.uuid4().hex, + 'id': qos_id, + 'tenant_id': 'project-id-' + uuid.uuid4().hex, + 'shared': False, + 'description': 'qos-policy-description-' + uuid.uuid4().hex, + 'rules': rules, } # Overwrite default attributes. - qos_bandwidth_limit_rule_attrs.update(attrs) + qos_policy_attrs.update(attrs) - qos_bandwidth_limit_rule = fakes.FakeResource( - info=copy.deepcopy(qos_bandwidth_limit_rule_attrs), + qos_policy = fakes.FakeResource( + info=copy.deepcopy(qos_policy_attrs), loaded=True) - return qos_bandwidth_limit_rule + # Set attributes with special mapping in OpenStack SDK. + qos_policy.is_shared = qos_policy_attrs['shared'] + qos_policy.project_id = qos_policy_attrs['tenant_id'] + + return qos_policy @staticmethod - def create_qos_bandwidth_limit_rules(attrs=None, count=2): - """Create multiple fake QoS bandwidth limit rules. + def create_qos_policies(attrs=None, count=2): + """Create multiple fake QoS policies. :param Dictionary attrs: A dictionary with all attributes :param int count: - The number of QoS bandwidth limit rules to fake + The number of QoS policies to fake :return: - A list of FakeResource objects faking the QoS bandwidth limit rules + A list of FakeResource objects faking the QoS policies """ qos_policies = [] for i in range(0, count): - qos_policies.append(FakeNetworkQosBandwidthLimitRule. - create_one_qos_bandwidth_limit_rule(attrs)) + qos_policies.append( + FakeNetworkQosPolicy.create_one_qos_policy(attrs)) return qos_policies @staticmethod - def get_qos_bandwidth_limit_rules(qos_rules=None, count=2): - """Get a list of faked QoS bandwidth limit rules. + def get_qos_policies(qos_policies=None, count=2): + """Get an iterable MagicMock object with a list of faked QoS policies. - If QoS bandwidth limit rules list is provided, then initialize the - Mock object with the list. Otherwise create one. + If qos policies list is provided, then initialize the Mock object + with the list. Otherwise create one. :param List address scopes: - A list of FakeResource objects faking QoS bandwidth limit rules + A list of FakeResource objects faking qos policies :param int count: - The number of QoS bandwidth limit rules to fake + The number of QoS policies to fake :return: An iterable Mock object with side_effect set to a list of faked - qos bandwidth limit rules + QoS policies """ - if qos_rules is None: - qos_rules = (FakeNetworkQosBandwidthLimitRule. - create_qos_bandwidth_limit_rules(count)) - return mock.Mock(side_effect=qos_rules) + if qos_policies is None: + qos_policies = FakeNetworkQosPolicy.create_qos_policies(count) + return mock.Mock(side_effect=qos_policies) -class FakeNetworkQosPolicy(object): - """Fake one or more QoS policies.""" +class FakeNetworkQosRule(object): + """Fake one or more Network QoS rules.""" @staticmethod - def create_one_qos_policy(attrs=None): - """Create a fake QoS policy. + def create_one_qos_rule(attrs=None): + """Create a fake Network QoS rule. :param Dictionary attrs: A dictionary with all attributes @@ -746,71 +767,69 @@ class FakeNetworkQosPolicy(object): A FakeResource object with name, id, etc. """ attrs = attrs or {} - qos_id = attrs.get('id') or 'qos-policy-id-' + uuid.uuid4().hex - rule_attrs = {'qos_policy_id': qos_id} - rules = [ - FakeNetworkQosBandwidthLimitRule. - create_one_qos_bandwidth_limit_rule(rule_attrs)] # Set default attributes. - qos_policy_attrs = { - 'name': 'qos-policy-name-' + uuid.uuid4().hex, - 'id': qos_id, + type = attrs.get('type') or choice(VALID_QOS_RULES) + qos_rule_attrs = { + 'id': 'qos-rule-id-' + uuid.uuid4().hex, + 'qos_policy_id': 'qos-policy-id-' + uuid.uuid4().hex, 'tenant_id': 'project-id-' + uuid.uuid4().hex, - 'shared': False, - 'description': 'qos-policy-description-' + uuid.uuid4().hex, - 'rules': rules, + 'type': type, } + if type == RULE_TYPE_BANDWIDTH_LIMIT: + qos_rule_attrs['max_kbps'] = randint(1, 10000) + qos_rule_attrs['max_burst_kbits'] = randint(1, 10000) + elif type == RULE_TYPE_DSCP_MARKING: + qos_rule_attrs['dscp_mark'] = choice(VALID_DSCP_MARKS) + elif type == RULE_TYPE_MINIMUM_BANDWIDTH: + qos_rule_attrs['min_kbps'] = randint(1, 10000) + qos_rule_attrs['direction'] = 'egress' # Overwrite default attributes. - qos_policy_attrs.update(attrs) + qos_rule_attrs.update(attrs) - qos_policy = fakes.FakeResource( - info=copy.deepcopy(qos_policy_attrs), - loaded=True) + qos_rule = fakes.FakeResource(info=copy.deepcopy(qos_rule_attrs), + loaded=True) # Set attributes with special mapping in OpenStack SDK. - qos_policy.is_shared = qos_policy_attrs['shared'] - qos_policy.project_id = qos_policy_attrs['tenant_id'] + qos_rule.project_id = qos_rule['tenant_id'] - return qos_policy + return qos_rule @staticmethod - def create_qos_policies(attrs=None, count=2): - """Create multiple fake QoS policies. + def create_qos_rules(attrs=None, count=2): + """Create multiple fake Network QoS rules. :param Dictionary attrs: A dictionary with all attributes :param int count: - The number of QoS policies to fake + The number of Network QoS rule to fake :return: - A list of FakeResource objects faking the QoS policies + A list of FakeResource objects faking the Network QoS rules """ - qos_policies = [] + qos_rules = [] for i in range(0, count): - qos_policies.append( - FakeNetworkQosPolicy.create_one_qos_policy(attrs)) - - return qos_policies + qos_rules.append(FakeNetworkQosRule.create_one_qos_rule(attrs)) + return qos_rules @staticmethod - def get_qos_policies(qos_policies=None, count=2): - """Get an iterable MagicMock object with a list of faked QoS policies. + def get_qos_rules(qos_rules=None, count=2): + """Get a list of faked Network QoS rules. - If qos policies list is provided, then initialize the Mock object - with the list. Otherwise create one. + If Network QoS rules list is provided, then initialize the Mock + object with the list. Otherwise create one. :param List address scopes: - A list of FakeResource objects faking qos policies + A list of FakeResource objects faking Network QoS rules :param int count: - The number of QoS policies to fake + The number of QoS minimum bandwidth rules to fake :return: An iterable Mock object with side_effect set to a list of faked - QoS policies + qos minimum bandwidth rules """ - if qos_policies is None: - qos_policies = FakeNetworkQosPolicy.create_qos_policies(count) - return mock.Mock(side_effect=qos_policies) + if qos_rules is None: + qos_rules = (FakeNetworkQosRule.create_qos_rules(count)) + return mock.Mock(side_effect=qos_rules) class FakeNetworkQosRuleType(object): diff --git a/openstackclient/tests/unit/network/v2/test_floating_ip.py b/openstackclient/tests/unit/network/v2/test_floating_ip.py index 63d22bf8..e395300d 100644 --- a/openstackclient/tests/unit/network/v2/test_floating_ip.py +++ b/openstackclient/tests/unit/network/v2/test_floating_ip.py @@ -208,13 +208,19 @@ class TestDeleteFloatingIPNetwork(TestFloatingIPNetwork): super(TestDeleteFloatingIPNetwork, self).setUp() self.network.delete_ip = mock.Mock(return_value=None) - self.network.find_ip = ( - network_fakes.FakeFloatingIP.get_floating_ips(self.floating_ips)) # Get the command object to test self.cmd = floating_ip.DeleteFloatingIP(self.app, self.namespace) - def test_floating_ip_delete(self): + @mock.patch( + "openstackclient.tests.unit.network.v2.test_floating_ip." + + "floating_ip._find_floating_ip" + ) + def test_floating_ip_delete(self, find_floating_ip_mock): + find_floating_ip_mock.side_effect = [ + (self.floating_ips[0], []), + (self.floating_ips[1], []), + ] arglist = [ self.floating_ips[0].id, ] @@ -225,12 +231,24 @@ class TestDeleteFloatingIPNetwork(TestFloatingIPNetwork): result = self.cmd.take_action(parsed_args) - self.network.find_ip.assert_called_once_with( - self.floating_ips[0].id, ignore_missing=False) + find_floating_ip_mock.assert_called_once_with( + mock.ANY, + [], + self.floating_ips[0].id, + ignore_missing=False, + ) self.network.delete_ip.assert_called_once_with(self.floating_ips[0]) self.assertIsNone(result) - def test_multi_floating_ips_delete(self): + @mock.patch( + "openstackclient.tests.unit.network.v2.test_floating_ip." + + "floating_ip._find_floating_ip" + ) + def test_floating_ip_delete_multi(self, find_floating_ip_mock): + find_floating_ip_mock.side_effect = [ + (self.floating_ips[0], []), + (self.floating_ips[1], []), + ] arglist = [] verifylist = [] @@ -243,13 +261,37 @@ class TestDeleteFloatingIPNetwork(TestFloatingIPNetwork): result = self.cmd.take_action(parsed_args) + calls = [ + call( + mock.ANY, + [], + self.floating_ips[0].id, + ignore_missing=False, + ), + call( + mock.ANY, + [], + self.floating_ips[1].id, + ignore_missing=False, + ), + ] + find_floating_ip_mock.assert_has_calls(calls) + calls = [] for f in self.floating_ips: calls.append(call(f)) self.network.delete_ip.assert_has_calls(calls) self.assertIsNone(result) - def test_multi_floating_ips_delete_with_exception(self): + @mock.patch( + "openstackclient.tests.unit.network.v2.test_floating_ip." + + "floating_ip._find_floating_ip" + ) + def test_floating_ip_delete_multi_exception(self, find_floating_ip_mock): + find_floating_ip_mock.side_effect = [ + (self.floating_ips[0], []), + exceptions.CommandError, + ] arglist = [ self.floating_ips[0].id, 'unexist_floating_ip', @@ -260,21 +302,24 @@ class TestDeleteFloatingIPNetwork(TestFloatingIPNetwork): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - find_mock_result = [self.floating_ips[0], exceptions.CommandError] - self.network.find_ip = ( - mock.Mock(side_effect=find_mock_result) - ) - try: self.cmd.take_action(parsed_args) self.fail('CommandError should be raised.') except exceptions.CommandError as e: self.assertEqual('1 of 2 floating_ips failed to delete.', str(e)) - self.network.find_ip.assert_any_call( - self.floating_ips[0].id, ignore_missing=False) - self.network.find_ip.assert_any_call( - 'unexist_floating_ip', ignore_missing=False) + find_floating_ip_mock.assert_any_call( + mock.ANY, + [], + self.floating_ips[0].id, + ignore_missing=False, + ) + find_floating_ip_mock.assert_any_call( + mock.ANY, + [], + 'unexist_floating_ip', + ignore_missing=False, + ) self.network.delete_ip.assert_called_once_with( self.floating_ips[0] ) @@ -534,7 +579,12 @@ class TestShowFloatingIPNetwork(TestFloatingIPNetwork): # Get the command object to test self.cmd = floating_ip.ShowFloatingIP(self.app, self.namespace) - def test_floating_ip_show(self): + @mock.patch( + "openstackclient.tests.unit.network.v2.test_floating_ip." + + "floating_ip._find_floating_ip" + ) + def test_floating_ip_show(self, find_floating_ip_mock): + find_floating_ip_mock.return_value = (self.floating_ip, []) arglist = [ self.floating_ip.id, ] @@ -545,9 +595,11 @@ class TestShowFloatingIPNetwork(TestFloatingIPNetwork): columns, data = self.cmd.take_action(parsed_args) - self.network.find_ip.assert_called_once_with( + find_floating_ip_mock.assert_called_once_with( + mock.ANY, + [], self.floating_ip.id, - ignore_missing=False + ignore_missing=False, ) self.assertEqual(self.columns, columns) self.assertEqual(self.data, data) diff --git a/openstackclient/tests/unit/network/v2/test_network_agent.py b/openstackclient/tests/unit/network/v2/test_network_agent.py index 9fd395b4..2fc0c043 100644 --- a/openstackclient/tests/unit/network/v2/test_network_agent.py +++ b/openstackclient/tests/unit/network/v2/test_network_agent.py @@ -195,6 +195,8 @@ class TestListNetworkAgent(TestNetworkAgent): self.assertEqual(self.data, list(data)) +# TODO(huanxuan): Also update by the new attribute name +# "is_admin_state_up" after sdk 0.9.12 class TestSetNetworkAgent(TestNetworkAgent): _network_agent = ( @@ -324,6 +326,6 @@ class TestShowNetworkAgent(TestNetworkAgent): columns, data = self.cmd.take_action(parsed_args) self.network.get_agent.assert_called_once_with( - self._network_agent.id, ignore_missing=False) + self._network_agent.id) self.assertEqual(self.columns, columns) self.assertEqual(list(self.data), list(data)) diff --git a/openstackclient/tests/unit/network/v2/test_network_qos_rule.py b/openstackclient/tests/unit/network/v2/test_network_qos_rule.py new file mode 100644 index 00000000..41ccae32 --- /dev/null +++ b/openstackclient/tests/unit/network/v2/test_network_qos_rule.py @@ -0,0 +1,1049 @@ +# Copyright (c) 2016, Intel Corporation. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import mock + +from osc_lib import exceptions + +from openstackclient.network.v2 import network_qos_rule +from openstackclient.tests.unit.network.v2 import fakes as network_fakes +from openstackclient.tests.unit import utils as tests_utils + + +RULE_TYPE_BANDWIDTH_LIMIT = 'bandwidth-limit' +RULE_TYPE_DSCP_MARKING = 'dscp-marking' +RULE_TYPE_MINIMUM_BANDWIDTH = 'minimum-bandwidth' +DSCP_VALID_MARKS = [0, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, + 34, 36, 38, 40, 46, 48, 56] + + +class TestNetworkQosRule(network_fakes.TestNetworkV2): + + def setUp(self): + super(TestNetworkQosRule, self).setUp() + # Get a shortcut to the network client + self.network = self.app.client_manager.network + self.qos_policy = (network_fakes.FakeNetworkQosPolicy. + create_one_qos_policy()) + self.network.find_qos_policy = mock.Mock(return_value=self.qos_policy) + + +class TestCreateNetworkQosRuleMinimumBandwidth(TestNetworkQosRule): + + def test_check_type_parameters(self): + pass + + def setUp(self): + super(TestCreateNetworkQosRuleMinimumBandwidth, self).setUp() + attrs = {'qos_policy_id': self.qos_policy.id, + 'type': RULE_TYPE_MINIMUM_BANDWIDTH} + self.new_rule = network_fakes.FakeNetworkQosRule.create_one_qos_rule( + attrs) + self.columns = ( + 'direction', + 'id', + 'min_kbps', + 'project_id', + 'qos_policy_id', + 'type' + ) + + self.data = ( + self.new_rule.direction, + self.new_rule.id, + self.new_rule.min_kbps, + self.new_rule.project_id, + self.new_rule.qos_policy_id, + self.new_rule.type, + ) + self.network.create_qos_minimum_bandwidth_rule = mock.Mock( + return_value=self.new_rule) + + # Get the command object to test + self.cmd = network_qos_rule.CreateNetworkQosRule(self.app, + self.namespace) + + def test_create_no_options(self): + arglist = [] + verifylist = [] + + # Missing required args should bail here + self.assertRaises(tests_utils.ParserException, self.check_parser, + self.cmd, arglist, verifylist) + + def test_create_default_options(self): + arglist = [ + '--type', RULE_TYPE_MINIMUM_BANDWIDTH, + '--min-kbps', str(self.new_rule.min_kbps), + '--egress', + self.new_rule.qos_policy_id, + ] + + verifylist = [ + ('type', RULE_TYPE_MINIMUM_BANDWIDTH), + ('min_kbps', self.new_rule.min_kbps), + ('egress', True), + ('qos_policy', self.new_rule.qos_policy_id), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = (self.cmd.take_action(parsed_args)) + + self.network.create_qos_minimum_bandwidth_rule.assert_called_once_with( + self.qos_policy.id, + **{'min_kbps': self.new_rule.min_kbps, + 'direction': self.new_rule.direction} + ) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + def test_create_wrong_options(self): + arglist = [ + '--type', RULE_TYPE_MINIMUM_BANDWIDTH, + '--max-kbps', '10000', + self.new_rule.qos_policy_id, + ] + + verifylist = [ + ('type', RULE_TYPE_MINIMUM_BANDWIDTH), + ('max_kbps', 10000), + ('qos_policy', self.new_rule.qos_policy_id), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + try: + self.cmd.take_action(parsed_args) + except exceptions.CommandError as e: + msg = ('"Create" rule command for type "minimum-bandwidth" ' + 'requires arguments min_kbps, direction') + self.assertEqual(msg, str(e)) + + +class TestCreateNetworkQosRuleDSCPMarking(TestNetworkQosRule): + + def test_check_type_parameters(self): + pass + + def setUp(self): + super(TestCreateNetworkQosRuleDSCPMarking, self).setUp() + attrs = {'qos_policy_id': self.qos_policy.id, + 'type': RULE_TYPE_DSCP_MARKING} + self.new_rule = network_fakes.FakeNetworkQosRule.create_one_qos_rule( + attrs) + self.columns = ( + 'dscp_mark', + 'id', + 'project_id', + 'qos_policy_id', + 'type' + ) + + self.data = ( + self.new_rule.dscp_mark, + self.new_rule.id, + self.new_rule.project_id, + self.new_rule.qos_policy_id, + self.new_rule.type, + ) + self.network.create_qos_dscp_marking_rule = mock.Mock( + return_value=self.new_rule) + + # Get the command object to test + self.cmd = network_qos_rule.CreateNetworkQosRule(self.app, + self.namespace) + + def test_create_no_options(self): + arglist = [] + verifylist = [] + + # Missing required args should bail here + self.assertRaises(tests_utils.ParserException, self.check_parser, + self.cmd, arglist, verifylist) + + def test_create_default_options(self): + arglist = [ + '--type', RULE_TYPE_DSCP_MARKING, + '--dscp-mark', str(self.new_rule.dscp_mark), + self.new_rule.qos_policy_id, + ] + + verifylist = [ + ('type', RULE_TYPE_DSCP_MARKING), + ('dscp_mark', self.new_rule.dscp_mark), + ('qos_policy', self.new_rule.qos_policy_id), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.network.create_qos_dscp_marking_rule.assert_called_once_with( + self.qos_policy.id, + **{'dscp_mark': self.new_rule.dscp_mark} + ) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + def test_create_wrong_options(self): + arglist = [ + '--type', RULE_TYPE_DSCP_MARKING, + '--max-kbps', '10000', + self.new_rule.qos_policy_id, + ] + + verifylist = [ + ('type', RULE_TYPE_DSCP_MARKING), + ('max_kbps', 10000), + ('qos_policy', self.new_rule.qos_policy_id), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + try: + self.cmd.take_action(parsed_args) + except exceptions.CommandError as e: + msg = ('"Create" rule command for type "dscp-marking" ' + 'requires arguments dscp_mark') + self.assertEqual(msg, str(e)) + + +class TestCreateNetworkQosRuleBandwidtLimit(TestNetworkQosRule): + + def test_check_type_parameters(self): + pass + + def setUp(self): + super(TestCreateNetworkQosRuleBandwidtLimit, self).setUp() + attrs = {'qos_policy_id': self.qos_policy.id, + 'type': RULE_TYPE_BANDWIDTH_LIMIT} + self.new_rule = network_fakes.FakeNetworkQosRule.create_one_qos_rule( + attrs) + self.columns = ( + 'id', + 'max_burst_kbits', + 'max_kbps', + 'project_id', + 'qos_policy_id', + 'type' + ) + + self.data = ( + self.new_rule.id, + self.new_rule.max_burst_kbits, + self.new_rule.max_kbps, + self.new_rule.project_id, + self.new_rule.qos_policy_id, + self.new_rule.type, + ) + self.network.create_qos_bandwidth_limit_rule = mock.Mock( + return_value=self.new_rule) + + # Get the command object to test + self.cmd = network_qos_rule.CreateNetworkQosRule(self.app, + self.namespace) + + def test_create_no_options(self): + arglist = [] + verifylist = [] + + # Missing required args should bail here + self.assertRaises(tests_utils.ParserException, self.check_parser, + self.cmd, arglist, verifylist) + + def test_create_default_options(self): + arglist = [ + '--type', RULE_TYPE_BANDWIDTH_LIMIT, + '--max-kbps', str(self.new_rule.max_kbps), + '--max-burst-kbits', str(self.new_rule.max_burst_kbits), + self.new_rule.qos_policy_id, + ] + + verifylist = [ + ('type', RULE_TYPE_BANDWIDTH_LIMIT), + ('max_kbps', self.new_rule.max_kbps), + ('max_burst_kbits', self.new_rule.max_burst_kbits), + ('qos_policy', self.new_rule.qos_policy_id), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = (self.cmd.take_action(parsed_args)) + + self.network.create_qos_bandwidth_limit_rule.assert_called_once_with( + self.qos_policy.id, + **{'max_kbps': self.new_rule.max_kbps, + 'max_burst_kbps': self.new_rule.max_burst_kbits} + ) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + def test_create_wrong_options(self): + arglist = [ + '--type', RULE_TYPE_BANDWIDTH_LIMIT, + '--min-kbps', '10000', + self.new_rule.qos_policy_id, + ] + + verifylist = [ + ('type', RULE_TYPE_BANDWIDTH_LIMIT), + ('min_kbps', 10000), + ('qos_policy', self.new_rule.qos_policy_id), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + try: + self.cmd.take_action(parsed_args) + except exceptions.CommandError as e: + msg = ('"Create" rule command for type "bandwidth-limit" ' + 'requires arguments max_kbps, max_burst_kbps') + self.assertEqual(msg, str(e)) + + +class TestDeleteNetworkQosRuleMinimumBandwidth(TestNetworkQosRule): + + def setUp(self): + super(TestDeleteNetworkQosRuleMinimumBandwidth, self).setUp() + attrs = {'qos_policy_id': self.qos_policy.id, + 'type': RULE_TYPE_MINIMUM_BANDWIDTH} + self.new_rule = network_fakes.FakeNetworkQosRule.create_one_qos_rule( + attrs) + self.qos_policy.rules = [self.new_rule] + self.network.delete_qos_minimum_bandwidth_rule = mock.Mock( + return_value=None) + self.network.find_qos_minimum_bandwidth_rule = ( + network_fakes.FakeNetworkQosRule.get_qos_rules( + qos_rules=self.new_rule) + ) + + # Get the command object to test + self.cmd = network_qos_rule.DeleteNetworkQosRule(self.app, + self.namespace) + + def test_qos_policy_delete(self): + arglist = [ + self.new_rule.qos_policy_id, + self.new_rule.id, + ] + verifylist = [ + ('qos_policy', self.new_rule.qos_policy_id), + ('id', self.new_rule.id), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + self.network.find_qos_policy.assert_called_once_with( + self.qos_policy.id, ignore_missing=False) + self.network.delete_qos_minimum_bandwidth_rule.assert_called_once_with( + self.new_rule.id, self.qos_policy.id) + self.assertIsNone(result) + + def test_qos_policy_delete_error(self): + arglist = [ + self.new_rule.qos_policy_id, + self.new_rule.id, + ] + verifylist = [ + ('qos_policy', self.new_rule.qos_policy_id), + ('id', self.new_rule.id), + ] + + self.network.delete_qos_minimum_bandwidth_rule.side_effect = \ + Exception('Error message') + try: + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.cmd.take_action(parsed_args) + except exceptions.CommandError as e: + msg = ('Failed to delete Network QoS rule ID "%(rule)s": %(e)s' % + {'rule': self.new_rule.id, 'e': 'Error message'}) + self.assertEqual(msg, str(e)) + + +class TestDeleteNetworkQosRuleDSCPMarking(TestNetworkQosRule): + + def setUp(self): + super(TestDeleteNetworkQosRuleDSCPMarking, self).setUp() + attrs = {'qos_policy_id': self.qos_policy.id, + 'type': RULE_TYPE_DSCP_MARKING} + self.new_rule = network_fakes.FakeNetworkQosRule.create_one_qos_rule( + attrs) + self.qos_policy.rules = [self.new_rule] + self.network.delete_qos_dscp_marking_rule = mock.Mock( + return_value=None) + self.network.find_qos_dscp_marking_rule = ( + network_fakes.FakeNetworkQosRule.get_qos_rules( + qos_rules=self.new_rule) + ) + + # Get the command object to test + self.cmd = network_qos_rule.DeleteNetworkQosRule(self.app, + self.namespace) + + def test_qos_policy_delete(self): + arglist = [ + self.new_rule.qos_policy_id, + self.new_rule.id, + ] + verifylist = [ + ('qos_policy', self.new_rule.qos_policy_id), + ('id', self.new_rule.id), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + self.network.find_qos_policy.assert_called_once_with( + self.qos_policy.id, ignore_missing=False) + self.network.delete_qos_dscp_marking_rule.assert_called_once_with( + self.new_rule.id, self.qos_policy.id) + self.assertIsNone(result) + + def test_qos_policy_delete_error(self): + arglist = [ + self.new_rule.qos_policy_id, + self.new_rule.id, + ] + verifylist = [ + ('qos_policy', self.new_rule.qos_policy_id), + ('id', self.new_rule.id), + ] + + self.network.delete_qos_dscp_marking_rule.side_effect = \ + Exception('Error message') + try: + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.cmd.take_action(parsed_args) + except exceptions.CommandError as e: + msg = ('Failed to delete Network QoS rule ID "%(rule)s": %(e)s' % + {'rule': self.new_rule.id, 'e': 'Error message'}) + self.assertEqual(msg, str(e)) + + +class TestDeleteNetworkQosRuleBandwidthLimit(TestNetworkQosRule): + + def setUp(self): + super(TestDeleteNetworkQosRuleBandwidthLimit, self).setUp() + attrs = {'qos_policy_id': self.qos_policy.id, + 'type': RULE_TYPE_BANDWIDTH_LIMIT} + self.new_rule = network_fakes.FakeNetworkQosRule.create_one_qos_rule( + attrs) + self.qos_policy.rules = [self.new_rule] + self.network.delete_qos_bandwidth_limit_rule = mock.Mock( + return_value=None) + self.network.find_qos_bandwidth_limit_rule = ( + network_fakes.FakeNetworkQosRule.get_qos_rules( + qos_rules=self.new_rule) + ) + + # Get the command object to test + self.cmd = network_qos_rule.DeleteNetworkQosRule(self.app, + self.namespace) + + def test_qos_policy_delete(self): + arglist = [ + self.new_rule.qos_policy_id, + self.new_rule.id, + ] + verifylist = [ + ('qos_policy', self.new_rule.qos_policy_id), + ('id', self.new_rule.id), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + self.network.find_qos_policy.assert_called_once_with( + self.qos_policy.id, ignore_missing=False) + self.network.delete_qos_bandwidth_limit_rule.assert_called_once_with( + self.new_rule.id, self.qos_policy.id) + self.assertIsNone(result) + + def test_qos_policy_delete_error(self): + arglist = [ + self.new_rule.qos_policy_id, + self.new_rule.id, + ] + verifylist = [ + ('qos_policy', self.new_rule.qos_policy_id), + ('id', self.new_rule.id), + ] + + self.network.delete_qos_bandwidth_limit_rule.side_effect = \ + Exception('Error message') + try: + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.cmd.take_action(parsed_args) + except exceptions.CommandError as e: + msg = ('Failed to delete Network QoS rule ID "%(rule)s": %(e)s' % + {'rule': self.new_rule.id, 'e': 'Error message'}) + self.assertEqual(msg, str(e)) + + +class TestSetNetworkQosRuleMinimumBandwidth(TestNetworkQosRule): + + def setUp(self): + super(TestSetNetworkQosRuleMinimumBandwidth, self).setUp() + attrs = {'qos_policy_id': self.qos_policy.id, + 'type': RULE_TYPE_MINIMUM_BANDWIDTH} + self.new_rule = network_fakes.FakeNetworkQosRule.create_one_qos_rule( + attrs=attrs) + self.qos_policy.rules = [self.new_rule] + self.network.update_qos_minimum_bandwidth_rule = mock.Mock( + return_value=None) + self.network.find_qos_minimum_bandwidth_rule = mock.Mock( + return_value=self.new_rule) + self.network.find_qos_policy = mock.Mock( + return_value=self.qos_policy) + + # Get the command object to test + self.cmd = (network_qos_rule.SetNetworkQosRule(self.app, + self.namespace)) + + def test_set_nothing(self): + arglist = [ + self.new_rule.qos_policy_id, + self.new_rule.id, + ] + verifylist = [ + ('qos_policy', self.new_rule.qos_policy_id), + ('id', self.new_rule.id), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + self.network.update_qos_minimum_bandwidth_rule.assert_called_with( + self.new_rule, self.qos_policy.id) + self.assertIsNone(result) + + def test_set_min_kbps(self): + self._set_min_kbps() + + def test_set_min_kbps_to_zero(self): + self._set_min_kbps(min_kbps=0) + + def _set_min_kbps(self, min_kbps=None): + if min_kbps: + previous_min_kbps = self.new_rule.min_kbps + self.new_rule.min_kbps = min_kbps + + arglist = [ + '--min-kbps', str(self.new_rule.min_kbps), + self.new_rule.qos_policy_id, + self.new_rule.id, + ] + verifylist = [ + ('min_kbps', self.new_rule.min_kbps), + ('qos_policy', self.new_rule.qos_policy_id), + ('id', self.new_rule.id), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + attrs = { + 'min_kbps': self.new_rule.min_kbps, + } + self.network.update_qos_minimum_bandwidth_rule.assert_called_with( + self.new_rule, self.qos_policy.id, **attrs) + self.assertIsNone(result) + + if min_kbps: + self.new_rule.min_kbps = previous_min_kbps + + def test_set_wrong_options(self): + arglist = [ + '--max-kbps', str(10000), + self.new_rule.qos_policy_id, + self.new_rule.id, + ] + verifylist = [ + ('max_kbps', 10000), + ('qos_policy', self.new_rule.qos_policy_id), + ('id', self.new_rule.id), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + try: + self.cmd.take_action(parsed_args) + except exceptions.CommandError as e: + msg = ('Failed to set Network QoS rule ID "%(rule)s": Rule type ' + '"minimum-bandwidth" only requires arguments min_kbps, ' + 'direction' % {'rule': self.new_rule.id}) + self.assertEqual(msg, str(e)) + + +class TestSetNetworkQosRuleDSCPMarking(TestNetworkQosRule): + + def setUp(self): + super(TestSetNetworkQosRuleDSCPMarking, self).setUp() + attrs = {'qos_policy_id': self.qos_policy.id, + 'type': RULE_TYPE_DSCP_MARKING} + self.new_rule = network_fakes.FakeNetworkQosRule.create_one_qos_rule( + attrs=attrs) + self.qos_policy.rules = [self.new_rule] + self.network.update_qos_dscp_marking_rule = mock.Mock( + return_value=None) + self.network.find_qos_dscp_marking_rule = mock.Mock( + return_value=self.new_rule) + self.network.find_qos_policy = mock.Mock( + return_value=self.qos_policy) + + # Get the command object to test + self.cmd = (network_qos_rule.SetNetworkQosRule(self.app, + self.namespace)) + + def test_set_nothing(self): + arglist = [ + self.new_rule.qos_policy_id, + self.new_rule.id, + ] + verifylist = [ + ('qos_policy', self.new_rule.qos_policy_id), + ('id', self.new_rule.id), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + self.network.update_qos_dscp_marking_rule.assert_called_with( + self.new_rule, self.qos_policy.id) + self.assertIsNone(result) + + def test_set_dscp_mark(self): + self._set_dscp_mark() + + def test_set_dscp_mark_to_zero(self): + self._set_dscp_mark(dscp_mark=0) + + def _set_dscp_mark(self, dscp_mark=None): + if dscp_mark: + previous_dscp_mark = self.new_rule.dscp_mark + self.new_rule.dscp_mark = dscp_mark + + arglist = [ + '--dscp-mark', str(self.new_rule.dscp_mark), + self.new_rule.qos_policy_id, + self.new_rule.id, + ] + verifylist = [ + ('dscp_mark', self.new_rule.dscp_mark), + ('qos_policy', self.new_rule.qos_policy_id), + ('id', self.new_rule.id), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + attrs = { + 'dscp_mark': self.new_rule.dscp_mark, + } + self.network.update_qos_dscp_marking_rule.assert_called_with( + self.new_rule, self.qos_policy.id, **attrs) + self.assertIsNone(result) + + if dscp_mark: + self.new_rule.dscp_mark = previous_dscp_mark + + def test_set_wrong_options(self): + arglist = [ + '--max-kbps', str(10000), + self.new_rule.qos_policy_id, + self.new_rule.id, + ] + verifylist = [ + ('max_kbps', 10000), + ('qos_policy', self.new_rule.qos_policy_id), + ('id', self.new_rule.id), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + try: + self.cmd.take_action(parsed_args) + except exceptions.CommandError as e: + msg = ('Failed to set Network QoS rule ID "%(rule)s": Rule type ' + '"dscp-marking" only requires arguments dscp_mark' % + {'rule': self.new_rule.id}) + self.assertEqual(msg, str(e)) + + +class TestSetNetworkQosRuleBandwidthLimit(TestNetworkQosRule): + + def setUp(self): + super(TestSetNetworkQosRuleBandwidthLimit, self).setUp() + attrs = {'qos_policy_id': self.qos_policy.id, + 'type': RULE_TYPE_BANDWIDTH_LIMIT} + self.new_rule = network_fakes.FakeNetworkQosRule.create_one_qos_rule( + attrs=attrs) + self.qos_policy.rules = [self.new_rule] + self.network.update_qos_bandwidth_limit_rule = mock.Mock( + return_value=None) + self.network.find_qos_bandwidth_limit_rule = mock.Mock( + return_value=self.new_rule) + self.network.find_qos_policy = mock.Mock( + return_value=self.qos_policy) + + # Get the command object to test + self.cmd = (network_qos_rule.SetNetworkQosRule(self.app, + self.namespace)) + + def test_set_nothing(self): + arglist = [ + self.new_rule.qos_policy_id, + self.new_rule.id, + ] + verifylist = [ + ('qos_policy', self.new_rule.qos_policy_id), + ('id', self.new_rule.id), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + self.network.update_qos_bandwidth_limit_rule.assert_called_with( + self.new_rule, self.qos_policy.id) + self.assertIsNone(result) + + def test_set_max_kbps(self): + self._set_max_kbps() + + def test_set_max_kbps_to_zero(self): + self._set_max_kbps(max_kbps=0) + + def _set_max_kbps(self, max_kbps=None): + if max_kbps: + previous_max_kbps = self.new_rule.max_kbps + self.new_rule.max_kbps = max_kbps + + arglist = [ + '--max-kbps', str(self.new_rule.max_kbps), + self.new_rule.qos_policy_id, + self.new_rule.id, + ] + verifylist = [ + ('max_kbps', self.new_rule.max_kbps), + ('qos_policy', self.new_rule.qos_policy_id), + ('id', self.new_rule.id), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + attrs = { + 'max_kbps': self.new_rule.max_kbps, + } + self.network.update_qos_bandwidth_limit_rule.assert_called_with( + self.new_rule, self.qos_policy.id, **attrs) + self.assertIsNone(result) + + if max_kbps: + self.new_rule.max_kbps = previous_max_kbps + + def test_set_max_burst_kbits(self): + self._set_max_burst_kbits() + + def test_set_max_burst_kbits_to_zero(self): + self._set_max_burst_kbits(max_burst_kbits=0) + + def _set_max_burst_kbits(self, max_burst_kbits=None): + if max_burst_kbits: + previous_max_burst_kbits = self.new_rule.max_burst_kbits + self.new_rule.max_burst_kbits = max_burst_kbits + + arglist = [ + '--max-burst-kbits', str(self.new_rule.max_burst_kbits), + self.new_rule.qos_policy_id, + self.new_rule.id, + ] + verifylist = [ + ('max_burst_kbits', self.new_rule.max_burst_kbits), + ('qos_policy', self.new_rule.qos_policy_id), + ('id', self.new_rule.id), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + attrs = { + 'max_burst_kbps': self.new_rule.max_burst_kbits, + } + self.network.update_qos_bandwidth_limit_rule.assert_called_with( + self.new_rule, self.qos_policy.id, **attrs) + self.assertIsNone(result) + + if max_burst_kbits: + self.new_rule.max_burst_kbits = previous_max_burst_kbits + + def test_set_wrong_options(self): + arglist = [ + '--min-kbps', str(10000), + self.new_rule.qos_policy_id, + self.new_rule.id, + ] + verifylist = [ + ('min_kbps', 10000), + ('qos_policy', self.new_rule.qos_policy_id), + ('id', self.new_rule.id), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + try: + self.cmd.take_action(parsed_args) + except exceptions.CommandError as e: + msg = ('Failed to set Network QoS rule ID "%(rule)s": Rule type ' + '"bandwidth-limit" only requires arguments max_kbps, ' + 'max_burst_kbps' % {'rule': self.new_rule.id}) + self.assertEqual(msg, str(e)) + + +class TestListNetworkQosRule(TestNetworkQosRule): + + def setUp(self): + super(TestListNetworkQosRule, self).setUp() + attrs = {'qos_policy_id': self.qos_policy.id, + 'type': RULE_TYPE_MINIMUM_BANDWIDTH} + self.new_rule_min_bw = (network_fakes.FakeNetworkQosRule. + create_one_qos_rule(attrs=attrs)) + attrs['type'] = RULE_TYPE_DSCP_MARKING + self.new_rule_dscp_mark = (network_fakes.FakeNetworkQosRule. + create_one_qos_rule(attrs=attrs)) + attrs['type'] = RULE_TYPE_BANDWIDTH_LIMIT + self.new_rule_max_bw = (network_fakes.FakeNetworkQosRule. + create_one_qos_rule(attrs=attrs)) + self.qos_policy.rules = [self.new_rule_min_bw, + self.new_rule_dscp_mark, + self.new_rule_max_bw] + self.network.find_qos_minimum_bandwidth_rule = mock.Mock( + return_value=self.new_rule_min_bw) + self.network.find_qos_dscp_marking_rule = mock.Mock( + return_value=self.new_rule_dscp_mark) + self.network.find_qos_bandwidth_limit_rule = mock.Mock( + return_value=self.new_rule_max_bw) + self.columns = ( + 'ID', + 'QoS Policy ID', + 'Type', + 'Max Kbps', + 'Max Burst Kbits', + 'Min Kbps', + 'DSCP mark', + 'Direction', + ) + self.data = [] + for index in range(len(self.qos_policy.rules)): + self.data.append(( + self.qos_policy.rules[index].id, + self.qos_policy.rules[index].qos_policy_id, + self.qos_policy.rules[index].type, + getattr(self.qos_policy.rules[index], 'max_kbps', ''), + getattr(self.qos_policy.rules[index], 'max_burst_kbps', ''), + getattr(self.qos_policy.rules[index], 'min_kbps', ''), + getattr(self.qos_policy.rules[index], 'dscp_mark', ''), + getattr(self.qos_policy.rules[index], 'direction', ''), + )) + # Get the command object to test + self.cmd = network_qos_rule.ListNetworkQosRule(self.app, + self.namespace) + + def test_qos_rule_list(self): + arglist = [ + self.qos_policy.id + ] + verifylist = [ + ('qos_policy', self.qos_policy.id), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.network.find_qos_policy.assert_called_once_with( + self.qos_policy.id, ignore_missing=False) + self.assertEqual(self.columns, columns) + list_data = list(data) + self.assertEqual(len(self.data), len(list_data)) + for index in range(len(list_data)): + self.assertEqual(self.data[index], list_data[index]) + + +class TestShowNetworkQosRuleMinimumBandwidth(TestNetworkQosRule): + + def setUp(self): + super(TestShowNetworkQosRuleMinimumBandwidth, self).setUp() + attrs = {'qos_policy_id': self.qos_policy.id, + 'type': RULE_TYPE_MINIMUM_BANDWIDTH} + self.new_rule = network_fakes.FakeNetworkQosRule.create_one_qos_rule( + attrs) + self.qos_policy.rules = [self.new_rule] + self.columns = ( + 'direction', + 'id', + 'min_kbps', + 'project_id', + 'qos_policy_id', + 'type' + ) + self.data = ( + self.new_rule.direction, + self.new_rule.id, + self.new_rule.min_kbps, + self.new_rule.project_id, + self.new_rule.qos_policy_id, + self.new_rule.type, + ) + + self.network.get_qos_minimum_bandwidth_rule = mock.Mock( + return_value=self.new_rule) + + # Get the command object to test + self.cmd = network_qos_rule.ShowNetworkQosRule(self.app, + self.namespace) + + def test_show_no_options(self): + arglist = [] + verifylist = [] + + # Missing required args should bail here + self.assertRaises(tests_utils.ParserException, self.check_parser, + self.cmd, arglist, verifylist) + + def test_show_all_options(self): + arglist = [ + self.new_rule.qos_policy_id, + self.new_rule.id, + ] + verifylist = [ + ('qos_policy', self.new_rule.qos_policy_id), + ('id', self.new_rule.id), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.network.get_qos_minimum_bandwidth_rule.assert_called_once_with( + self.new_rule.id, self.qos_policy.id) + self.assertEqual(self.columns, columns) + self.assertEqual(list(self.data), list(data)) + + +class TestShowNetworkQosDSCPMarking(TestNetworkQosRule): + + def setUp(self): + super(TestShowNetworkQosDSCPMarking, self).setUp() + attrs = {'qos_policy_id': self.qos_policy.id, + 'type': RULE_TYPE_DSCP_MARKING} + self.new_rule = network_fakes.FakeNetworkQosRule.create_one_qos_rule( + attrs) + self.qos_policy.rules = [self.new_rule] + self.columns = ( + 'dscp_mark', + 'id', + 'project_id', + 'qos_policy_id', + 'type' + ) + self.data = ( + self.new_rule.dscp_mark, + self.new_rule.id, + self.new_rule.project_id, + self.new_rule.qos_policy_id, + self.new_rule.type, + ) + + self.network.get_qos_dscp_marking_rule = mock.Mock( + return_value=self.new_rule) + + # Get the command object to test + self.cmd = network_qos_rule.ShowNetworkQosRule(self.app, + self.namespace) + + def test_show_no_options(self): + arglist = [] + verifylist = [] + + # Missing required args should bail here + self.assertRaises(tests_utils.ParserException, self.check_parser, + self.cmd, arglist, verifylist) + + def test_show_all_options(self): + arglist = [ + self.new_rule.qos_policy_id, + self.new_rule.id, + ] + verifylist = [ + ('qos_policy', self.new_rule.qos_policy_id), + ('id', self.new_rule.id), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.network.get_qos_dscp_marking_rule.assert_called_once_with( + self.new_rule.id, self.qos_policy.id) + self.assertEqual(self.columns, columns) + self.assertEqual(list(self.data), list(data)) + + +class TestShowNetworkQosBandwidthLimit(TestNetworkQosRule): + + def setUp(self): + super(TestShowNetworkQosBandwidthLimit, self).setUp() + attrs = {'qos_policy_id': self.qos_policy.id, + 'type': RULE_TYPE_BANDWIDTH_LIMIT} + self.new_rule = network_fakes.FakeNetworkQosRule.create_one_qos_rule( + attrs) + self.qos_policy.rules = [self.new_rule] + self.columns = ( + 'id', + 'max_burst_kbits', + 'max_kbps', + 'project_id', + 'qos_policy_id', + 'type' + ) + self.data = ( + self.new_rule.id, + self.new_rule.max_burst_kbits, + self.new_rule.max_kbps, + self.new_rule.project_id, + self.new_rule.qos_policy_id, + self.new_rule.type, + ) + + self.network.get_qos_bandwidth_limit_rule = mock.Mock( + return_value=self.new_rule) + + # Get the command object to test + self.cmd = network_qos_rule.ShowNetworkQosRule(self.app, + self.namespace) + + def test_show_no_options(self): + arglist = [] + verifylist = [] + + # Missing required args should bail here + self.assertRaises(tests_utils.ParserException, self.check_parser, + self.cmd, arglist, verifylist) + + def test_show_all_options(self): + arglist = [ + self.new_rule.qos_policy_id, + self.new_rule.id, + ] + verifylist = [ + ('qos_policy', self.new_rule.qos_policy_id), + ('id', self.new_rule.id), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.network.get_qos_bandwidth_limit_rule.assert_called_once_with( + self.new_rule.id, self.qos_policy.id) + self.assertEqual(self.columns, columns) + self.assertEqual(list(self.data), list(data)) diff --git a/openstackclient/tests/unit/volume/v2/fakes.py b/openstackclient/tests/unit/volume/v2/fakes.py index d5cd72ec..a6676403 100644 --- a/openstackclient/tests/unit/volume/v2/fakes.py +++ b/openstackclient/tests/unit/volume/v2/fakes.py @@ -903,3 +903,23 @@ class FakeType(object): volume_types.append(volume_type) return volume_types + + @staticmethod + def get_types(types=None, count=2): + """Get an iterable MagicMock object with a list of faked types. + + If types list is provided, then initialize the Mock object with the + list. Otherwise create one. + + :param List types: + A list of FakeResource objects faking types + :param Integer count: + The number of types to be faked + :return + An iterable Mock object with side_effect set to a list of faked + types + """ + if types is None: + types = FakeType.create_types(count) + + return mock.Mock(side_effect=types) diff --git a/openstackclient/tests/unit/volume/v2/test_consistency_group.py b/openstackclient/tests/unit/volume/v2/test_consistency_group.py index bc99ca8d..6eeeae39 100644 --- a/openstackclient/tests/unit/volume/v2/test_consistency_group.py +++ b/openstackclient/tests/unit/volume/v2/test_consistency_group.py @@ -36,10 +36,115 @@ class TestConsistencyGroup(volume_fakes.TestVolume): self.app.client_manager.volume.cgsnapshots) self.cgsnapshots_mock.reset_mock() + self.volumes_mock = ( + self.app.client_manager.volume.volumes) + self.volumes_mock.reset_mock() + self.types_mock = self.app.client_manager.volume.volume_types self.types_mock.reset_mock() +class TestConsistencyGroupAddVolume(TestConsistencyGroup): + + _consistency_group = ( + volume_fakes.FakeConsistencyGroup.create_one_consistency_group()) + + def setUp(self): + super(TestConsistencyGroupAddVolume, self).setUp() + + self.consistencygroups_mock.get.return_value = ( + self._consistency_group) + # Get the command object to test + self.cmd = \ + consistency_group.AddVolumeToConsistencyGroup(self.app, None) + + def test_add_one_volume_to_consistency_group(self): + volume = volume_fakes.FakeVolume.create_one_volume() + self.volumes_mock.get.return_value = volume + arglist = [ + self._consistency_group.id, + volume.id, + ] + verifylist = [ + ('consistency_group', self._consistency_group.id), + ('volumes', [volume.id]), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'add_volumes': volume.id, + } + self.consistencygroups_mock.update.assert_called_once_with( + self._consistency_group.id, + **kwargs + ) + self.assertIsNone(result) + + def test_add_multiple_volumes_to_consistency_group(self): + volumes = volume_fakes.FakeVolume.create_volumes(count=2) + self.volumes_mock.get = volume_fakes.FakeVolume.get_volumes(volumes) + arglist = [ + self._consistency_group.id, + volumes[0].id, + volumes[1].id, + ] + verifylist = [ + ('consistency_group', self._consistency_group.id), + ('volumes', [volumes[0].id, volumes[1].id]), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'add_volumes': volumes[0].id + ',' + volumes[1].id, + } + self.consistencygroups_mock.update.assert_called_once_with( + self._consistency_group.id, + **kwargs + ) + self.assertIsNone(result) + + @mock.patch.object(consistency_group.LOG, 'error') + def test_add_multiple_volumes_to_consistency_group_with_exception( + self, mock_error): + volume = volume_fakes.FakeVolume.create_one_volume() + arglist = [ + self._consistency_group.id, + volume.id, + 'unexist_volume', + ] + verifylist = [ + ('consistency_group', self._consistency_group.id), + ('volumes', [volume.id, 'unexist_volume']), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + find_mock_result = [volume, + exceptions.CommandError, + self._consistency_group] + with mock.patch.object(utils, 'find_resource', + side_effect=find_mock_result) as find_mock: + result = self.cmd.take_action(parsed_args) + mock_error.assert_called_with("1 of 2 volumes failed to add.") + self.assertIsNone(result) + find_mock.assert_any_call(self.consistencygroups_mock, + self._consistency_group.id) + find_mock.assert_any_call(self.volumes_mock, + volume.id) + find_mock.assert_any_call(self.volumes_mock, + 'unexist_volume') + self.assertEqual(3, find_mock.call_count) + self.consistencygroups_mock.update.assert_called_once_with( + self._consistency_group.id, add_volumes=volume.id + ) + + class TestConsistencyGroupCreate(TestConsistencyGroup): volume_type = volume_fakes.FakeType.create_one_type() @@ -394,6 +499,107 @@ class TestConsistencyGroupList(TestConsistencyGroup): self.assertEqual(self.data_long, list(data)) +class TestConsistencyGroupRemoveVolume(TestConsistencyGroup): + + _consistency_group = ( + volume_fakes.FakeConsistencyGroup.create_one_consistency_group()) + + def setUp(self): + super(TestConsistencyGroupRemoveVolume, self).setUp() + + self.consistencygroups_mock.get.return_value = ( + self._consistency_group) + # Get the command object to test + self.cmd = \ + consistency_group.RemoveVolumeFromConsistencyGroup(self.app, None) + + def test_remove_one_volume_from_consistency_group(self): + volume = volume_fakes.FakeVolume.create_one_volume() + self.volumes_mock.get.return_value = volume + arglist = [ + self._consistency_group.id, + volume.id, + ] + verifylist = [ + ('consistency_group', self._consistency_group.id), + ('volumes', [volume.id]), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'remove_volumes': volume.id, + } + self.consistencygroups_mock.update.assert_called_once_with( + self._consistency_group.id, + **kwargs + ) + self.assertIsNone(result) + + def test_remove_multi_volumes_from_consistency_group(self): + volumes = volume_fakes.FakeVolume.create_volumes(count=2) + self.volumes_mock.get = volume_fakes.FakeVolume.get_volumes(volumes) + arglist = [ + self._consistency_group.id, + volumes[0].id, + volumes[1].id, + ] + verifylist = [ + ('consistency_group', self._consistency_group.id), + ('volumes', [volumes[0].id, volumes[1].id]), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'remove_volumes': volumes[0].id + ',' + volumes[1].id, + } + self.consistencygroups_mock.update.assert_called_once_with( + self._consistency_group.id, + **kwargs + ) + self.assertIsNone(result) + + @mock.patch.object(consistency_group.LOG, 'error') + def test_remove_multiple_volumes_from_consistency_group_with_exception( + self, mock_error): + volume = volume_fakes.FakeVolume.create_one_volume() + arglist = [ + self._consistency_group.id, + volume.id, + 'unexist_volume', + ] + verifylist = [ + ('consistency_group', self._consistency_group.id), + ('volumes', [volume.id, 'unexist_volume']), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + find_mock_result = [volume, + exceptions.CommandError, + self._consistency_group] + with mock.patch.object(utils, 'find_resource', + side_effect=find_mock_result) as find_mock: + result = self.cmd.take_action(parsed_args) + mock_error.assert_called_with("1 of 2 volumes failed to remove.") + self.assertIsNone(result) + find_mock.assert_any_call(self.consistencygroups_mock, + self._consistency_group.id) + find_mock.assert_any_call(self.volumes_mock, + volume.id) + find_mock.assert_any_call(self.volumes_mock, + 'unexist_volume') + self.assertEqual(3, find_mock.call_count) + self.consistencygroups_mock.update.assert_called_once_with( + self._consistency_group.id, remove_volumes=volume.id + ) + + class TestConsistencyGroupSet(TestConsistencyGroup): consistency_group = ( diff --git a/openstackclient/tests/unit/volume/v2/test_type.py b/openstackclient/tests/unit/volume/v2/test_type.py index 0d556e13..cec01bd8 100644 --- a/openstackclient/tests/unit/volume/v2/test_type.py +++ b/openstackclient/tests/unit/volume/v2/test_type.py @@ -13,6 +13,7 @@ # import mock +from mock import call from osc_lib import exceptions from osc_lib import utils @@ -133,12 +134,13 @@ class TestTypeCreate(TestType): class TestTypeDelete(TestType): - volume_type = volume_fakes.FakeType.create_one_type() + volume_types = volume_fakes.FakeType.create_types(count=2) def setUp(self): super(TestTypeDelete, self).setUp() - self.types_mock.get.return_value = self.volume_type + self.types_mock.get = volume_fakes.FakeType.get_types( + self.volume_types) self.types_mock.delete.return_value = None # Get the command object to mock @@ -146,18 +148,64 @@ class TestTypeDelete(TestType): def test_type_delete(self): arglist = [ - self.volume_type.id + self.volume_types[0].id ] verifylist = [ - ("volume_types", [self.volume_type.id]) + ("volume_types", [self.volume_types[0].id]) ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) - self.types_mock.delete.assert_called_with(self.volume_type) + self.types_mock.delete.assert_called_with(self.volume_types[0]) self.assertIsNone(result) + def test_delete_multiple_types(self): + arglist = [] + for t in self.volume_types: + arglist.append(t.id) + verifylist = [ + ('volume_types', arglist), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + calls = [] + for t in self.volume_types: + calls.append(call(t)) + self.types_mock.delete.assert_has_calls(calls) + self.assertIsNone(result) + + def test_delete_multiple_types_with_exception(self): + arglist = [ + self.volume_types[0].id, + 'unexist_type', + ] + verifylist = [ + ('volume_types', arglist), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + find_mock_result = [self.volume_types[0], exceptions.CommandError] + with mock.patch.object(utils, 'find_resource', + side_effect=find_mock_result) as find_mock: + try: + self.cmd.take_action(parsed_args) + self.fail('CommandError should be raised.') + except exceptions.CommandError as e: + self.assertEqual('1 of 2 volume types failed to delete.', + str(e)) + find_mock.assert_any_call( + self.types_mock, self.volume_types[0].id) + find_mock.assert_any_call(self.types_mock, 'unexist_type') + + self.assertEqual(2, find_mock.call_count) + self.types_mock.delete.assert_called_once_with( + self.volume_types[0] + ) + class TestTypeList(TestType): diff --git a/openstackclient/tests/unit/volume/v2/test_volume.py b/openstackclient/tests/unit/volume/v2/test_volume.py index 1529c2e2..4fef9dd9 100644 --- a/openstackclient/tests/unit/volume/v2/test_volume.py +++ b/openstackclient/tests/unit/volume/v2/test_volume.py @@ -823,6 +823,19 @@ class TestVolumeList(TestVolume): columns, data = self.cmd.take_action(parsed_args) + search_opts = { + 'all_tenants': False, + 'project_id': None, + 'user_id': None, + 'display_name': None, + 'status': None, + } + self.volumes_mock.list.assert_called_once_with( + search_opts=search_opts, + marker=None, + limit=None, + ) + self.assertEqual(self.columns, columns) server = self.mock_volume.attachments[0]['server_id'] @@ -853,6 +866,19 @@ class TestVolumeList(TestVolume): columns, data = self.cmd.take_action(parsed_args) + search_opts = { + 'all_tenants': True, + 'project_id': self.project.id, + 'user_id': None, + 'display_name': None, + 'status': None, + } + self.volumes_mock.list.assert_called_once_with( + search_opts=search_opts, + marker=None, + limit=None, + ) + self.assertEqual(self.columns, columns) server = self.mock_volume.attachments[0]['server_id'] @@ -885,6 +911,19 @@ class TestVolumeList(TestVolume): columns, data = self.cmd.take_action(parsed_args) + search_opts = { + 'all_tenants': True, + 'project_id': self.project.id, + 'user_id': None, + 'display_name': None, + 'status': None, + } + self.volumes_mock.list.assert_called_once_with( + search_opts=search_opts, + marker=None, + limit=None, + ) + self.assertEqual(self.columns, columns) server = self.mock_volume.attachments[0]['server_id'] @@ -915,6 +954,19 @@ class TestVolumeList(TestVolume): columns, data = self.cmd.take_action(parsed_args) + search_opts = { + 'all_tenants': False, + 'project_id': None, + 'user_id': self.user.id, + 'display_name': None, + 'status': None, + } + self.volumes_mock.list.assert_called_once_with( + search_opts=search_opts, + marker=None, + limit=None, + ) + self.assertEqual(self.columns, columns) server = self.mock_volume.attachments[0]['server_id'] device = self.mock_volume.attachments[0]['device'] @@ -946,6 +998,19 @@ class TestVolumeList(TestVolume): columns, data = self.cmd.take_action(parsed_args) + search_opts = { + 'all_tenants': False, + 'project_id': None, + 'user_id': self.user.id, + 'display_name': None, + 'status': None, + } + self.volumes_mock.list.assert_called_once_with( + search_opts=search_opts, + marker=None, + limit=None, + ) + self.assertEqual(self.columns, columns) server = self.mock_volume.attachments[0]['server_id'] @@ -976,6 +1041,19 @@ class TestVolumeList(TestVolume): columns, data = self.cmd.take_action(parsed_args) + search_opts = { + 'all_tenants': False, + 'project_id': None, + 'user_id': None, + 'display_name': self.mock_volume.name, + 'status': None, + } + self.volumes_mock.list.assert_called_once_with( + search_opts=search_opts, + marker=None, + limit=None, + ) + self.assertEqual(self.columns, columns) server = self.mock_volume.attachments[0]['server_id'] @@ -1006,6 +1084,19 @@ class TestVolumeList(TestVolume): columns, data = self.cmd.take_action(parsed_args) + search_opts = { + 'all_tenants': False, + 'project_id': None, + 'user_id': None, + 'display_name': None, + 'status': self.mock_volume.status, + } + self.volumes_mock.list.assert_called_once_with( + search_opts=search_opts, + marker=None, + limit=None, + ) + self.assertEqual(self.columns, columns) server = self.mock_volume.attachments[0]['server_id'] @@ -1036,6 +1127,19 @@ class TestVolumeList(TestVolume): columns, data = self.cmd.take_action(parsed_args) + search_opts = { + 'all_tenants': True, + 'project_id': None, + 'user_id': None, + 'display_name': None, + 'status': None, + } + self.volumes_mock.list.assert_called_once_with( + search_opts=search_opts, + marker=None, + limit=None, + ) + self.assertEqual(self.columns, columns) server = self.mock_volume.attachments[0]['server_id'] @@ -1067,6 +1171,19 @@ class TestVolumeList(TestVolume): columns, data = self.cmd.take_action(parsed_args) + search_opts = { + 'all_tenants': False, + 'project_id': None, + 'user_id': None, + 'display_name': None, + 'status': None, + } + self.volumes_mock.list.assert_called_once_with( + search_opts=search_opts, + marker=None, + limit=None, + ) + collist = [ 'ID', 'Display Name', diff --git a/openstackclient/volume/v2/consistency_group.py b/openstackclient/volume/v2/consistency_group.py index 2f4f3c95..0a932f84 100644 --- a/openstackclient/volume/v2/consistency_group.py +++ b/openstackclient/volume/v2/consistency_group.py @@ -27,6 +27,60 @@ from openstackclient.i18n import _ LOG = logging.getLogger(__name__) +def _find_volumes(parsed_args_volumes, volume_client): + result = 0 + uuid = '' + for volume in parsed_args_volumes: + try: + volume_id = utils.find_resource( + volume_client.volumes, volume).id + uuid += volume_id + ',' + except Exception as e: + result += 1 + LOG.error(_("Failed to find volume with " + "name or ID '%(volume)s':%(e)s") + % {'volume': volume, 'e': e}) + + return result, uuid + + +class AddVolumeToConsistencyGroup(command.Command): + _description = _("Add volume(s) to consistency group") + + def get_parser(self, prog_name): + parser = super(AddVolumeToConsistencyGroup, self).get_parser(prog_name) + parser.add_argument( + 'consistency_group', + metavar="<consistency-group>", + help=_('Consistency group to contain <volume> (name or ID)'), + ) + parser.add_argument( + 'volumes', + metavar='<volume>', + nargs='+', + help=_('Volume(s) to add to <consistency-group> (name or ID) ' + '(repeat option to add multiple volumes)'), + ) + return parser + + def take_action(self, parsed_args): + volume_client = self.app.client_manager.volume + result, add_uuid = _find_volumes(parsed_args.volumes, volume_client) + + if result > 0: + total = len(parsed_args.volumes) + LOG.error(_("%(result)s of %(total)s volumes failed " + "to add.") % {'result': result, 'total': total}) + + if add_uuid: + add_uuid = add_uuid.rstrip(',') + consistency_group_id = utils.find_resource( + volume_client.consistencygroups, + parsed_args.consistency_group).id + volume_client.consistencygroups.update( + consistency_group_id, add_volumes=add_uuid) + + class CreateConsistencyGroup(command.ShowOne): _description = _("Create new consistency group.") @@ -188,6 +242,44 @@ class ListConsistencyGroup(command.Lister): for s in consistency_groups)) +class RemoveVolumeFromConsistencyGroup(command.Command): + _description = _("Remove volume(s) from consistency group") + + def get_parser(self, prog_name): + parser = \ + super(RemoveVolumeFromConsistencyGroup, self).get_parser(prog_name) + parser.add_argument( + 'consistency_group', + metavar="<consistency-group>", + help=_('Consistency group containing <volume> (name or ID)'), + ) + parser.add_argument( + 'volumes', + metavar='<volume>', + nargs='+', + help=_('Volume(s) to remove from <consistency-group> (name or ID) ' + '(repeat option to remove multiple volumes)'), + ) + return parser + + def take_action(self, parsed_args): + volume_client = self.app.client_manager.volume + result, remove_uuid = _find_volumes(parsed_args.volumes, volume_client) + + if result > 0: + total = len(parsed_args.volumes) + LOG.error(_("%(result)s of %(total)s volumes failed " + "to remove.") % {'result': result, 'total': total}) + + if remove_uuid: + remove_uuid = remove_uuid.rstrip(',') + consistency_group_id = utils.find_resource( + volume_client.consistencygroups, + parsed_args.consistency_group).id + volume_client.consistencygroups.update( + consistency_group_id, remove_volumes=remove_uuid) + + class SetConsistencyGroup(command.Command): _description = _("Set consistency group properties") diff --git a/openstackclient/volume/v2/volume.py b/openstackclient/volume/v2/volume.py index 301bf5e4..78db261b 100644 --- a/openstackclient/volume/v2/volume.py +++ b/openstackclient/volume/v2/volume.py @@ -417,16 +417,19 @@ class ListVolume(command.Lister): project_id = identity_common.find_project( identity_client, parsed_args.project, - parsed_args.project_domain) + parsed_args.project_domain).id user_id = None if parsed_args.user: user_id = identity_common.find_user(identity_client, parsed_args.user, - parsed_args.user_domain) + parsed_args.user_domain).id + + # set value of 'all_tenants' when using project option + all_projects = bool(parsed_args.project) or parsed_args.all_projects search_opts = { - 'all_tenants': parsed_args.all_projects, + 'all_tenants': all_projects, 'project_id': project_id, 'user_id': user_id, 'display_name': parsed_args.name, diff --git a/releasenotes/notes/add-network-qos-rule-22cc1ddd509941db.yaml b/releasenotes/notes/add-network-qos-rule-22cc1ddd509941db.yaml new file mode 100644 index 00000000..812d510f --- /dev/null +++ b/releasenotes/notes/add-network-qos-rule-22cc1ddd509941db.yaml @@ -0,0 +1,8 @@ +--- +features: + - | + Add support for Network QoS rule commands: + ``network qos rule create``, ``network qos rule delete``, + ``network qos rule list``, ``network qos rule show`` and + ``network qos rule set`` + [Bug `1609472 <https://bugs.launchpad.net/bugs/1609472>`_] diff --git a/releasenotes/notes/add-overwrite-option-to-router-7c50c8031dab6bae.yaml b/releasenotes/notes/add-overwrite-option-to-router-7c50c8031dab6bae.yaml index ac83b7e8..1b022cf1 100644 --- a/releasenotes/notes/add-overwrite-option-to-router-7c50c8031dab6bae.yaml +++ b/releasenotes/notes/add-overwrite-option-to-router-7c50c8031dab6bae.yaml @@ -1,7 +1,6 @@ --- features: - | - Add support to overwrite routes in a router instance, using ``--router`` - and ``--no-router`` option in ``osc router set``. + Add ``--router`` and ``--no-router`` options to ``osc router set`` command to + modify routes in a router instance. [ Blueprint `allow-overwrite-set-options <https://blueprints.launchpad.net/python-openstackclient/+spec/allow-overwrite-set-options>`_] - diff --git a/releasenotes/notes/bug-1613964-b3e8d9d828a3822c.yaml b/releasenotes/notes/bug-1613964-b3e8d9d828a3822c.yaml new file mode 100644 index 00000000..b11d0c1f --- /dev/null +++ b/releasenotes/notes/bug-1613964-b3e8d9d828a3822c.yaml @@ -0,0 +1,6 @@ +--- +features: + - Add ``consistency group add volume`` and ``consistency group remove volume`` commands + in volume v2. + [Bug `1642238 <https://bugs.launchpad.net/python-openstackclient/+bug/1642238>`_] + diff --git a/releasenotes/notes/bug-1652827-f59bbd1b64df958d.yaml b/releasenotes/notes/bug-1652827-f59bbd1b64df958d.yaml new file mode 100644 index 00000000..24991cbd --- /dev/null +++ b/releasenotes/notes/bug-1652827-f59bbd1b64df958d.yaml @@ -0,0 +1,6 @@ +--- +fixes: + - | + Fix an endpoint version problem with Image endpoints that contained the + substring 'v2'. + [Bug `1652827 <https://bugs.launchpad.net/bugs/1652827>`_] diff --git a/releasenotes/notes/bug-1655537-20b0eb676afa278f.yaml b/releasenotes/notes/bug-1655537-20b0eb676afa278f.yaml new file mode 100644 index 00000000..77cb3df0 --- /dev/null +++ b/releasenotes/notes/bug-1655537-20b0eb676afa278f.yaml @@ -0,0 +1,6 @@ +--- +fixes: + - | + Fixed a ``'Quota' object is not iterable`` error in the ``quota show`` command + that appeared with the initial release of openstacksdk v0.9.11 and v0.9.12. + [Bug `1655537 <https://bugs.launchpad.net/bugs/1655537>`_] diff --git a/releasenotes/notes/bug-1656402-88b12760fb2d4ef9.yaml b/releasenotes/notes/bug-1656402-88b12760fb2d4ef9.yaml new file mode 100644 index 00000000..c8a30298 --- /dev/null +++ b/releasenotes/notes/bug-1656402-88b12760fb2d4ef9.yaml @@ -0,0 +1,6 @@ +--- +fixes: + - | + Fix ``floating ip delete`` and ``floating ip show`` to accept IP addresses + in addition to IDs to select floating IPs to delete or show. + [Bug `1656402 <https://bugs.launchpad.net/bugs/1656402>`_ diff --git a/releasenotes/notes/bug-1656572-b40303ae50a41000.yaml b/releasenotes/notes/bug-1656572-b40303ae50a41000.yaml new file mode 100644 index 00000000..e6d0bcc5 --- /dev/null +++ b/releasenotes/notes/bug-1656572-b40303ae50a41000.yaml @@ -0,0 +1,6 @@ +--- +fixes: + - | + Work around a bug in OpenStackSDK 0.9.11 and 0.9.12 that causes + ``quota show --default`` to fail. + [Bug `1656572 <https://bugs.launchpad.net/python-openstackclient/+bug/1656572>`_] diff --git a/releasenotes/notes/bug-volume-list-with-project-2dc867c5e8346a66.yaml b/releasenotes/notes/bug-volume-list-with-project-2dc867c5e8346a66.yaml new file mode 100644 index 00000000..f2245201 --- /dev/null +++ b/releasenotes/notes/bug-volume-list-with-project-2dc867c5e8346a66.yaml @@ -0,0 +1,5 @@ +--- +fixes: + - | + Fix a bug of unable to filter volume list by ``--project`` + and ``--user`` options in the ``openstack volume list``. diff --git a/releasenotes/notes/image-set-to-update-image-membership-68221f226ca3b6e0.yaml b/releasenotes/notes/image-set-to-update-image-membership-68221f226ca3b6e0.yaml new file mode 100644 index 00000000..6db63f87 --- /dev/null +++ b/releasenotes/notes/image-set-to-update-image-membership-68221f226ca3b6e0.yaml @@ -0,0 +1,4 @@ +--- +features: + - Add support to update image membership with the ``--accept``, + ``--reject`` and ``--pending`` options of the ``image set command``. diff --git a/requirements.txt b/requirements.txt index 81a129e0..e5ca3c7b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,8 +6,8 @@ six>=1.9.0 # MIT Babel>=2.3.4 # BSD cliff>=2.3.0 # Apache-2.0 -keystoneauth1>=2.16.0 # Apache-2.0 -openstacksdk!=0.9.11,>=0.9.10 # Apache-2.0 +keystoneauth1>=2.17.0 # Apache-2.0 +openstacksdk!=0.9.11,!=0.9.12,>=0.9.10 # Apache-2.0 osc-lib>=1.2.0 # Apache-2.0 oslo.i18n>=2.1.0 # Apache-2.0 oslo.utils>=3.18.0 # Apache-2.0 @@ -377,6 +377,12 @@ openstack.network.v2 = network_qos_policy_set = openstackclient.network.v2.network_qos_policy:SetNetworkQosPolicy network_qos_policy_show = openstackclient.network.v2.network_qos_policy:ShowNetworkQosPolicy + network_qos_rule_create = openstackclient.network.v2.network_qos_rule:CreateNetworkQosRule + network_qos_rule_delete = openstackclient.network.v2.network_qos_rule:DeleteNetworkQosRule + network_qos_rule_list = openstackclient.network.v2.network_qos_rule:ListNetworkQosRule + network_qos_rule_set = openstackclient.network.v2.network_qos_rule:SetNetworkQosRule + network_qos_rule_show = openstackclient.network.v2.network_qos_rule:ShowNetworkQosRule + network_qos_rule_type_list = openstackclient.network.v2.network_qos_rule_type:ListNetworkQosRuleType network_rbac_create = openstackclient.network.v2.network_rbac:CreateNetworkRBAC @@ -522,9 +528,11 @@ openstack.volume.v2 = backup_restore = openstackclient.volume.v2.backup:RestoreBackup backup_show = openstackclient.volume.v2.backup:ShowBackup + consistency_group_add_volume = openstackclient.volume.v2.consistency_group:AddVolumeToConsistencyGroup consistency_group_create = openstackclient.volume.v2.consistency_group:CreateConsistencyGroup consistency_group_delete = openstackclient.volume.v2.consistency_group:DeleteConsistencyGroup consistency_group_list = openstackclient.volume.v2.consistency_group:ListConsistencyGroup + consistency_group_remove_volume = openstackclient.volume.v2.consistency_group:RemoveVolumeFromConsistencyGroup consistency_group_set = openstackclient.volume.v2.consistency_group:SetConsistencyGroup consistency_group_show = openstackclient.volume.v2.consistency_group:ShowConsistencyGroup diff --git a/test-requirements.txt b/test-requirements.txt index 08b49c98..b53d08dd 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -28,7 +28,7 @@ python-barbicanclient>=4.0.0 # Apache-2.0 python-congressclient<2000,>=1.3.0 # Apache-2.0 python-designateclient>=1.5.0 # Apache-2.0 python-heatclient>=1.6.1 # Apache-2.0 -python-ironicclient>=1.6.0 # Apache-2.0 +python-ironicclient>=1.9.0 # Apache-2.0 python-ironic-inspector-client>=1.5.0 # Apache-2.0 python-mistralclient>=2.0.0 # Apache-2.0 python-muranoclient>=0.8.2 # Apache-2.0 @@ -54,6 +54,16 @@ commands = setenv = OS_TEST_PATH=./openstackclient/tests/functional passenv = OS_* +[testenv:functional-tips] +setenv = OS_TEST_PATH=./openstackclient/tests/functional +passenv = OS_* +commands = + pip install -q -U -e "git+file:///opt/stack/new/osc-lib#egg=osc_lib" + pip install -q -U -e "git+file:///opt/stack/new/python-openstacksdk#egg=openstacksdk" + pip install -q -U -e "git+file:///opt/stack/new/os-client-config#egg=os_client_config" + pip freeze + ostestr {posargs} + [testenv:venv] commands = {posargs} |
