diff options
| author | Ubuntu Git Importer <usd-importer-do-not-mail@canonical.com> | 2020-07-02 17:11:03 +0000 |
|---|---|---|
| committer | Ubuntu Git Importer <usd-importer-do-not-mail@canonical.com> | 2020-07-02 17:11:03 +0000 |
| commit | b05c5f27b31d31c2ce31e6a45f30160329ba6948 (patch) | |
| tree | 3c788619c2713bc78995ce67481a12b6541e6d22 | |
| parent | 6cba309f131e026498ef5c7cf5a0de7af29346cd (diff) | |
New upstream version 2.5.0upstream/debian/2.5.0.xz
275 files changed, 16243 insertions, 4211 deletions
@@ -2,4 +2,3 @@ host=review.openstack.org port=29418 project=openstack/python-openstackclient.git -defaultbranch=stable/mitaka diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst new file mode 100644 index 0000000..de9324e --- /dev/null +++ b/CONTRIBUTING.rst @@ -0,0 +1,16 @@ +If you would like to contribute to the development of OpenStack, +you must follow the steps documented at: + + http://docs.openstack.org/infra/manual/developers.html#development-workflow + +Once those steps have been completed, changes to OpenStack +should be submitted for review via the Gerrit tool, following +the workflow documented at: + + http://docs.openstack.org/infra/manual/developers.html#development-workflow + +Pull requests submitted through GitHub will be ignored. + +Bugs should be filed on Launchpad, not GitHub: + + https://bugs.launchpad.net/python-openstackclient @@ -25,6 +25,7 @@ language to describe operations in OpenStack. * `Source`_ * `Developer` - getting started as a developer * `Contributing` - contributing code +* `Testing` - testing code * IRC: #openstack-sdks on Freenode (irc.freenode.net) * License: Apache 2.0 @@ -34,8 +35,9 @@ language to describe operations in OpenStack. .. _Blueprints: https://blueprints.launchpad.net/python-openstackclient .. _Bugs: https://bugs.launchpad.net/python-openstackclient .. _Source: https://git.openstack.org/cgit/openstack/python-openstackclient -.. _Developer: http://docs.openstack.org/infra/manual/python.html +.. _Developer: http://docs.openstack.org/project-team-guide/project-setup/python.html .. _Contributing: http://docs.openstack.org/infra/manual/developers.html +.. _Testing: http://docs.openstack.org/developer/python-openstackclient/developing.html#testing Getting Started =============== diff --git a/doc/source/backwards-incompatible.rst b/doc/source/backwards-incompatible.rst index bb2b0bd..00b314a 100644 --- a/doc/source/backwards-incompatible.rst +++ b/doc/source/backwards-incompatible.rst @@ -10,6 +10,9 @@ Should positional arguments for a command need to change, the OpenStackClient team attempts to make the transition as painless as possible. Look for deprecation warnings that indicate the new commands (or options) to use. +Commands labeled as a beta according to :doc:`command-beta` are exempt from +this backwards incompatible change handling. + List of Backwards Incompatible Changes ====================================== @@ -162,6 +165,36 @@ List of Backwards Incompatible Changes * Bug: https://bugs.launchpad.net/python-openstackclient/+bug/1546065 * Commit: https://review.openstack.org/#/c/281089/ +14. Output of `ip floating list` command has changed. + + When using Compute v2, the original output of `ip floating list` command is: + +----+--------+------------+----------+-------------+ + | ID | Pool | IP | Fixed IP | Instance ID | + +----+--------+-----------------------+-------------+ + | 1 | public | 172.24.4.1 | None | None | + +----+--------+------------+----------+-------------+ + + Now it changes to: + +----+---------------------+------------------+-----------+--------+ + | ID | Floating IP Address | Fixed IP Address | Server ID | Pool | + +----+---------------------+------------------+-----------+--------+ + | 1 | 172.24.4.1 | None | None | public | + +----+---------------------+------------------+-----------+--------+ + + When using Network v2, the output of `ip floating list` command is: + +--------------------------------------+---------------------+------------------+------+ + | ID | Floating IP Address | Fixed IP Address | Port | + +--------------------------------------+---------------------+------------------+------+ + | 1976df86-e66a-4f96-81bd-c6ffee6407f1 | 172.24.4.3 | None | None | + +--------------------------------------+---------------------+------------------+------+ + which is different from Compute v2. + + * In favor of: Use `ip floating list` command + * As of: NA + * Removed in: NA + * Bug: https://bugs.launchpad.net/python-openstackclient/+bug/1519502 + * Commit: https://review.openstack.org/#/c/277720/ + For Developers ============== diff --git a/doc/source/command-beta.rst b/doc/source/command-beta.rst new file mode 100644 index 0000000..53a4420 --- /dev/null +++ b/doc/source/command-beta.rst @@ -0,0 +1,72 @@ +============ +Command Beta +============ + +OpenStackClient releases do not always coincide with OpenStack +releases. This creates challenges when developing new OpenStackClient +commands for the current OpenStack release under development +since there may not be an official release of the REST API +enhancements necessary for the command. In addition, backwards +compatibility may not be guaranteed until an official OpenStack release. +To address these challenges, an OpenStackClient command may +be labeled as a beta command according to the guidelines +below. Such commands may introduce backwards incompatible +changes and may use REST API enhancements not yet released. + +See the examples below on how to label a command as a beta +by updating the command documentation, help and implementation. + +The initial release note must label the new command as a beta. +No further release notes are required until the command +is no longer a beta. At which time, the command beta label +or the command itself must be removed and a new release note +must be provided. + +Documentation +------------- + +The command documentation must label the command as a beta. + +example list +~~~~~~~~~~~~ + +List examples + +.. caution:: This is a beta command and subject to change. + Use global option ``--enable-beta-commands`` to + enable this command. + +.. program:: example list +.. code:: bash + + os example list + +Help +---- + +The command help must label the command as a beta. + +.. code-block:: python + + class ShowExample(command.ShowOne): + """Display example details + + (Caution: This is a beta command and subject to change. + Use global option --enable-beta-commands to enable + this command) + """ + +Implementation +-------------- + +The command must raise a ``CommandError`` exception if beta commands +are not enabled via ``--enable-beta-commands`` global option. + +.. code-block:: python + + def take_action(self, parsed_args): + if not self.app.options.enable_beta_commands: + msg = _('Caution: This is a beta command and subject to ' + 'change. Use global option --enable-beta-commands ' + 'to enable this command.') + raise exceptions.CommandError(msg) diff --git a/doc/source/command-errors.rst b/doc/source/command-errors.rst new file mode 100644 index 0000000..a24dc31 --- /dev/null +++ b/doc/source/command-errors.rst @@ -0,0 +1,201 @@ +============== +Command Errors +============== + +Handling errors in OpenStackClient commands is fairly straightforward. An +exception is thrown and handled by the application-level caller. + +Note: There are many cases that need to be filled out here. The initial +version of this document considers the general command error handling as well +as the specific case of commands that make multiple REST API calls and how to +handle when one or more of those calls fails. + +General Command Errors +====================== + +The general pattern for handling OpenStackClient command-level errors is to +raise a CommandError exception with an appropriate message. This should include +conditions arising from arguments that are not valid/allowed (that are not otherwise +enforced by ``argparse``) as well as errors arising from external conditions. + +External Errors +--------------- + +External errors are a result of things outside OpenStackClient not being as +expected. + +Example +~~~~~~~ + +This example is taken from ``keypair create`` where the ``--public-key`` option +specifies a file containing the public key to upload. If the file is not found, +the IOError exception is trapped and a more specific CommandError exception is +raised that includes the name of the file that was attempted to be opened. + +.. code-block:: python + + class CreateKeypair(command.ShowOne): + """Create new public key""" + + ## ... + + def take_action(self, parsed_args): + compute_client = self.app.client_manager.compute + + public_key = parsed_args.public_key + if public_key: + try: + with io.open( + os.path.expanduser(parsed_args.public_key), + "rb" + ) as p: + public_key = p.read() + except IOError as e: + msg = "Key file %s not found: %s" + raise exceptions.CommandError( + msg % (parsed_args.public_key, e), + ) + + keypair = compute_client.keypairs.create( + parsed_args.name, + public_key=public_key, + ) + + ## ... + +REST API Errors +=============== + +Most commands make a single REST API call via the supporting client library +or SDK. Errors based on HTML return codes are usually handled well by default, +but in some cases more specific or user-friendly messages need to be logged. +Trapping the exception and raising a CommandError exception with a useful +message is the correct approach. + +Multiple REST API Calls +----------------------- + +Some CLI commands make multiple calls to library APIs and thus REST APIs. +Most of the time these are ``create`` or ``set`` commands that expect to add or +change a resource on the server. When one of these calls fails, the behaviour +of the remainder of the command handler is defined as such: + +* Whenever possible, all API calls will be made. This may not be possible for + specific commands where the subsequent calls are dependent on the results of + an earlier call. + +* Any failure of an API call will be logged for the user + +* A failure of any API call results in a non-zero exit code + +* In the cases of failures in a ``create`` command a follow-up mode needs to + be present that allows the user to attempt to complete the call, or cleanly + remove the partially-created resource and re-try. + +The desired behaviour is for commands to appear to the user as idempotent +whenever possible, i.e. a partial failure in a ``set`` command can be safely +retried without harm. ``create`` commands are a harder problem and may need +to be handled by having the proper options in a set command available to allow +recovery in the case where the primary resource has been created but the +subsequent calls did not complete. + +Example 1 +~~~~~~~~~ + +This example is taken from the ``volume snapshot set`` command where ``--property`` +arguments are set using the volume manager's ``set_metadata()`` method, +``--state`` arguments are set using the ``reset_state()`` method, and the +remaining arguments are set using the ``update()`` method. + +.. code-block:: python + + class SetSnapshot(command.Command): + """Set snapshot properties""" + + ## ... + + def take_action(self, parsed_args): + volume_client = self.app.client_manager.volume + snapshot = utils.find_resource( + volume_client.volume_snapshots, + parsed_args.snapshot, + ) + + kwargs = {} + if parsed_args.name: + kwargs['name'] = parsed_args.name + if parsed_args.description: + kwargs['description'] = parsed_args.description + + result = 0 + if parsed_args.property: + try: + volume_client.volume_snapshots.set_metadata( + snapshot.id, + parsed_args.property, + ) + except SomeException: # Need to define the exceptions to catch here + self.app.log.error("Property set failed") + result += 1 + + if parsed_args.state: + try: + volume_client.volume_snapshots.reset_state( + snapshot.id, + parsed_args.state, + ) + except SomeException: # Need to define the exceptions to catch here + self.app.log.error("State set failed") + result += 1 + + try: + volume_client.volume_snapshots.update( + snapshot.id, + **kwargs + ) + except SomeException: # Need to define the exceptions to catch here + self.app.log.error("Update failed") + result += 1 + + # NOTE(dtroyer): We need to signal the error, and a non-zero return code, + # without aborting prematurely + if result > 0: + raise SomeNonFatalException + +Example 2 +~~~~~~~~~ + +This example is taken from the ``network delete`` command which takes multiple +networks to delete. All networks will be delete in a loop, which makes multiple +``delete_network()`` calls. + +.. code-block:: python + + class DeleteNetwork(common.NetworkAndComputeCommand): + """Delete network(s)""" + + def update_parser_common(self, parser): + parser.add_argument( + 'network', + metavar="<network>", + nargs="+", + help=("Network(s) to delete (name or ID)") + ) + return parser + + def take_action(self, client, parsed_args): + ret = 0 + + for network in parsed_args.network: + try: + obj = client.find_network(network, ignore_missing=False) + client.delete_network(obj) + except Exception: + self.app.log.error("Failed to delete network with name " + "or ID %s." % network) + ret += 1 + + if ret > 0: + total = len(parsed_args.network) + msg = "Failed to delete %s of %s networks." % (ret, total) + raise exceptions.CommandError(msg) diff --git a/doc/source/command-objects/address-scope.rst b/doc/source/command-objects/address-scope.rst new file mode 100644 index 0000000..a2ab566 --- /dev/null +++ b/doc/source/command-objects/address-scope.rst @@ -0,0 +1,120 @@ +============= +address scope +============= + +An **address scope** is a scope of IPv4 or IPv6 addresses that belongs +to a given project and may be shared between projects. + +Network v2 + +address scope create +-------------------- + +Create new address scope + +.. program:: address scope create +.. code:: bash + + os address scope create + [--project <project> [--project-domain <project-domain>]] + [--ip-version <ip-version>] + [--share | --no-share] + <name> + +.. option:: --project <project> + + Owner's project (name or ID) + +.. option:: --project-domain <project-domain> + + Domain the project belongs to (name or ID). + This can be used in case collisions between project names exist. + +.. option:: --ip-version <ip-version> + + IP version (4 or 6, default is 4) + +.. option:: --share + + Share the address scope between projects + +.. option:: --no-share + + Do not share the address scope between projects (default) + +.. _address_scope_create-name: +.. describe:: <name> + + New address scope name + +address scope delete +-------------------- + +Delete an address scope + +.. program:: address scope delete +.. code:: bash + + os address scope delete + <address-scope> + +.. _address_scope_delete-address-scope: +.. describe:: <address-scope> + + Address scope to delete (name or ID) + +address scope list +------------------ + +List address scopes + +.. program:: address scope list +.. code:: bash + + os address scope list + +address scope set +----------------- + +Set address scope properties + +.. program:: address scope set +.. code:: bash + + os address scope set + [--name <name>] + [--share | --no-share] + <address-scope> + +.. option:: --name <name> + + Set address scope name + +.. option:: --share + + Share the address scope between projects + +.. option:: --no-share + + Do not share the address scope between projects + +.. _address_scope_set-address-scope: +.. describe:: <address-scope> + + Address scope to modify (name or ID) + +address scope show +------------------ + +Display address scope details + +.. program:: address scope show +.. code:: bash + + os address scope show + <address-scope> + +.. _address_scope_show-address-scope: +.. describe:: <address-scope> + + Address scope to display (name or ID) diff --git a/doc/source/command-objects/aggregate.rst b/doc/source/command-objects/aggregate.rst index 6a1dd90..6c4baa5 100644 --- a/doc/source/command-objects/aggregate.rst +++ b/doc/source/command-objects/aggregate.rst @@ -2,7 +2,7 @@ aggregate ========= -Server aggregates provide a mechanism to group servers according to certain +Host aggregates provide a mechanism to group hosts according to certain criteria. Compute v2 @@ -150,3 +150,26 @@ Display aggregate details .. describe:: <aggregate> Aggregate to display (name or ID) + +aggregate unset +--------------- + +Unset aggregate properties + +.. program:: aggregate unset +.. code-block:: bash + + os aggregate unset + --property <key> + [--property <key>] ... + <aggregate> + +.. option:: --property <key> + + Property to remove from :ref:`\<aggregate\> <aggregate_unset-aggregate>` + (repeat option to remove multiple properties) + +.. _aggregate_unset-aggregate: +.. describe:: <aggregate> + + Aggregate to modify (name or ID) diff --git a/doc/source/command-objects/availability-zone.rst b/doc/source/command-objects/availability-zone.rst index 1f56843..149e108 100644 --- a/doc/source/command-objects/availability-zone.rst +++ b/doc/source/command-objects/availability-zone.rst @@ -2,6 +2,9 @@ availability zone ================= +An **availability zone** is a logical partition of cloud block storage, +compute and network services. + Block Storage v2, Compute v2, Network v2 availability zone list diff --git a/doc/source/command-objects/compute-service.rst b/doc/source/command-objects/compute-service.rst index 7bcab4d..bda64c8 100644 --- a/doc/source/command-objects/compute-service.rst +++ b/doc/source/command-objects/compute-service.rst @@ -31,15 +31,20 @@ List service command os compute service list [--host <host>] [--service <service>] + [--long] .. _compute-service-list: -.. describe:: --host <host> +.. option:: --host <host> - Name of host + List services on specified host (name only) -.. describe:: --service <service> +.. option:: --service <service> - Name of service + List only specified service (name only) + +.. option:: --long + + List additional fields in output compute service set @@ -50,19 +55,25 @@ Set service command .. program:: compute service set .. code:: bash - os compute service list + os compute service set [--enable | --disable] + [--disable-reason <reason>] <host> <service> .. _compute-service-set: -.. describe:: --enable +.. option:: --enable Enable service (default) -.. describe:: --disable +.. option:: --disable Disable service +.. option:: --disable-reason <reason> + + Reason for disabling the service (in quotes). Note that when the service + is enabled, this option is ignored. + .. describe:: <host> Name of host diff --git a/doc/source/command-objects/credentials.rst b/doc/source/command-objects/credential.rst index 9f4aabe..ed8a00d 100644 --- a/doc/source/command-objects/credentials.rst +++ b/doc/source/command-objects/credential.rst @@ -1,27 +1,27 @@ =========== -credentials +credential =========== Identity v3 -credentials create +credential create ------------------ .. ''[consider rolling the ec2 creds into this too]'' .. code:: bash - os credentials create + os credential create --x509 [<private-key-file>] [<certificate-file>] -credentials show +credential show ---------------- .. code:: bash - os credentials show + os credential show [--token] [--user] [--x509 [--root]] diff --git a/doc/source/command-objects/flavor.rst b/doc/source/command-objects/flavor.rst index 2389f7d..322943d 100644 --- a/doc/source/command-objects/flavor.rst +++ b/doc/source/command-objects/flavor.rst @@ -49,7 +49,7 @@ Create new flavor .. option:: --rxtx-factor <factor> - RX/TX factor (default 1) + RX/TX factor (default 1.0) .. option:: --public @@ -118,22 +118,6 @@ List flavors Maximum number of flavors to display -flavor show ------------ - -Display flavor details - -.. program:: flavor show -.. code:: bash - - os flavor show - <flavor> - -.. _flavor_show-flavor: -.. describe:: <flavor> - - Flavor to display (name or ID) - flavor set ---------- @@ -154,6 +138,22 @@ Set flavor properties Flavor to modify (name or ID) +flavor show +----------- + +Display flavor details + +.. program:: flavor show +.. code:: bash + + os flavor show + <flavor> + +.. _flavor_show-flavor: +.. describe:: <flavor> + + Flavor to display (name or ID) + flavor unset ------------ diff --git a/doc/source/command-objects/host.rst b/doc/source/command-objects/host.rst index 680efcc..ede62c5 100644 --- a/doc/source/command-objects/host.rst +++ b/doc/source/command-objects/host.rst @@ -21,6 +21,41 @@ List all hosts Only return hosts in the availability zone +host set +-------- + +Set host command + +.. program:: host set +.. code:: bash + + os host set + [--enable | --disable] + [--enable-maintenance | --disable-maintenance] + <host> + +.. _host-set: +.. option:: --enable + + Enable the host + +.. option:: --disable + + Disable the host + +.. _maintenance-set: +.. option:: --enable-maintenance + + Enable maintenance mode for the host + +.. option:: --disable-maintenance + + Disable maintenance mode for the host + +.. describe:: <host> + + The host (name or ID) + host show --------- diff --git a/doc/source/command-objects/identity-provider.rst b/doc/source/command-objects/identity-provider.rst index ac46273..ca773d8 100644 --- a/doc/source/command-objects/identity-provider.rst +++ b/doc/source/command-objects/identity-provider.rst @@ -22,8 +22,8 @@ Create new identity provider .. option:: --remote-id <remote-id> - Remote IDs to associate with the Identity Provider (repeat to provide - multiple values) + Remote IDs to associate with the Identity Provider + (repeat option to provide multiple values) .. option:: --remote-id-file <file-name> @@ -87,8 +87,8 @@ Set identity provider properties .. option:: --remote-id <remote-id> - Remote IDs to associate with the Identity Provider (repeat to provide - multiple values) + Remote IDs to associate with the Identity Provider + (repeat option to provide multiple values) .. option:: --remote-id-file <file-name> diff --git a/doc/source/command-objects/image.rst b/doc/source/command-objects/image.rst index 61872ec..942c03d 100644 --- a/doc/source/command-objects/image.rst +++ b/doc/source/command-objects/image.rst @@ -4,6 +4,33 @@ image Image v1, v2 +image add project +----------------- + +*Only supported for Image v2* + +Associate project with image + +.. program:: image add project +.. code:: bash + + os image add project + [--project-domain <project-domain>] + <image> <project> + +.. option:: --project-domain <project-domain> + + Domain the project belongs to (name or ID). + This can be used in case collisions between project names exist. + +.. describe:: <image> + + Image to share (name or ID). + +.. describe:: <project> + + Project to associate with image (name or ID) + image create ------------ @@ -206,6 +233,33 @@ List available images The last image (name or ID) of the previous page. Display list of images after marker. Display all images if not specified. +image remove project +-------------------- + +*Only supported for Image v2* + +Disassociate project with image + +.. program:: image remove project +.. code:: bash + + os image remove remove + [--project-domain <project-domain>] + <image> <project> + +.. option:: --project-domain <project-domain> + + Domain the project belongs to (name or ID). + This can be used in case collisions between project names exist. + +.. describe:: <image> + + Image to unshare (name or ID). + +.. describe:: <project> + + Project to disassociate with image (name or ID) + image save ---------- @@ -445,57 +499,3 @@ Display image details .. describe:: <image> Image to display (name or ID) - -image add project ------------------ - -*Only supported for Image v2* - -Associate project with image - -.. program:: image add project -.. code:: bash - - os image add project - [--project-domain <project-domain>] - <image> <project> - -.. option:: --project-domain <project-domain> - - Domain the project belongs to (name or ID). - This can be used in case collisions between project names exist. - -.. describe:: <image> - - Image to share (name or ID). - -.. describe:: <project> - - Project to associate with image (name or ID) - -image remove project --------------------- - -*Only supported for Image v2* - -Disassociate project with image - -.. program:: image remove project -.. code:: bash - - os image remove remove - [--project-domain <project-domain>] - <image> <project> - -.. option:: --project-domain <project-domain> - - Domain the project belongs to (name or ID). - This can be used in case collisions between project names exist. - -.. describe:: <image> - - Image to unshare (name or ID). - -.. describe:: <project> - - Project to disassociate with image (name or ID) diff --git a/doc/source/command-objects/ip-floating.rst b/doc/source/command-objects/ip-floating.rst index 99d06d0..869b7af 100644 --- a/doc/source/command-objects/ip-floating.rst +++ b/doc/source/command-objects/ip-floating.rst @@ -33,11 +33,35 @@ Create new floating IP address .. code:: bash os ip floating create - <pool> + [--subnet <subnet>] + [--port <port>] + [--floating-ip-address <floating-ip-address>] + [--fixed-ip-address <fixed-ip-address>] + <network> -.. describe:: <pool> +.. option:: --subnet <subnet> - Pool to fetch IP address from (name or ID) + Subnet on which you want to create the floating IP (name or ID) + (Network v2 only) + +.. option:: --port <port> + + Port to be associated with the floating IP (name or ID) + (Network v2 only) + +.. option:: --floating-ip-address <floating-ip-address> + + Floating IP address + (Network v2 only) + +.. option:: --fixed-ip-address <fixed-ip-address> + + Fixed IP address mapped to the floating IP + (Network v2 only) + +.. describe:: <network> + + Network to allocate floating IP from (name or ID) ip floating delete ------------------ @@ -45,7 +69,7 @@ ip floating delete Delete floating IP .. program:: ip floating delete - .. code:: bash +.. code:: bash os ip floating delete <floating-ip> @@ -89,7 +113,7 @@ ip floating show Display floating IP details .. program:: ip floating show - .. code:: bash +.. code:: bash os ip floating show <floating-ip> diff --git a/doc/source/command-objects/limits.rst b/doc/source/command-objects/limits.rst index 6a7509f..87a5b33 100644 --- a/doc/source/command-objects/limits.rst +++ b/doc/source/command-objects/limits.rst @@ -37,4 +37,4 @@ Show compute and block storage limits .. option:: --domain <domain> - Domain that owns --project (name or ID) [only valid with --absolute] + Domain the project belongs to (name or ID) [only valid with --absolute] diff --git a/doc/source/command-objects/network.rst b/doc/source/command-objects/network.rst index 9e10927..8668dae 100644 --- a/doc/source/command-objects/network.rst +++ b/doc/source/command-objects/network.rst @@ -2,6 +2,13 @@ network ======= +A **network** is an isolated Layer 2 networking segment. There are two types +of networks, project and provider networks. Project networks are fully isolated +and are not shared with other projects. Provider networks map to existing +physical networks in the data center and provide external network access for +servers and other resources. Only an OpenStack administrator can create +provider networks. Networks can be connected via routers. + Compute v2, Network v2 network create @@ -17,28 +24,37 @@ Create new network [--enable | --disable] [--share | --no-share] [--availability-zone-hint <availability-zone>] + [--external [--default | --no-default] | --internal] + [--provider-network-type <provider-network-type>] + [--provider-physical-network <provider-physical-network>] + [--provider-segment <provider-segment>] + [--transparent-vlan | --no-transparent-vlan] <name> .. option:: --project <project> Owner's project (name or ID) - (Network v2 only) + + *Network version 2 only* .. option:: --project-domain <project-domain> Domain the project belongs to (name or ID). This can be used in case collisions between project names exist. - (Network v2 only) + + *Network version 2 only* .. option:: --enable Enable network (default) - (Network v2 only) + + *Network version 2 only* .. option:: --disable Disable network - (Network v2 only) + + *Network version 2 only* .. option:: --share @@ -50,14 +66,75 @@ Create new network .. option:: --availability-zone-hint <availability-zone> - Availability Zone in which to create this network (requires the Network - Availability Zone extension, this option can be repeated). - (Network v2 only) + Availability Zone in which to create this network + (Network Availability Zone extension required, + repeat option to set multiple availability zones) + + *Network version 2 only* .. option:: --subnet <subnet> IPv4 subnet for fixed IPs (in CIDR notation) - (Compute v2 network only) + + *Compute version 2 only* + +.. option:: --external + + Set this network as an external network + (external-net extension required) + + *Network version 2 only* + +.. option:: --internal + + Set this network as an internal network (default) + + *Network version 2 only* + +.. option:: --default + + Specify if this network should be used as + the default external network + + *Network version 2 only* + +.. option:: --no-default + + Do not use the network as the default external network + (default) + + *Network version 2 only* + +.. option:: --provider-network-type <provider-network-type> + + The physical mechanism by which the virtual network is implemented. + The supported options are: flat, gre, local, vlan, vxlan + + *Network version 2 only* + +.. option:: --provider-physical-network <provider-physical-network> + + Name of the physical network over which the virtual network is implemented + + *Network version 2 only* + +.. option:: --provider-segment <provider-segment> + + VLAN ID for VLAN networks or Tunnel ID for GRE/VXLAN networks + + *Network version 2 only* + +.. option:: --transparent-vlan + + Make the network VLAN transparent + + *Network version 2 only* + +.. option:: --no-transparent-vlan + + Do not make the network VLAN transparent + + *Network version 2 only* .. _network_create-name: .. describe:: <name> @@ -105,6 +182,8 @@ network set Set network properties +*Network version 2 only* + .. program:: network set .. code:: bash @@ -112,6 +191,11 @@ Set network properties [--name <name>] [--enable | --disable] [--share | --no-share] + [--external [--default | --no-default] | --internal] + [--provider-network-type <provider-network-type>] + [--provider-physical-network <provider-physical-network>] + [--provider-segment <provider-segment>] + [--transparent-vlan | --no-transparent-vlan] <network> .. option:: --name <name> @@ -134,6 +218,44 @@ Set network properties Do not share the network between projects +.. option:: --external + + Set this network as an external network. + (external-net extension required) + +.. option:: --internal + + Set this network as an internal network + +.. option:: --default + + Set the network as the default external network + +.. option:: --no-default + + Do not use the network as the default external network. + +.. option:: --provider-network-type <provider-network-type> + + The physical mechanism by which the virtual network is implemented. + The supported options are: flat, gre, local, vlan, vxlan + +.. option:: --provider-physical-network <provider-physical-network> + + Name of the physical network over which the virtual network is implemented + +.. option:: --provider-segment <provider-segment> + + VLAN ID for VLAN networks or Tunnel ID for GRE/VXLAN networks + +.. option:: --transparent-vlan + + Make the network VLAN transparent + +.. option:: --no-transparent-vlan + + Do not make the network VLAN transparent + .. _network_set-network: .. describe:: <network> diff --git a/doc/source/command-objects/port.rst b/doc/source/command-objects/port.rst index 414b743..e4cf2cd 100644 --- a/doc/source/command-objects/port.rst +++ b/doc/source/command-objects/port.rst @@ -2,8 +2,91 @@ port ==== +A **port** is a connection point for attaching a single device, such as the +NIC of a server, to a network. The port also describes the associated network +configuration, such as the MAC and IP addresses to be used on that port. + Network v2 +port create +----------- + +Create new port + +.. program:: port create +.. code:: bash + + os port create + --network <network> + [--fixed-ip subnet=<subnet>,ip-address=<ip-address>] + [--device <device-id>] + [--device-owner <device-owner>] + [--vnic-type <vnic-type>] + [--binding-profile <binding-profile>] + [--host <host-id>] + [--enable | --disable] + [--mac-address <mac-address>] + [--project <project> [--project-domain <project-domain>]] + <name> + +.. option:: --network <network> + + Network this port belongs to (name or ID) + +.. option:: --fixed-ip subnet=<subnet>,ip-address=<ip-address> + + Desired IP and/or subnet (name or ID) for this port: + subnet=<subnet>,ip-address=<ip-address> + (repeat option to set multiple fixed IP addresses) + +.. option:: --device <device-id> + + Port device ID + +.. option:: --device-owner <device-owner> + + Device owner of this port + +.. option:: --vnic-type <vnic-type> + + VNIC type for this port (direct | direct-physical | macvtap | normal | baremetal, + default: normal) + +.. option:: --binding-profile <binding-profile> + + Custom data to be passed as binding:profile: <key>=<value> + (repeat option to set multiple binding:profile data) + +.. option:: --host <host-id> + + Allocate port on host ``<host-id>`` (ID only) + +.. option:: --enable + + Enable port (default) + +.. option:: --disable + + Disable port + +.. option:: --mac-address <mac-address> + + MAC address of this port + +.. option:: --project <project> + + Owner's project (name or ID) + +.. option:: --project-domain <project-domain> + + Domain the project belongs to (name or ID). + This can be used in case collisions between project names exist. + +.. _port_create-name: +.. describe:: <name> + + Name of this port + port delete ----------- @@ -20,6 +103,93 @@ Delete port(s) Port(s) to delete (name or ID) +port list +--------- + +List ports + +.. program:: port list +.. code:: bash + + os port list + [--router <router>] + +.. option:: --router <router> + + List only ports attached to this router (name or ID) + +port set +-------- + +Set port properties + +.. program:: port set +.. code:: bash + + os port set + [--fixed-ip subnet=<subnet>,ip-address=<ip-address> | --no-fixed-ip] + [--device <device-id>] + [--device-owner <device-owner>] + [--vnic-type <vnic-type>] + [--binding-profile <binding-profile> | --no-binding-profile] + [--host <host-id>] + [--enable | --disable] + [--name <name>] + <port> + +.. option:: --fixed-ip subnet=<subnet>,ip-address=<ip-address> + + Desired IP and/or subnet (name or ID) for this port: + subnet=<subnet>,ip-address=<ip-address> + (repeat option to set multiple fixed IP addresses) + +.. option:: --no-fixed-ip + + Clear existing information of fixed IP addresses + +.. option:: --device <device-id> + + Port device ID + +.. option:: --device-owner <device-owner> + + Device owner of this port + +.. option:: --vnic-type <vnic-type> + + VNIC type for this port (direct | direct-physical | macvtap | normal | baremetal, + default: normal) + +.. option:: --binding-profile <binding-profile> + + Custom data to be passed as binding:profile: <key>=<value> + (repeat option to set multiple binding:profile data) + +.. option:: --no-binding-profile + + Clear existing information of binding:profile + +.. option:: --host <host-id> + + Allocate port on host ``<host-id>`` (ID only) + +.. option:: --enable + + Enable port + +.. option:: --disable + + Disable port + +.. option:: --name + + Set port name + +.. _port_set-port: +.. describe:: <port> + + Port to modify (name or ID) + port show --------- diff --git a/doc/source/command-objects/quota.rst b/doc/source/command-objects/quota.rst index 98e6df3..dc5e362 100644 --- a/doc/source/command-objects/quota.rst +++ b/doc/source/command-objects/quota.rst @@ -4,7 +4,7 @@ quota Resource quotas appear in multiple APIs, OpenStackClient presents them as a single object with multiple properties. -Compute v2, Block Storage v1 +Block Storage v1, Compute v2, Network v2 quota set --------- @@ -32,6 +32,20 @@ Set quotas for project [--volumes <new-volumes>] [--volume-type <volume-type>] + # Network settings + [--floating-ips <num-floatingips>] + [--secgroup-rules <num-security-group-rules>] + [--secgroups <num-security-groups>] + [--networks <num-networks>] + [--subnets <num-subnets>] + [--ports <num-ports>] + [--routers <num-routers>] + [--rbac-policies <num-rbac-policies>] + [--vips <num-vips>] + [--subnetpools <num-subnetpools>] + [--members <num-members>] + [--health-monitors <num-health-monitors>] + <project> Set quotas for class @@ -126,17 +140,53 @@ Set quotas for class Set quotas for a specific <volume-type> +.. option:: --networks <num-networks> + + New value for the networks quota + +.. option:: --subnets <num-subnets> + + New value for the subnets quota + +.. option:: --ports <num-ports> + + New value for the ports quota + +.. option:: --routers <num-routers> + + New value for the routers quota + +.. option:: --rbac-policies <num-rbac-policies> + + New value for the rbac-policies quota + +.. option:: --vips <num-vips> + + New value for the vips quota + +.. option:: --subnetpools <num-subnetpools> + + New value for the subnetpools quota + +.. option:: --members <num-members> + + New value for the members quota + +.. option:: --health-monitors <num-health-monitors> + + New value for the health-monitors quota + quota show ---------- -Show quotas for project +Show quotas for project or class .. program:: quota show .. code:: bash os quota show [--default] - <project> + [<project>] .. option:: --default @@ -146,13 +196,13 @@ Show quotas for project .. _quota_show-project: .. describe:: <project> - Show quotas for class + Show quotas for this project (name or ID) .. code:: bash os quota show --class - <class> + [<class>] .. option:: --class @@ -161,4 +211,4 @@ Show quotas for project .. _quota_show-class: .. describe:: <class> - Class to show + Show quotas for this class (name or ID) diff --git a/doc/source/command-objects/router.rst b/doc/source/command-objects/router.rst index 6b8b357..9ca7661 100644 --- a/doc/source/command-objects/router.rst +++ b/doc/source/command-objects/router.rst @@ -2,8 +2,56 @@ router ====== +A **router** is a logical component that forwards data packets between +networks. It also provides Layer 3 and NAT forwarding to provide external +network access for servers on project networks. + Network v2 +router add port +--------------- + +Add a port to a router + +.. program:: router add port +.. code:: bash + + os router add port + <router> + <port> + +.. _router_add_port: + +.. describe:: <router> + + Router to which port will be added (name or ID) + +.. describe:: <port> + + Port to be added (name or ID) + +router add subnet +----------------- + +Add a subnet to a router + +.. program:: router add subnet +.. code:: bash + + os router add subnet + <router> + <subnet> + +.. _router_add_subnet: + +.. describe:: <router> + + Router to which subnet will be added (name or ID) + +.. describe:: <subnet> + + Subnet to be added (name or ID) + router create ------------- @@ -16,7 +64,7 @@ Create new router [--project <project> [--project-domain <project-domain>]] [--enable | --disable] [--distributed] - [--availability-zone-hint <availability-zone>] + [--availability-zone-hint <availability-zone>] <name> .. option:: --project <project> @@ -42,8 +90,9 @@ Create new router .. option:: --availability-zone-hint <availability-zone> - Availability Zone in which to create this router (requires the Router - Availability Zone extension, this option can be repeated). + Availability Zone in which to create this router + (Router Availability Zone extension required, + repeat option to set multiple availability zones) .. _router_create-name: .. describe:: <name> @@ -81,6 +130,50 @@ List routers List additional fields in output +router remove port +------------------ + +Remove a port from a router + +.. program:: router remove port +.. code:: bash + + os router remove port + <router> + <port> + +.. _router_remove_port: + +.. describe:: <router> + + Router from which port will be removed (name or ID) + +.. describe:: <port> + + Port to be removed (name or ID) + +router remove subnet +-------------------- + +Remove a subnet from a router + +.. program:: router remove subnet +.. code:: bash + + os router remove subnet + <router> + <subnet> + +.. _router_remove_subnet: + +.. describe:: <router> + + Router from which subnet will be removed (name or ID) + +.. describe:: <subnet> + + Subnet to be removed (name or ID) + router set ---------- @@ -93,7 +186,7 @@ Set router properties [--name <name>] [--enable | --disable] [--distributed | --centralized] - [--route destination=<subnet>,gateway=<ip-address> | --clear-routes] + [--route destination=<subnet>,gateway=<ip-address> | --no-route] <router> .. option:: --name <name> @@ -118,12 +211,12 @@ Set router properties .. option:: --route destination=<subnet>,gateway=<ip-address> - Routes associated with the router. - Repeat this option to set multiple routes. - destination: destination subnet (in CIDR notation). - gateway: nexthop IP address. + Routes associated with the router + destination: destination subnet (in CIDR notation) + gateway: nexthop IP address + (repeat option to set multiple routes) -.. option:: --clear-routes +.. option:: --no-route Clear routes associated with the router diff --git a/doc/source/command-objects/security-group-rule.rst b/doc/source/command-objects/security-group-rule.rst index 58e7a0a..97cce35 100644 --- a/doc/source/command-objects/security-group-rule.rst +++ b/doc/source/command-objects/security-group-rule.rst @@ -2,6 +2,9 @@ security group rule =================== +A **security group rule** specifies the network access rules for servers +and other resources on the network. + Compute v2, Network v2 security group rule create @@ -13,26 +16,86 @@ Create a new security group rule .. code:: bash os security group rule create - [--proto <proto>] [--src-ip <ip-address> | --src-group <group>] - [--dst-port <port-range>] + [--dst-port <port-range> | [--icmp-type <icmp-type> [--icmp-code <icmp-code>]]] + [--protocol <protocol>] + [--ingress | --egress] + [--ethertype <ethertype>] + [--project <project> [--project-domain <project-domain>]] <group> -.. option:: --proto <proto> - - IP protocol (icmp, tcp, udp; default: tcp) - .. option:: --src-ip <ip-address> - Source IP address block (may use CIDR notation; default: 0.0.0.0/0) + Source IP address block + (may use CIDR notation; default for IPv4 rule: 0.0.0.0/0) .. option:: --src-group <group> - Source security group (ID only) + Source security group (name or ID) .. option:: --dst-port <port-range> - Destination port, may be a range: 137:139 (default: 0; only required for proto tcp and udp) + Destination port, may be a single port or a starting and + ending port range: 137:139. Required for IP protocols TCP + and UDP. Ignored for ICMP IP protocols. + +.. option:: --icmp-type <icmp-type> + + ICMP type for ICMP IP protocols + + *Network version 2 only* + +.. option:: --icmp-code <icmp-code> + + ICMP code for ICMP IP protocols + + *Network version 2 only* + +.. option:: --protocol <protocol> + + IP protocol (icmp, tcp, udp; default: tcp) + + *Compute version 2* + + IP protocol (ah, dccp, egp, esp, gre, icmp, igmp, + ipv6-encap, ipv6-frag, ipv6-icmp, ipv6-nonxt, + ipv6-opts, ipv6-route, ospf, pgm, rsvp, sctp, tcp, + udp, udplite, vrrp and integer representations [0-255]; + default: tcp) + + *Network version 2* + +.. option:: --ingress + + Rule applies to incoming network traffic (default) + + *Network version 2 only* + +.. option:: --egress + + Rule applies to outgoing network traffic + + *Network version 2 only* + +.. option:: --ethertype <ethertype> + + Ethertype of network traffic + (IPv4, IPv6; default: based on IP protocol) + + *Network version 2 only* + +.. option:: --project <project> + + Owner's project (name or ID) + + *Network version 2 only* + +.. option:: --project-domain <project-domain> + + Domain the project belongs to (name or ID). + This can be used in case collisions between project names exist. + + *Network version 2 only* .. describe:: <group> @@ -62,8 +125,23 @@ List security group rules .. code:: bash os security group rule list + [--all-projects] + [--long] [<group>] +.. option:: --all-projects + + Display information from all projects (admin only) + + *Network version 2 ignores this option and will always display information* + *for all projects (admin only).* + +.. option:: --long + + List additional fields in output + + *Compute version 2 does not have additional fields to display.* + .. describe:: <group> List all rules in this security group (name or ID) diff --git a/doc/source/command-objects/security-group.rst b/doc/source/command-objects/security-group.rst index 22ea5cb..3af11b5 100644 --- a/doc/source/command-objects/security-group.rst +++ b/doc/source/command-objects/security-group.rst @@ -2,6 +2,10 @@ security group ============== +A **security group** acts as a virtual firewall for servers and other +resources on a network. It is a container for security group rules +which specify the network access rules. + Compute v2, Network v2 security group create @@ -14,12 +18,26 @@ Create a new security group os security group create [--description <description>] + [--project <project> [--project-domain <project-domain>]] <name> .. option:: --description <description> Security group description +.. option:: --project <project> + + Owner's project (name or ID) + + *Network version 2 only* + +.. option:: --project-domain <project-domain> + + Domain the project belongs to (name or ID). + This can be used in case collisions between project names exist. + + *Network version 2 only* + .. describe:: <name> New security group name @@ -55,7 +73,7 @@ List security groups Display information from all projects (admin only) *Network version 2 ignores this option and will always display information* - *for all projects.* + *for all projects (admin only).* security group set ------------------ diff --git a/doc/source/command-objects/server-group.rst b/doc/source/command-objects/server-group.rst new file mode 100644 index 0000000..55d789c --- /dev/null +++ b/doc/source/command-objects/server-group.rst @@ -0,0 +1,80 @@ +============ +server group +============ + +Server group provides a mechanism to group servers according to certain policy. + +Compute v2 + +server group create +------------------- + +Create a new server group + +.. program:: server group create +.. code-block:: bash + + os server group create + --policy <policy> [--policy <policy>] ... + <name> + +.. option:: --policy <policy> + + Add a policy to :ref:`\<name\> <server_group_create-name>` + (repeat option to add multiple policies) + +.. _server_group_create-name: +.. describe:: <name> + + New server group name + +server group delete +------------------- + +Delete an existing server group + +.. program:: server group delete +.. code-block:: bash + + os server group delete + <server-group> [<server-group> ...] + +.. describe:: <server-group> + + Server group(s) to delete (name or ID) + (repeat to delete multiple server groups) + +server group list +----------------- + +List all server groups + +.. program:: server group list +.. code-block:: bash + + os server group list + [--all-projects] + [--long] + +.. option:: --all-projects + + Display information from all projects (admin only) + +.. option:: --long + + List additional fields in output + +server group show +----------------- + +Display server group details + +.. program:: server group show +.. code-block:: bash + + os server group show + <server-group> + +.. describe:: <server-group> + + Server group to display (name or ID) diff --git a/doc/source/command-objects/server.rst b/doc/source/command-objects/server.rst index d50ad37..bf97298 100644 --- a/doc/source/command-objects/server.rst +++ b/doc/source/command-objects/server.rst @@ -90,7 +90,7 @@ Create a new server .. option:: --security-group <security-group-name> Security group to assign to this server (name or ID) - (repeat for multiple groups) + (repeat option to set multiple groups) .. option:: --key-name <key-name> @@ -98,11 +98,13 @@ Create a new server .. option:: --property <key=value> - Set a property on this server (repeat for multiple values) + Set a property on this server + (repeat option to set multiple values) .. option:: --file <dest-filename=source-filename> - File to inject into image before boot (repeat for multiple files) + File to inject into image before boot + (repeat option to set multiple files) .. option:: --user-data <user-data> @@ -514,6 +516,21 @@ process for the user: the first is to perform the resize, the second is to either confirm (verify) success and release the old server, or to declare a revert to release the new server and restart the old one. +server restore +-------------- + +Restore server(s) from soft-deleted state + +.. program:: server restore +.. code:: bash + + os server restore + <server> [<server> ...] + +.. describe:: <server> + + Server(s) to restore (name or ID) + server resume ------------- @@ -554,8 +571,8 @@ Set server properties .. option:: --property <key=value> - Property to add/change for this server (repeat option to set - multiple properties) + Property to add/change for this server + (repeat option to set multiple properties) .. describe:: <server> @@ -599,7 +616,7 @@ Show server details server ssh ---------- -Ssh to server +SSH to server .. program:: server ssh .. code:: bash @@ -749,7 +766,8 @@ Unset server properties .. option:: --property <key> - Property key to remove from server (repeat to set multiple values) + Property key to remove from server + (repeat option to remove multiple values) .. describe:: <server> diff --git a/doc/source/command-objects/snapshot.rst b/doc/source/command-objects/snapshot.rst index b483868..9033011 100644 --- a/doc/source/command-objects/snapshot.rst +++ b/doc/source/command-objects/snapshot.rst @@ -2,7 +2,7 @@ snapshot ======== -Block Storage v1 +Block Storage v1, v2 snapshot create --------------- @@ -82,6 +82,7 @@ Set snapshot properties [--name <name>] [--description <description>] [--property <key=value> [...] ] + [--state <state>] <snapshot> .. _snapshot_restore-snapshot: @@ -97,6 +98,14 @@ Set snapshot properties Property to add or modify for this snapshot (repeat option to set multiple properties) +.. option:: --state <state> + + New snapshot state. + Valid values are "available", "error", "creating", + "deleting", and "error_deleting". + + *Volume version 2 only* + .. describe:: <snapshot> Snapshot to modify (name or ID) diff --git a/doc/source/command-objects/subnet-pool.rst b/doc/source/command-objects/subnet-pool.rst index e181cae..8abf25a 100644 --- a/doc/source/command-objects/subnet-pool.rst +++ b/doc/source/command-objects/subnet-pool.rst @@ -2,8 +2,82 @@ subnet pool =========== +A **subnet pool** contains a collection of prefixes in CIDR notation +that are available for IP address allocation. + Network v2 +subnet pool create +------------------ + +Create subnet pool + +.. program:: subnet pool create +.. code:: bash + + os subnet pool create + [--default-prefix-length <default-prefix-length>] + [--min-prefix-length <min-prefix-length>] + [--max-prefix-length <max-prefix-length>] + [--project <project> [--project-domain <project-domain>]] + [--address-scope <address-scope>] + [--default | --no-default] + [--share | --no-share] + --pool-prefix <pool-prefix> [...] + <name> + +.. option:: --default-prefix-length <default-prefix-length> + + Set subnet pool default prefix length + +.. option:: --min-prefix-length <min-prefix-length> + + Set subnet pool minimum prefix length + +.. option:: --max-prefix-length <max-prefix-length> + + Set subnet pool maximum prefix length + +.. option:: --project <project> + + Owner's project (name or ID) + +.. option:: --project-domain <project-domain> + + Domain the project belongs to (name or ID). This can be used in case + collisions between project names exist. + +.. option:: --address-scope <address-scope> + + Set address scope associated with the subnet pool (name or ID), + prefixes must be unique across address scopes + +.. option:: --default + + Set this as a default subnet pool + +.. option:: --no-default + + Set this as a non-default subnet pool + +.. option:: --share + + Set this subnet pool as shared + +.. option:: --no-share + + Set this subnet pool as not shared + +.. describe:: --pool-prefix <pool-prefix> + + Set subnet pool prefixes (in CIDR notation) + (repeat option to set multiple prefixes) + +.. _subnet_pool_create-name: +.. describe:: <name> + + Name of the new subnet pool + subnet pool delete ------------------ @@ -35,6 +109,67 @@ List subnet pools List additional fields in output +subnet pool set +--------------- + +Set subnet pool properties + +.. program:: subnet pool set +.. code:: bash + + os subnet pool set + [--name <name>] + [--pool-prefix <pool-prefix> [...]] + [--default-prefix-length <default-prefix-length>] + [--min-prefix-length <min-prefix-length>] + [--max-prefix-length <max-prefix-length>] + [--address-scope <address-scope> | --no-address-scope] + [--default | --no-default] + <subnet-pool> + +.. option:: --name <name> + + Set subnet pool name + +.. option:: --pool-prefix <pool-prefix> + + Set subnet pool prefixes (in CIDR notation) + (repeat option to set multiple prefixes) + +.. option:: --default-prefix-length <default-prefix-length> + + Set subnet pool default prefix length + +.. option:: --min-prefix-length <min-prefix-length> + + Set subnet pool minimum prefix length + +.. option:: --max-prefix-length <max-prefix-length> + + Set subnet pool maximum prefix length + +.. option:: --address-scope <address-scope> + + Set address scope associated with the subnet pool (name or ID), + prefixes must be unique across address scopes + +.. option:: --no-address-scope + + Remove address scope associated with the subnet pool + +.. option:: --default + + Set this as a default subnet pool + +.. option:: --no-default + + Set this as a non-default subnet pool + +.. _subnet_pool_set-subnet-pool: +.. describe:: <subnet-pool> + + Subnet pool to modify (name or ID) + subnet pool show ---------------- diff --git a/doc/source/command-objects/subnet.rst b/doc/source/command-objects/subnet.rst index 97d5c68..ff6354e 100644 --- a/doc/source/command-objects/subnet.rst +++ b/doc/source/command-objects/subnet.rst @@ -2,8 +2,120 @@ subnet ====== +A **subnet** is a block of IP addresses and associated configuration state. +Subnets are used to allocate IP addresses when new ports are created on a +network. + Network v2 +subnet create +------------- + +Create new subnet + +.. program:: subnet create +.. code:: bash + + os subnet create + [--project <project> [--project-domain <project-domain>]] + [--subnet-pool <subnet-pool> | --use-default-subnet-pool [--prefix-length <prefix-length>]] + [--subnet-range <subnet-range>] + [--allocation-pool start=<ip-address>,end=<ip-address>] + [--dhcp | --no-dhcp] + [--dns-nameserver <dns-nameserver>] + [--gateway <gateway>] + [--host-route destination=<subnet>,gateway=<ip-address>] + [--ip-version {4,6}] + [--ipv6-ra-mode {dhcpv6-stateful,dhcpv6-stateless,slaac}] + [--ipv6-address-mode {dhcpv6-stateful,dhcpv6-stateless,slaac}] + --network <network> + <name> + +.. option:: --project <project> + + Owner's project (name or ID) + +.. option:: --project-domain <project-domain> + + Domain the project belongs to (name or ID). + This can be used in case collisions between project names exist. + +.. option:: --subnet-pool <subnet-pool> + + Subnet pool from which this subnet will obtain a CIDR (name or ID) + +.. option:: --use-default-subnet-pool + + Use default subnet pool for ``--ip-version`` + +.. option:: --prefix-length <prefix-length> + + Prefix length for subnet allocation from subnet pool + +.. option:: --subnet-range <subnet-range> + + Subnet range in CIDR notation + (required if ``--subnet-pool`` is not specified, optional otherwise) + +.. option:: --allocation-pool start=<ip-address>,end=<ip-address> + + Allocation pool IP addresses for this subnet e.g.: + ``start=192.168.199.2,end=192.168.199.254`` + (repeat option to add multiple IP addresses) + +.. option:: --dhcp + + Enable DHCP (default) + +.. option:: --no-dhcp + + Disable DHCP + +.. option:: --dns-nameserver <dns-nameserver> + + DNS server for this subnet (repeat option to set multiple DNS servers) + +.. option:: --gateway <gateway> + + Specify a gateway for the subnet. The three options are: + <ip-address>: Specific IP address to use as the gateway, + 'auto': Gateway address should automatically be chosen from + within the subnet itself, 'none': This subnet will not use + a gateway, e.g.: ``--gateway 192.168.9.1``, ``--gateway auto``, + ``--gateway none`` (default is 'auto') + +.. option:: --host-route destination=<subnet>,gateway=<ip-address> + + Additional route for this subnet e.g.: + ``destination=10.10.0.0/16,gateway=192.168.71.254`` + destination: destination subnet (in CIDR notation) + gateway: nexthop IP address + (repeat option to add multiple routes) + +.. option:: --ip-version {4,6} + + IP version (default is 4). Note that when subnet pool is specified, + IP version is determined from the subnet pool and this option + is ignored. + +.. option:: --ipv6-ra-mode {dhcpv6-stateful,dhcpv6-stateless,slaac} + + IPv6 RA (Router Advertisement) mode, + valid modes: [dhcpv6-stateful, dhcpv6-stateless, slaac] + +.. option:: --ipv6-address-mode {dhcpv6-stateful,dhcpv6-stateless,slaac} + + IPv6 address mode, valid modes: [dhcpv6-stateful, dhcpv6-stateless, slaac] + +.. option:: --network <network> + + Network this subnet belongs to (name or ID) + +.. _subnet_create-name: +.. describe:: <name> + + Name of subnet to create + subnet delete ------------- @@ -35,10 +147,74 @@ List subnets List additional fields in output +.. option:: --ip-version {4, 6} + + List only subnets of given IP version in output + +subnet set +---------- + +Set subnet properties + +.. program:: subnet set +.. code:: bash + + os subnet set + [--allocation-pool start=<ip-address>,end=<ip-address>] + [--dhcp | --no-dhcp] + [--dns-nameserver <dns-nameserver>] + [--gateway <gateway-ip>] + [--host-route destination=<subnet>,gateway=<ip-address>] + [--name <new-name>] + <subnet> + +.. option:: --allocation-pool start=<ip-address>,end=<ip-address> + + Allocation pool IP addresses for this subnet e.g.: + ``start=192.168.199.2,end=192.168.199.254`` + (repeat option to add multiple IP addresses) + +.. option:: --dhcp + + Enable DHCP + +.. option:: --no-dhcp + + Disable DHCP + +.. option:: --dns-nameserver <dns-nameserver> + + DNS server for this subnet (repeat option to set multiple DNS servers) + +.. option:: --gateway <gateway> + + Specify a gateway for the subnet. The options are: + <ip-address>: Specific IP address to use as the gateway, + 'none': This subnet will not use a gateway, + e.g.: ``--gateway 192.168.9.1``, ``--gateway none`` + +.. option:: --host-route destination=<subnet>,gateway=<ip-address> + + Additional route for this subnet e.g.: + ``destination=10.10.0.0/16,gateway=192.168.71.254`` + destination: destination subnet (in CIDR notation) + gateway: nexthop IP address + (repeat option to add multiple routes) + +.. option:: --name + + Updated name of the subnet + +.. _subnet_set-subnet: +.. describe:: <subnet> + + Subnet to modify (name or ID) + + subnet show ----------- -Show subnet details +Display subnet details .. program:: subnet show .. code:: bash @@ -49,4 +225,4 @@ Show subnet details .. _subnet_show-subnet: .. describe:: <subnet> - Subnet to show (name or ID) + Subnet to display (name or ID) diff --git a/doc/source/command-objects/trust.rst b/doc/source/command-objects/trust.rst index 556edc5..cb44c6b 100644 --- a/doc/source/command-objects/trust.rst +++ b/doc/source/command-objects/trust.rst @@ -29,7 +29,7 @@ Create new trust .. option:: --role <role> - Roles to authorize (name or ID) (repeat to set multiple values) (required) + Roles to authorize (name or ID) (repeat option to set multiple values, required) .. option:: --impersonate diff --git a/doc/source/command-objects/volume-service.rst b/doc/source/command-objects/volume-service.rst new file mode 100644 index 0000000..aa9fa6d --- /dev/null +++ b/doc/source/command-objects/volume-service.rst @@ -0,0 +1,31 @@ +============== +volume service +============== + +Volume v1, v2 + +volume service list +------------------- + +List volume service + +.. program:: volume service list +.. code:: bash + + os volume service list + [--host <host>] + [--service <service>] + [--long] + +.. _volume-service-list: +.. option:: --host <host> + + List services on specified host (name only) + +.. option:: --service <service> + + List only specified service (name only) + +.. option:: --long + + List additional fields in output diff --git a/doc/source/command-objects/volume-type.rst b/doc/source/command-objects/volume-type.rst index 8a11384..b7aea63 100644 --- a/doc/source/command-objects/volume-type.rst +++ b/doc/source/command-objects/volume-type.rst @@ -20,7 +20,7 @@ Create new volume type .. option:: --description <description> - New volume type description + Volume type description .. versionadded:: 2 @@ -40,9 +40,10 @@ Create new volume type Set a property on this volume type (repeat option to set multiple properties) +.. _volume_type_create-name: .. describe:: <name> - New volume type name + Volume type name volume type delete ------------------ @@ -55,6 +56,7 @@ Delete volume type os volume type delete <volume-type> +.. _volume_type_delete-volume-type: .. describe:: <volume-type> Volume type to delete (name or ID) @@ -86,6 +88,8 @@ Set volume type properties [--name <name>] [--description <description>] [--property <key=value> [...] ] + [--project <project>] + [--project-domain <project-domain>] <volume-type> .. option:: --name <name> @@ -100,14 +104,43 @@ Set volume type properties .. versionadded:: 2 +.. option:: --project <project> + + Set volume type access to project (name or ID) (admin only) + + *Volume version 2 only* + +.. option:: --project-domain <project-domain> + + Domain the project belongs to (name or ID). + This can be used in case collisions between project names exist. + .. option:: --property <key=value> - Property to add or modify for this volume type (repeat option to set multiple properties) + Set a property on this volume type (repeat option to set multiple properties) +.. _volume_type_set-volume-type: .. describe:: <volume-type> Volume type to modify (name or ID) +volume type show +---------------- + +Display volume type details + + +.. program:: volume type show +.. code:: bash + + os volume type show + <volume-type> + +.. _volume_type_show-volume-type: +.. describe:: <volume-type> + + Volume type to display (name or ID) + volume type unset ----------------- @@ -118,12 +151,26 @@ Unset volume type properties os volume type unset [--property <key>] + [--project <project>] + [--project-domain <project-domain>] <volume-type> .. option:: --property <key> Property to remove from volume type (repeat option to remove multiple properties) +.. option:: --project <project> + + Removes volume type access from project (name or ID) (admin only) + + *Volume version 2 only* + +.. option:: --project-domain <project-domain> + + Domain the project belongs to (name or ID). + This can be used in case collisions between project names exist. + +.. _volume_type_unset-volume-type: .. describe:: <volume-type> Volume type to modify (name or ID) diff --git a/doc/source/command-objects/volume.rst b/doc/source/command-objects/volume.rst index bf89dc4..a51d111 100644 --- a/doc/source/command-objects/volume.rst +++ b/doc/source/command-objects/volume.rst @@ -14,32 +14,45 @@ Create new volume os volume create --size <size> + [--type <volume-type>] + [--image <image>] [--snapshot <snapshot>] + [--source <volume>] [--description <description>] - [--type <volume-type>] [--user <user>] [--project <project>] [--availability-zone <availability-zone>] - [--image <image>] - [--source <volume>] [--property <key=value> [...] ] <name> .. option:: --size <size> (required) - New volume size in GB + Volume size in GB + +.. option:: --type <volume-type> + + Set the type of volume + + Select :option:`\<volume-type\>` from the available types as shown + by ``volume type list``. + +.. option:: --image <image> + + Use :option:`\<image\>` as source of volume (name or ID) + + This is commonly used to create a boot volume for a server. .. option:: --snapshot <snapshot> - Use <snapshot> as source of new volume + Use :option:`\<snapshot\>` as source of volume (name or ID) -.. option:: --description <description> +.. option:: --source <source> - New volume description + Volume to clone (name or ID) -.. option:: --type <volume-type> +.. option:: --description <description> - Use <volume-type> as the new volume type + Volume description .. option:: --user <user> @@ -51,23 +64,16 @@ Create new volume .. option:: --availability-zone <availability-zone> - Create new volume in <availability-zone> - -.. option:: --image <image> - - Use <image> as source of new volume (name or ID) - -.. option:: --source <source> - - Volume to clone (name or ID) + Create volume in :option:`\<availability-zone\>` .. option:: --property <key=value> Set a property on this volume (repeat option to set multiple properties) +.. _volume_create-name: .. describe:: <name> - New volume name + Volume name The :option:`--project` and :option:`--user` options are typically only useful for admin users, but may be allowed for other users depending on @@ -83,12 +89,13 @@ Delete volume(s) os volume delete [--force] - <volume> [<volume> ...] + <volume> [<volume> ...] .. option:: --force Attempt forced removal of volume(s), regardless of state (defaults to False) +.. _volume_delete-volume: .. describe:: <volume> Volume(s) to delete (name or ID) @@ -102,35 +109,37 @@ List volumes .. code:: bash os volume list - [--all-projects] [--project <project> [--project-domain <project-domain>]] [--user <user> [--user-domain <user-domain>]] [--name <name>] [--status <status>] + [--all-projects] [--long] .. option:: --project <project> - Filter results by project (name or ID) (admin only) + Filter results by :option:`\<project\>` (name or ID) (admin only) *Volume version 2 only* .. option:: --project-domain <project-domain> Domain the project belongs to (name or ID). + This can be used in case collisions between project names exist. *Volume version 2 only* .. option:: --user <user> - Filter results by user (name or ID) (admin only) + Filter results by :option:`\<user\>` (name or ID) (admin only) *Volume version 2 only* .. option:: --user-domain <user-domain> Domain the user belongs to (name or ID). + This can be used in case collisions between user names exist. *Volume version 2 only* @@ -161,27 +170,40 @@ Set volume properties os volume set [--name <name>] - [--description <description>] [--size <size>] + [--description <description>] [--property <key=value> [...] ] + [--image-property <key=value> [...] ] <volume> .. option:: --name <name> New volume name +.. option:: --size <size> + + Extend volume size in GB + .. option:: --description <description> New volume description -.. option:: --size <size> +.. option:: --property <key=value> - Extend volume size in GB + Set a property on this volume (repeat option to set multiple properties) -.. option:: --property <key=value> +.. option:: --image-property <key=value> + + Set an image property on this volume + (repeat option to set multiple image properties) - Property to add or modify for this volume (repeat option to set multiple properties) + Image properties are copied along with the image when creating a volume + using :option:`--image`. Note that these properties are immutable on the + image itself, this option updates the copy attached to this volume. + *Volume version 2 only* + +.. _volume_set-volume: .. describe:: <volume> Volume to modify (name or ID) @@ -197,6 +219,7 @@ Show volume details os volume show <volume> +.. _volume_show-volume: .. describe:: <volume> Volume to display (name or ID) @@ -211,12 +234,21 @@ Unset volume properties os volume unset [--property <key>] + [--image-property <key>] <volume> .. option:: --property <key> - Property to remove from volume (repeat option to remove multiple properties) + Remove a property from volume (repeat option to remove multiple properties) + +.. option:: --image-property <key> + + Remove an image property from volume + (repeat option to remove multiple image properties) + + *Volume version 2 only* +.. _volume_unset-volume: .. describe:: <volume> Volume to modify (name or ID) diff --git a/doc/source/command-options.rst b/doc/source/command-options.rst index b723e98..5cb8468 100644 --- a/doc/source/command-options.rst +++ b/doc/source/command-options.rst @@ -100,11 +100,111 @@ An example parser declaration: choice_option.add_argument( '--test', - metavar='<test>, + metavar='<test>', choices=['choice1', 'choice2', 'choice3'], help=_('Test type (choice1, choice2 or choice3)'), ) +Options with Multiple Values +---------------------------- + +Some options can be repeated to build a collection of values for a property. +Adding a value to the collection must be provided via the ``set`` action. +Removing a value from the collection must be provided via an ``unset`` action. +As a convenience, removing all values from the collection may be provided via a +``--no`` option on the ``set`` and ``unset`` actions. If both ``--no`` option +and option are specified, the values specified on the command would overwrite +the collection property instead of appending on the ``set`` action. The +``--no`` option must be part of a mutually exclusive group with the related +property option on the ``unset`` action, overwrite case don't exist in +``unset`` action. + +An example behavior for ``set`` action: + +Append: + +.. code-block:: bash + + object set --example-property xxx + +Overwrite: + +.. code-block:: bash + + object set --no-example-property --example-property xxx + +The example below assumes a property that contains a list of unique values. +However, this example can also be applied to other collections using the +appropriate parser action and action implementation (e.g. a dict of key/value +pairs). Implementations will vary depending on how the REST API handles +adding/removing values to/from the collection and whether or not duplicate +values are allowed. + +Implementation +~~~~~~~~~~~~~~ + +An example parser declaration for `set` action: + +.. code-block:: python + + parser.add_argument( + '--example-property', + metavar='<example-property>', + dest='example_property', + action='append', + help=_('Example property for this <resource> ' + '(repeat option to set multiple properties)'), + ) + parser.add_argument( + '--no-example-property', + dest='no_example_property', + action='store_true', + help=_('Remove all example properties for this <resource>'), + ) + +An example handler in `take_action()` for `set` action: + +.. code-block:: python + + if parsed_args.example_property and parsed_args.no_example_property: + kwargs['example_property'] = parsed_args.example_property + elif parsed_args.example_property: + kwargs['example_property'] = \ + resource_example_property + parsed_args.example_property + elif parsed_args.no_example_property: + kwargs['example_property'] = [] + +An example parser declaration for `unset` action: + +.. code-block:: python + + example_property_group = parser.add_mutually_exclusive_group() + example_property_group.add_argument( + '--example-property', + metavar='<example-property>', + dest='example_property', + action='append', + help=_('Example property for this <resource> ' + '(repeat option to remove multiple properties)'), + ) + example_property_group.add_argument( + '--no-example-property', + dest='no_example_property', + action='store_true', + help=_('Remove all example properties for this <resource>'), + ) + +An example handler in `take_action()` for `unset` action: + +.. code-block:: python + + if parsed_args.example_property: + kwargs['example_property'] = \ + list(set(resource_example_property) - \ + set(parsed_args.example_property)) + if parsed_args.no_example_property: + kwargs['example_property'] = [] + List Command Options ==================== diff --git a/doc/source/commands.rst b/doc/source/commands.rst index 5678e5d..12542d1 100644 --- a/doc/source/commands.rst +++ b/doc/source/commands.rst @@ -70,17 +70,20 @@ the API resources will be merged, as in the ``quota`` object that has options referring to both Compute and Volume quotas. * ``access token``: (**Identity**) long-lived OAuth-based token +* ``address scope``: (**Network**) a scope of IPv4 or IPv6 addresses +* ``aggregate``: (**Compute**) a grouping of compute hosts * ``availability zone``: (**Compute**, **Network**, **Volume**) a logical partition of hosts or block storage or network services -* ``aggregate``: (**Compute**) a grouping of servers * ``backup``: (**Volume**) a volume copy * ``catalog``: (**Identity**) service catalog * ``command``: (**Internal**) installed commands in the OSC process +* ``compute agent``: (**Compute**) a cloud Compute agent available to a hypervisor +* ``compute service``: (**Compute**) a cloud Compute process running on a host * ``configuration``: (**Internal**) openstack client configuration * ``console log``: (**Compute**) server console text dump * ``console url``: (**Compute**) server remote console URL * ``consumer``: (**Identity**) OAuth-based delegatee * ``container``: (**Object Storage**) a grouping of objects -* ``credentials``: (**Identity**) specific to identity providers +* ``credential``: (**Identity**) specific to identity providers * ``domain``: (**Identity**) a grouping of projects * ``ec2 credentials``: (**Identity**) AWS EC2-compatible credentials * ``endpoint``: (**Identity**) the base URL used to contact a specific service @@ -88,7 +91,7 @@ referring to both Compute and Volume quotas. * ``federation protocol``: (**Identity**) the underlying protocol used while federating identities * ``flavor``: (**Compute**) predefined server configurations: ram, root disk, etc * ``group``: (**Identity**) a grouping of users -* ``host``: (**Compute**) - the physical computer running a hypervisor +* ``host``: (**Compute**) - the physical computer running compute services * ``hypervisor``: (**Compute**) the virtual machine manager * ``hypervisor stats``: (**Compute**) hypervisor statistics over all compute nodes * ``identity provider``: (**Identity**) a source of users and authentication @@ -102,6 +105,7 @@ referring to both Compute and Volume quotas. * ``module``: (**Internal**) - installed Python modules in the OSC process * ``network``: (**Compute**, **Network**) - a virtual network for connecting servers and other resources * ``object``: (**Object Storage**) a single file in the Object Storage +* ``object store account``: (**Object Storage**) owns a group of Object Storage resources * ``policy``: (**Identity**) determines authorization * ``port``: (**Network**) - a virtual port for connecting servers and other resources to a network * ``project``: (**Identity**) owns a group of resources @@ -115,6 +119,7 @@ referring to both Compute and Volume quotas. * ``security group rule``: (**Compute**, **Network**) - the individual rules that define protocol/IP/port access * ``server``: (**Compute**) virtual machine instance * ``server dump``: (**Compute**) a dump file of a server created by features like kdump +* ``server group``: (**Compute**) a grouping of servers * ``server image``: (**Compute**) saved server disk image * ``service``: (**Identity**) a cloud service * ``service provider``: (**Identity**) a resource that consumes assertions from an ``identity provider`` @@ -122,11 +127,14 @@ referring to both Compute and Volume quotas. * ``subnet``: (**Network**) - a contiguous range of IP addresses assigned to a network * ``subnet pool``: (**Network**) - a pool of subnets * ``token``: (**Identity**) a bearer token managed by Identity service +* ``trust``: (**Identity**) project-specific role delegation between users, with optional impersonation * ``usage``: (**Compute**) display host resources being consumed * ``user``: (**Identity**) individual cloud resources users * ``user role``: (**Identity**) roles assigned to a user * ``volume``: (**Volume**) block volumes +* ``volume qos``: (**Volume**) quality-of-service (QoS) specification for volumes * ``volume type``: (**Volume**) deployment-specific types of volumes available +* ``volume service``: (**Volume**) services to manage block storage operations Plugin Objects @@ -139,6 +147,17 @@ list check out :doc:`plugin-commands`. * ``action definition``: (**Workflow Engine (Mistral)**) * ``action execution``: (**Workflow Engine (Mistral)**) * ``baremetal``: (**Baremetal (Ironic)**) +* ``cluster``: (**Clustering (Senlin)**) +* ``cluster action``: (**Clustering (Senlin)**) +* ``cluster event``: (**Clustering (Senlin)**) +* ``cluster members``: (**Clustering (Senlin)**) +* ``cluster node``: (**Clustering (Senlin)**) +* ``cluster policy``: (**CLustering (Senlin)**) +* ``cluster policy binding``: (**Clustering (Senlin)**) +* ``cluster policy type``: (**Clustering (Senlin)**) +* ``cluster profile``: (**Clustering (Senlin)**) +* ``cluster profile type``: (**Clustering (Senlin)**) +* ``cluster receiver``: (**Clustering (Senlin)**) * ``congress datasource``: (**Policy (Congress)**) * ``congress driver``: (**Policy (Congress)**) * ``congress policy``: (**Policy (Congress)**) @@ -190,6 +209,7 @@ Those actions with an opposite action are noted in parens if applicable. the positional arguments appear in the same order * ``create`` (``delete``) - create a new occurrence of the specified object * ``delete`` (``create``) - delete specific occurrences of the specified objects +* ``expand`` (``shrink``) - increase the capacity of a cluster * ``issue`` (``revoke``) - issue a token * ``list`` - display summary information about multiple objects * ``lock`` (``unlock``) - lock one or more servers so that non-admin user won't be able to execute actions @@ -200,14 +220,15 @@ Those actions with an opposite action are noted in parens if applicable. * ``rebuild`` - rebuild a server using (most of) the same arguments as in the original create * ``remove`` (``add``) - remove an object from a group of objects * ``rescue`` (``unrescue``) - reboot a server in a special rescue mode allowing access to the original disks -* ``resize`` - change a server's flavor -* ``restore`` - restore a heat stack snapshot +* ``resize`` - change a server's flavor or a cluster's capacity +* ``restore`` - restore a heat stack snapshot or restore a server in soft-deleted state * ``resume`` (``suspend``) - return one or more suspended servers to running state * ``revoke`` (``issue``) - revoke a token * ``save`` - download an object locally * ``set`` (``unset``) - set a property on the object, formerly called metadata * ``shelve`` (``unshelve``) - shelve one or more servers * ``show`` - display detailed information about the specific object +* ``shrink`` (``expand``) - reduce the capacity of a cluster * ``start`` (``stop``) - start one or more servers * ``stop`` (``start``) - stop one or more servers * ``suspend`` (``resume``) - stop one or more servers and save to disk freeing memory diff --git a/doc/source/configuration.rst b/doc/source/configuration.rst index c770014..d80b3b3 100644 --- a/doc/source/configuration.rst +++ b/doc/source/configuration.rst @@ -140,8 +140,8 @@ that appears in :file:`clouds.yaml` Debugging ~~~~~~~~~ -You may find the :doc:`config show <command-objects/config>` -helpful to debug configuration issues. It will display your current +You may find the :doc:`configuration show <command-objects/configuration>` +command helpful to debug configuration issues. It will display your current configuration. Logging Settings diff --git a/doc/source/developing.rst b/doc/source/developing.rst index 9f8e55c..2ddaeb2 100644 --- a/doc/source/developing.rst +++ b/doc/source/developing.rst @@ -8,9 +8,9 @@ Communication Meetings ========= The OpenStackClient team meets regularly on every Thursday. For details -please refer to the `wiki`_. +please refer to the `OpenStack IRC meetings`_ page. -.. _`wiki`: https://wiki.openstack.org/wiki/Meetings/OpenStackClient +.. _`OpenStack IRC meetings`: http://eavesdrop.openstack.org/#OpenStackClient_Team_Meeting Testing ------- diff --git a/doc/source/index.rst b/doc/source/index.rst index b1cc056..0869344 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -13,6 +13,7 @@ User Documentation .. toctree:: :maxdepth: 1 + Manual Page <man/openstack> command-list commands configuration @@ -22,7 +23,6 @@ User Documentation interactive humaninterfaceguide backwards-incompatible - man/openstack Getting Started --------------- @@ -47,8 +47,10 @@ Developer Documentation :maxdepth: 1 developing + command-beta command-options command-wrappers + command-errors specs/commands Project Goals diff --git a/doc/source/man/openstack.rst b/doc/source/man/openstack.rst index f02705d..8cd10a7 100644 --- a/doc/source/man/openstack.rst +++ b/doc/source/man/openstack.rst @@ -114,6 +114,12 @@ OPTIONS :option:`--verify` | :option:`--insecure` Verify or ignore server certificate (default: verify) +:option:`--os-cert` <certificate-file> + Client certificate bundle file + +:option:`--os-key` <key-file> + Client certificate key file + :option:`--os-identity-api-version` <identity-api-version> Identity API version (Default: 2.0) @@ -123,11 +129,11 @@ OPTIONS :option:`--os-interface` <interface> Interface type. Valid options are `public`, `admin` and `internal`. -:option: `--profile` <hmac-key> - HMAC key to use for encrypting context data for performance profiling of - requested operation. This key should be the value of one of the HMAC keys - defined in the configuration files of OpenStack services, user would like - to trace through. +:option:`--os-profile` <hmac-key> + Performance profiling HMAC key for encrypting context data + + This key should be the value of one of the HMAC keys defined in the + configuration files of OpenStack services to be traced. :option:`--log-file` <LOGFILE> Specify a file to log output. Disabled by default. @@ -141,6 +147,9 @@ OPTIONS :option:`--debug` show tracebacks on errors and set verbosity to debug +:option:`--enable-beta-commands` + Enable beta commands which are subject to change + COMMANDS ======== @@ -367,6 +376,12 @@ The following environment variables can be set to alter the behaviour of :progra :envvar:`OS_CACERT` CA certificate bundle file +:envvar:`OS_CERT` + Client certificate bundle file + +:envvar:`OS_KEY` + Client certificate key file + :envvar:`OS_IDENTITY_API_VERSION` Identity API version (Default: 2.0) diff --git a/doc/source/plugin-commands.rst b/doc/source/plugin-commands.rst index a3f4152..b23e7a3 100644 --- a/doc/source/plugin-commands.rst +++ b/doc/source/plugin-commands.rst @@ -33,4 +33,24 @@ Note: To see the complete syntax for the plugin commands, see the `CLI_Ref`_ .. list-plugins:: openstack.orchestration.v1 :detailed: +.. list-plugins:: openstack.search.v1 + :detailed: + +.. list-plugins:: openstack.baremetal_introspection.v1 + :detailed: + +.. list-plugins:: openstack.application_catalog.v1 + :detailed: + +.. list-plugins:: openstack.clustering.v1 + :detailed: + +.. # tripleoclient is not in global-requirements +.. #.. list-plugins:: openstack.tripleoclient.v1 +.. # :detailed: + +.. # cueclient is not in global-requirements +.. #.. list-plugins:: openstack.mb.v1 +.. # :detailed: + .. _CLI_Ref: http://docs.openstack.org/cli-reference/openstack.html
\ No newline at end of file diff --git a/doc/source/plugins.rst b/doc/source/plugins.rst index f5bbd6d..5911b33 100644 --- a/doc/source/plugins.rst +++ b/doc/source/plugins.rst @@ -19,32 +19,40 @@ Other OpenStack services, such as Orchestration or Telemetry may create an OpenStackClient plugin. The source code will not be hosted by OpenStackClient. -The following is a list of projects and their status as an OpenStackClient -plugin. - -============================= ====================================== - project notes -============================= ====================================== -python-barbicanclient using OpenStackClient -python-ceilometerclient using argparse -python-congressclient using OpenStackClient -python-cueclient using OpenStackClient -python-designateclient using OpenStackClient -python-heatclient using OpenStackClient -python-ironicclient Using OpenStackClient -python-magnumclient using argparse -python-manilaclient using argparse -python-mistralclient using OpenStackClient -python-muranoclient using argparse -python-saharaclient using OpenStackClient -python-searchlightclient using OpenStackClient -python-troveclient using argparse -python-zaqarclient using OpenStackClient -============================= ====================================== +The following is a list of projects that are an OpenStackClient plugin. + +- python-barbicanclient +- python-congressclient +- python-cueclient\*\* +- python-designateclient +- python-heatclient +- python-ironicclient +- python-ironic-inspector-client +- python-mistralclient +- python-muranoclient +- python-saharaclient +- python-searchlightclient +- python-senlinclient +- python-tripleoclient\*\* +- python-zaqarclient + +\*\* Note that some clients are not listed in global-requirements + +The following is a list of projects that are not an OpenStackClient plugin. + +- aodhclient +- gnocchiclient +- python-troveclient +- python-magnumclient +- python-ceilometerclient +- python-solumclient Implementation ============== +Client module +------------- + Plugins are discovered by enumerating the entry points found under :py:mod:`openstack.cli.extension` and initializing the specified client module. @@ -60,7 +68,9 @@ The client module must define the following top-level variables: * ``API_NAME`` - A string containing the plugin API name; this is the name of the entry point declaring the plugin client module (``oscplugin = ...`` in the example above) and the group name for - the plugin commands (``openstack.oscplugin.v1 =`` in the example below) + the plugin commands (``openstack.oscplugin.v1 =`` in the example below). + OSC reserves the following API names: ``compute``, ``identity``, + ``image``, ``network``, ``object_store`` and ``volume``. * ``API_VERSION_OPTION`` (optional) - If set, the name of the API version attribute; this must be a valid Python identifier and match the destination set in ``build_option_parser()``. @@ -85,6 +95,9 @@ so the version should not contain the leading 'v' character. .. code-block:: python + from openstackclient.common import utils + + DEFAULT_API_VERSION = '1' # Required by the OSC plugin interface @@ -130,6 +143,60 @@ so the version should not contain the leading 'v' character. ' (Env: OS_OSCPLUGIN_API_VERSION)') return parser +Client usage of OSC interfaces +------------------------------ + +OSC provides the following interfaces that may be used to implement +the plugin commands: + +.. code-block:: python + + # OSC common interfaces available to plugins: + from openstackclient.common import command + from openstackclient.common import exceptions + from openstackclient.common import parseractions + from openstackclient.common import logs + from openstackclient.common import utils + + + class DeleteMypluginobject(command.Command): + """Delete mypluginobject""" + + ... + + def take_action(self, parsed_args): + # Client manager interfaces are availble to plugins. + # This includes the OSC clients created. + client_manager = self.app.client_manager + + ... + + return + +OSC provides the following interfaces that may be used to implement +unit tests for the plugin commands: + +.. code-block:: python + + # OSC unit test interfaces available to plugins: + from openstackclient.tests import fakes + from openstackclient.tests import utils + + ... + +Requirements +------------ + +OSC must be included in ``requirements.txt`` or ``test-requirements.txt`` +for the plugin project. Update ``requirements.txt`` if the plugin project +considers the CLI a required feature. Update ``test-requirements.txt`` if +the plugin project can be installed as a library with the CLI being an +optional feature (available when OSC is also installed). + +.. code-block:: ini + + python-openstackclient>=X.Y.Z # Apache-2.0 + Checklist for adding new OpenStack plugins ========================================== diff --git a/doc/source/specs/ip-availability.rst b/doc/source/specs/ip-availability.rst new file mode 100644 index 0000000..cf0c71f --- /dev/null +++ b/doc/source/specs/ip-availability.rst @@ -0,0 +1,61 @@ +=============== +ip availability +=============== + +Network v2 + +ip availability list +-------------------- + +List IP availability for network + +This command retrieves information about IP availability. +Useful for admins who need a quick way to check the +IP availability for all associated networks. +List specifically returns total IP capacity and the +number of allocated IP addresses from that pool. + +.. program:: ip availability list +.. code:: bash + + os ip availability list + [--ip-version {4,6}] + [--project <project>] + +.. option:: --ip-version {4,6} + + List IP availability for specific version + (Default is 4) + +.. option:: --project <project> + + List IP availability for specific project + (name or ID) + +ip availability show +-------------------- + +Show network IP availability details + +This command retrieves information about IP availability. +Useful for admins who need a quick way to +check the IP availability and details for a +specific network. + +This command will return information about +IP availability for the network as a whole, and +return availability information for each individual +subnet within the network as well. + + +.. program:: ip availability show +.. code:: bash + + os ip availability show + <network> + +.. _ip_availability_show-network +.. describe:: <network> + + Show network IP availability for specific + network (name or ID) diff --git a/functional/common/exceptions.py b/functional/common/exceptions.py deleted file mode 100644 index 47c6071..0000000 --- a/functional/common/exceptions.py +++ /dev/null @@ -1,26 +0,0 @@ -# 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. - - -class CommandFailed(Exception): - def __init__(self, returncode, cmd, output, stderr): - super(CommandFailed, self).__init__() - self.returncode = returncode - self.cmd = cmd - self.stdout = output - self.stderr = stderr - - def __str__(self): - return ("Command '%s' returned non-zero exit status %d.\n" - "stdout:\n%s\n" - "stderr:\n%s" % (self.cmd, self.returncode, - self.stdout, self.stderr)) diff --git a/functional/common/test.py b/functional/common/test.py index 1e767af..4361561 100644 --- a/functional/common/test.py +++ b/functional/common/test.py @@ -17,8 +17,9 @@ import subprocess import testtools import six +from tempest.lib.cli import output_parser +from tempest.lib import exceptions -from functional.common import exceptions COMMON_DIR = os.path.dirname(os.path.abspath(__file__)) FUNCTIONAL_DIR = os.path.normpath(os.path.join(COMMON_DIR, '..')) @@ -76,13 +77,6 @@ class TestCase(testtools.TestCase): if expected not in actual: raise Exception(expected + ' not in ' + actual) - @classmethod - def cleanup_tmpfile(cls, filename): - try: - os.remove(filename) - except OSError: - pass - def assert_table_structure(self, items, field_names): """Verify that all items have keys listed in field_names.""" for item in items: @@ -119,7 +113,7 @@ class TestCase(testtools.TestCase): """Return list of dicts with item values parsed from cli output.""" items = [] - table_ = self.table(raw_output) + table_ = output_parser.table(raw_output) for row in table_['values']: item = {} item[row[0]] = row[1] @@ -128,60 +122,4 @@ class TestCase(testtools.TestCase): def parse_listing(self, raw_output): """Return list of dicts with basic item parsed from cli output.""" - - items = [] - table_ = self.table(raw_output) - for row in table_['values']: - item = {} - for col_idx, col_key in enumerate(table_['headers']): - item[col_key] = row[col_idx] - items.append(item) - return items - - def table(self, output_lines): - """Parse single table from cli output. - - Return dict with list of column names in 'headers' key and - rows in 'values' key. - """ - table_ = {'headers': [], 'values': []} - columns = None - - if not isinstance(output_lines, list): - output_lines = output_lines.split('\n') - - if not output_lines[-1]: - # skip last line if empty (just newline at the end) - output_lines = output_lines[:-1] - - for line in output_lines: - if self.delimiter_line.match(line): - columns = self._table_columns(line) - continue - if '|' not in line: - continue - row = [] - for col in columns: - row.append(line[col[0]:col[1]].strip()) - if table_['headers']: - table_['values'].append(row) - else: - table_['headers'] = row - - return table_ - - def _table_columns(self, first_table_row): - """Find column ranges in output line. - - Return list of tuples (start,end) for each column - detected by plus (+) characters in delimiter line. - """ - positions = [] - start = 1 # there is '+' at 0 - while start < len(first_table_row): - end = first_table_row.find('+', start) - if end == -1: - break - positions.append((start, end)) - start = end + 1 - return positions + return output_parser.listing(raw_output) diff --git a/functional/tests/common/test_help.py b/functional/tests/common/test_help.py new file mode 100644 index 0000000..fcce5f9 --- /dev/null +++ b/functional/tests/common/test_help.py @@ -0,0 +1,65 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from functional.common import test + + +class HelpTests(test.TestCase): + """Functional tests for openstackclient help output.""" + + SERVER_COMMANDS = [ + ('server add security group', 'Add security group to server'), + ('server add volume', 'Add volume to server'), + ('server create', 'Create a new server'), + ('server delete', 'Delete server(s)'), + ('server dump create', 'Create a dump file in server(s)'), + ('server image create', + 'Create a new disk image from a running server'), + ('server list', 'List servers'), + ('server lock', + 'Lock server(s). ' + 'A non-admin user will not be able to execute actions'), + ('server migrate', 'Migrate server to different host'), + ('server pause', 'Pause server(s)'), + ('server reboot', 'Perform a hard or soft server reboot'), + ('server rebuild', 'Rebuild server'), + ('server remove security group', 'Remove security group from server'), + ('server remove volume', 'Remove volume from server'), + ('server rescue', 'Put server in rescue mode'), + ('server resize', 'Scale server to a new flavor'), + ('server resume', 'Resume server(s)'), + ('server set', 'Set server properties'), + ('server shelve', 'Shelve server(s)'), + ('server show', 'Show server details'), + ('server ssh', 'SSH to server'), + ('server start', 'Start server(s).'), + ('server stop', 'Stop server(s).'), + ('server suspend', 'Suspend server(s)'), + ('server unlock', 'Unlock server(s)'), + ('server unpause', 'Unpause server(s)'), + ('server unrescue', 'Restore server from rescue mode'), + ('server unset', 'Unset server properties'), + ('server unshelve', 'Unshelve server(s)') + ] + + def test_server_commands_main_help(self): + """Check server commands in main help message.""" + raw_output = self.openstack('help') + for command, description in self.SERVER_COMMANDS: + self.assertIn(command, raw_output) + self.assertIn(description, raw_output) + + def test_server_only_help(self): + """Check list of server-related commands only.""" + raw_output = self.openstack('help server') + for command in [row[0] for row in self.SERVER_COMMANDS]: + self.assertIn(command, raw_output) diff --git a/functional/tests/common/test_quota.py b/functional/tests/common/test_quota.py index 7a0cb64..0bc93db 100644 --- a/functional/tests/common/test_quota.py +++ b/functional/tests/common/test_quota.py @@ -16,7 +16,7 @@ from functional.common import test class QuotaTests(test.TestCase): """Functional tests for quota. """ # Test quota information for compute, network and volume. - EXPECTED_FIELDS = ['instances', 'network', 'volumes'] + EXPECTED_FIELDS = ['instances', 'networks', 'volumes'] PROJECT_NAME = None @classmethod @@ -25,14 +25,18 @@ class QuotaTests(test.TestCase): cls.get_openstack_configuration_value('auth.project_name') def test_quota_set(self): - # TODO(rtheis): Add --network option once supported on set. - self.openstack('quota set --instances 11 --volumes 11 ' + - self.PROJECT_NAME) + self.openstack('quota set --instances 11 --volumes 11 --networks 11 ' + + self.PROJECT_NAME) opts = self.get_show_opts(self.EXPECTED_FIELDS) raw_output = self.openstack('quota show ' + self.PROJECT_NAME + opts) - self.assertEqual("11\n10\n11\n", raw_output) + self.assertEqual("11\n11\n11\n", raw_output) def test_quota_show(self): raw_output = self.openstack('quota show ' + self.PROJECT_NAME) for expected_field in self.EXPECTED_FIELDS: - self.assertInOutput(expected_field, raw_output) + self.assertIn(expected_field, raw_output) + + 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) diff --git a/functional/tests/compute/v2/test_keypair.py b/functional/tests/compute/v2/test_keypair.py index 1c6e1b1..6bc5cdb 100644 --- a/functional/tests/compute/v2/test_keypair.py +++ b/functional/tests/compute/v2/test_keypair.py @@ -10,34 +10,159 @@ # License for the specific language governing permissions and limitations # under the License. -import uuid +import tempfile from functional.common import test +from tempest.lib.common.utils import data_utils +from tempest.lib import exceptions -class KeypairTests(test.TestCase): - """Functional tests for compute keypairs. """ - NAME = uuid.uuid4().hex - HEADERS = ['Name'] - FIELDS = ['name'] - @classmethod - def setUpClass(cls): - private_key = cls.openstack('keypair create ' + cls.NAME) - cls.assertInOutput('-----BEGIN RSA PRIVATE KEY-----', private_key) - cls.assertInOutput('-----END RSA PRIVATE KEY-----', private_key) +class KeypairBase(test.TestCase): + """Methods for functional tests.""" - @classmethod - def tearDownClass(cls): - raw_output = cls.openstack('keypair delete ' + cls.NAME) - cls.assertOutput('', raw_output) + def keypair_create(self, name=data_utils.rand_uuid()): + """Create keypair and add cleanup.""" + raw_output = self.openstack('keypair create ' + name) + self.addCleanup(self.keypair_delete, name, True) + if not raw_output: + self.fail('Keypair has not been created!') + + def keypair_list(self, params=''): + """Return dictionary with list of keypairs.""" + raw_output = self.openstack('keypair list') + keypairs = self.parse_show_as_object(raw_output) + return keypairs + + def keypair_delete(self, name, ignore_exceptions=False): + """Try to delete keypair by name.""" + try: + self.openstack('keypair delete ' + name) + except exceptions.CommandFailed: + if not ignore_exceptions: + raise + + +class KeypairTests(KeypairBase): + """Functional tests for compute keypairs.""" + + PUBLIC_KEY = ( + 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDWNGczJxNaFUrJJVhta4dWsZY6bU' + '5HUMPbyfSMu713ca3mYtG848W4dfDCB98KmSQx2Bl0D6Q2nrOszOXEQWAXNdfMadnW' + 'c4mNwhZcPBVohIFoC1KZJC8kcBTvFZcoz3mdIijxJtywZNpGNh34VRJlZeHyYjg8/D' + 'esHzdoBVd5c/4R36emQSIV9ukY6PHeZ3scAH4B3K9PxItJBwiFtouSRphQG0bJgOv/' + 'gjAjMElAvg5oku98cb4QiHZ8T8WY68id804raHR6pJxpVVJN4TYJmlUs+NOVM+pPKb' + 'KJttqrIBTkawGK9pLHNfn7z6v1syvUo/4enc1l0Q/Qn2kWiz67 fake@openstack' + ) + + def setUp(self): + """Create keypair with randomized name for tests.""" + super(KeypairTests, self).setUp() + self.KPName = data_utils.rand_name('TestKeyPair') + self.keypair = self.keypair_create(self.KPName) + + def test_keypair_create_duplicate(self): + """Try to create duplicate name keypair. + + Test steps: + 1) Create keypair in setUp + 2) Try to create duplicate keypair with the same name + """ + self.assertRaises(exceptions.CommandFailed, + self.openstack, 'keypair create ' + self.KPName) + + def test_keypair_create_noname(self): + """Try to create keypair without name. + + Test steps: + 1) Try to create keypair without a name + """ + self.assertRaises(exceptions.CommandFailed, + self.openstack, 'keypair create') + + def test_keypair_create_public_key(self): + """Test for create keypair with --public-key option. + + Test steps: + 1) Create keypair with given public key + 2) Delete keypair + """ + with tempfile.NamedTemporaryFile() as f: + f.write(self.PUBLIC_KEY) + f.flush() + + raw_output = self.openstack( + 'keypair create --public-key %s tmpkey' % f.name, + ) + self.addCleanup( + self.openstack, + 'keypair delete tmpkey', + ) + self.assertIn('tmpkey', raw_output) + + def test_keypair_create(self): + """Test keypair create command. + + Test steps: + 1) Create keypair in setUp + 2) Check RSA private key in output + 3) Check for new keypair in keypairs list + """ + NewName = data_utils.rand_name('TestKeyPairCreated') + raw_output = self.openstack('keypair create ' + NewName) + self.addCleanup(self.openstack, 'keypair delete ' + NewName) + self.assertInOutput('-----BEGIN RSA PRIVATE KEY-----', raw_output) + self.assertRegex(raw_output, "[0-9A-Za-z+/]+[=]{0,3}\n") + self.assertInOutput('-----END RSA PRIVATE KEY-----', raw_output) + self.assertIn(NewName, self.keypair_list()) + + def test_keypair_delete_not_existing(self): + """Try to delete keypair with not existing name. + + Test steps: + 1) Create keypair in setUp + 2) Try to delete not existing keypair + """ + self.assertRaises(exceptions.CommandFailed, + self.openstack, 'keypair delete not_existing') + + def test_keypair_delete(self): + """Test keypair delete command. + + Test steps: + 1) Create keypair in setUp + 2) Delete keypair + 3) Check that keypair not in keypairs list + """ + self.openstack('keypair delete ' + self.KPName) + self.assertNotIn(self.KPName, self.keypair_list()) def test_keypair_list(self): - opts = self.get_list_opts(self.HEADERS) - raw_output = self.openstack('keypair list' + opts) - self.assertIn(self.NAME, raw_output) + """Test keypair list command. + + Test steps: + 1) Create keypair in setUp + 2) List keypairs + 3) Check output table structure + 4) Check keypair name in output + """ + HEADERS = ['Name', 'Fingerprint'] + raw_output = self.openstack('keypair list') + items = self.parse_listing(raw_output) + self.assert_table_structure(items, HEADERS) + self.assertIn(self.KPName, raw_output) def test_keypair_show(self): - opts = self.get_show_opts(self.FIELDS) - raw_output = self.openstack('keypair show ' + self.NAME + opts) - self.assertEqual(self.NAME + "\n", raw_output) + """Test keypair show command. + + Test steps: + 1) Create keypair in setUp + 2) Show keypair + 3) Check output table structure + 4) Check keypair name in output + """ + HEADERS = ['Field', 'Value'] + raw_output = self.openstack('keypair show ' + self.KPName) + items = self.parse_listing(raw_output) + self.assert_table_structure(items, HEADERS) + self.assertInOutput(self.KPName, raw_output) diff --git a/functional/tests/compute/v2/test_server.py b/functional/tests/compute/v2/test_server.py index 6c09a42..6cb82cb 100644 --- a/functional/tests/compute/v2/test_server.py +++ b/functional/tests/compute/v2/test_server.py @@ -11,33 +11,28 @@ # under the License. import time -import uuid -import testtools +from tempest.lib.common.utils import data_utils -from functional.common import exceptions from functional.common import test +from tempest.lib import exceptions class ServerTests(test.TestCase): - """Functional tests for server. """ - - NAME = uuid.uuid4().hex - OTHER_NAME = uuid.uuid4().hex - HEADERS = ['"Name"'] - FIELDS = ['name'] - IP_POOL = 'public' + """Functional tests for openstack server commands.""" @classmethod def get_flavor(cls): - raw_output = cls.openstack('flavor list -f value -c ID') - ray = raw_output.split('\n') - idx = int(len(ray) / 2) - return ray[idx] + # NOTE(rtheis): Get m1.tiny flavor since functional tests may + # create other flavors. + raw_output = cls.openstack('flavor show m1.tiny -c id -f value') + return raw_output.strip('\n') @classmethod def get_image(cls): - raw_output = cls.openstack('image list -f value -c ID') + # NOTE(rtheis): Get public images since functional tests may + # create private images. + raw_output = cls.openstack('image list --public -f value -c ID') ray = raw_output.split('\n') idx = int(len(ray) / 2) return ray[idx] @@ -45,116 +40,210 @@ class ServerTests(test.TestCase): @classmethod def get_network(cls): try: - raw_output = cls.openstack('network list -f value -c ID') + # NOTE(rtheis): Get private network since functional tests may + # create other networks. + raw_output = cls.openstack('network show private -c id -f value') except exceptions.CommandFailed: return '' - ray = raw_output.split('\n') - idx = int(len(ray) / 2) - return ' --nic net-id=' + ray[idx] + return ' --nic net-id=' + raw_output.strip('\n') - @classmethod - def setUpClass(cls): - opts = cls.get_show_opts(cls.FIELDS) - flavor = cls.get_flavor() - image = cls.get_image() - network = cls.get_network() - raw_output = cls.openstack('server create --flavor ' + flavor + - ' --image ' + image + network + ' ' + - cls.NAME + opts) - expected = cls.NAME + '\n' - cls.assertOutput(expected, raw_output) + def server_create(self, name=None): + """Create server. Add cleanup.""" + name = name or data_utils.rand_uuid() + opts = self.get_show_opts(self.FIELDS) + flavor = self.get_flavor() + image = self.get_image() + network = self.get_network() + raw_output = self.openstack('--debug server create --flavor ' + + flavor + + ' --image ' + image + network + ' ' + + name + opts) + if not raw_output: + self.fail('Server has not been created!') + self.addCleanup(self.server_delete, name) - @classmethod - def tearDownClass(cls): - # Rename test - raw_output = cls.openstack('server set --name ' + cls.OTHER_NAME + - ' ' + cls.NAME) - cls.assertOutput("", raw_output) - # Delete test - raw_output = cls.openstack('server delete ' + cls.OTHER_NAME) - cls.assertOutput('', raw_output) + def server_list(self, params=[]): + """List servers.""" + opts = self.get_list_opts(params) + return self.openstack('server list' + opts) + + def server_delete(self, name): + """Delete server by name.""" + self.openstack('server delete ' + name) + + def setUp(self): + """Set necessary variables and create server.""" + super(ServerTests, self).setUp() + self.NAME = data_utils.rand_name('TestServer') + self.OTHER_NAME = data_utils.rand_name('TestServer') + self.HEADERS = ['"Name"'] + self.FIELDS = ['name'] + self.IP_POOL = 'public' + self.server_create(self.NAME) + + def test_server_rename(self): + """Test server rename command. + + Test steps: + 1) Boot server in setUp + 2) Rename server + 3) Check output + 4) Rename server back to original name + """ + raw_output = self.openstack('server set --name ' + self.OTHER_NAME + + ' ' + self.NAME) + self.assertOutput("", raw_output) + self.assertNotIn(self.NAME, self.server_list(['Name'])) + self.assertIn(self.OTHER_NAME, self.server_list(['Name'])) + self.openstack('server set --name ' + self.NAME + ' ' + + self.OTHER_NAME) def test_server_list(self): + """Test server list command. + + Test steps: + 1) Boot server in setUp + 2) List servers + 3) Check output + """ opts = self.get_list_opts(self.HEADERS) raw_output = self.openstack('server list' + opts) self.assertIn(self.NAME, raw_output) def test_server_show(self): + """Test server show command. + + Test steps: + 1) Boot server in setUp + 2) Show server + 3) Check output + """ opts = self.get_show_opts(self.FIELDS) raw_output = self.openstack('server show ' + self.NAME + opts) self.assertEqual(self.NAME + "\n", raw_output) - def wait_for(self, desired, wait=120, interval=5, failures=['ERROR']): - # TODO(thowe): Add a server wait command to osc - status = "notset" - wait = 120 - interval = 5 - total_sleep = 0 - opts = self.get_show_opts(['status']) - while total_sleep < wait: - status = self.openstack('server show ' + self.NAME + opts) - status = status.rstrip() - print('Waiting for {} current status: {}'.format(desired, status)) - if status == desired: - break - self.assertNotIn(status, failures) - time.sleep(interval) - total_sleep += interval - self.assertEqual(desired, status) + def test_server_metadata(self): + """Test command to set server metadata. - @testtools.skip('skipping due to bug 1483422') - def test_server_up_test(self): - self.wait_for("ACTIVE") - # give it a little bit more time - time.sleep(5) + Test steps: + 1) Boot server in setUp + 2) Set properties for server + 3) Check server properties in server show output + """ + self.wait_for_status("ACTIVE") # metadata raw_output = self.openstack( 'server set --property a=b --property c=d ' + self.NAME) opts = self.get_show_opts(["name", "properties"]) raw_output = self.openstack('server show ' + self.NAME + opts) self.assertEqual(self.NAME + "\na='b', c='d'\n", raw_output) + + def test_server_suspend_resume(self): + """Test server suspend and resume commands. + + Test steps: + 1) Boot server in setUp + 2) Suspend server + 3) Check for SUSPENDED server status + 4) Resume server + 5) Check for ACTIVE server status + """ + self.wait_for_status("ACTIVE") # suspend raw_output = self.openstack('server suspend ' + self.NAME) self.assertEqual("", raw_output) - self.wait_for("SUSPENDED") + self.wait_for_status("SUSPENDED") # resume raw_output = self.openstack('server resume ' + self.NAME) self.assertEqual("", raw_output) - self.wait_for("ACTIVE") + self.wait_for_status("ACTIVE") + + def test_server_lock_unlock(self): + """Test server lock and unlock commands. + + Test steps: + 1) Boot server in setUp + 2) Lock server + 3) Check output + 4) Unlock server + 5) Check output + """ + self.wait_for_status("ACTIVE") # lock raw_output = self.openstack('server lock ' + self.NAME) self.assertEqual("", raw_output) # unlock raw_output = self.openstack('server unlock ' + self.NAME) self.assertEqual("", raw_output) + + def test_server_pause_unpause(self): + """Test server pause and unpause commands. + + Test steps: + 1) Boot server in setUp + 2) Pause server + 3) Check for PAUSED server status + 4) Unpause server + 5) Check for ACTIVE server status + """ + self.wait_for_status("ACTIVE") # pause raw_output = self.openstack('server pause ' + self.NAME) self.assertEqual("", raw_output) - self.wait_for("PAUSED") + self.wait_for_status("PAUSED") # unpause raw_output = self.openstack('server unpause ' + self.NAME) self.assertEqual("", raw_output) - self.wait_for("ACTIVE") + self.wait_for_status("ACTIVE") + + def test_server_rescue_unrescue(self): + """Test server rescue and unrescue commands. + + Test steps: + 1) Boot server in setUp + 2) Rescue server + 3) Check for RESCUE server status + 4) Unrescue server + 5) Check for ACTIVE server status + """ + self.wait_for_status("ACTIVE") # rescue opts = self.get_show_opts(["adminPass"]) raw_output = self.openstack('server rescue ' + self.NAME + opts) self.assertNotEqual("", raw_output) - self.wait_for("RESCUE") + self.wait_for_status("RESCUE") # unrescue raw_output = self.openstack('server unrescue ' + self.NAME) self.assertEqual("", raw_output) - self.wait_for("ACTIVE") + self.wait_for_status("ACTIVE") + + def test_server_attach_detach_floating_ip(self): + """Test commands to attach and detach floating IP for server. + + Test steps: + 1) Boot server in setUp + 2) Create floating IP + 3) Add floating IP to server + 4) Check for floating IP in server show output + 5) Remove floating IP from server + 6) Check that floating IP is not in server show output + 7) Delete floating IP + 8) Check output + """ + self.wait_for_status("ACTIVE") # attach ip - opts = self.get_show_opts(["id", "ip"]) - raw_output = self.openstack('ip floating create ' + self.IP_POOL + + opts = self.get_show_opts(["id", "floating_ip_address"]) + raw_output = self.openstack('ip floating create ' + + self.IP_POOL + opts) - ipid, ip, rol = tuple(raw_output.split('\n')) + ip, ipid, rol = tuple(raw_output.split('\n')) self.assertNotEqual("", ipid) self.assertNotEqual("", ip) raw_output = self.openstack('ip floating add ' + ip + ' ' + self.NAME) self.assertEqual("", raw_output) raw_output = self.openstack('server show ' + self.NAME) self.assertIn(ip, raw_output) + # detach ip raw_output = self.openstack('ip floating remove ' + ip + ' ' + self.NAME) @@ -163,7 +252,40 @@ class ServerTests(test.TestCase): self.assertNotIn(ip, raw_output) raw_output = self.openstack('ip floating delete ' + ipid) self.assertEqual("", raw_output) + + def test_server_reboot(self): + """Test server reboot command. + + Test steps: + 1) Boot server in setUp + 2) Reboot server + 3) Check for ACTIVE server status + """ + self.wait_for_status("ACTIVE") # reboot raw_output = self.openstack('server reboot ' + self.NAME) self.assertEqual("", raw_output) - self.wait_for("ACTIVE") + self.wait_for_status("ACTIVE") + + def wait_for_status(self, expected_status='ACTIVE', wait=900, interval=30): + """Wait until server reaches expected status.""" + # TODO(thowe): Add a server wait command to osc + failures = ['ERROR'] + total_sleep = 0 + opts = self.get_show_opts(['status']) + while total_sleep < wait: + status = self.openstack('server show ' + self.NAME + opts) + status = status.rstrip() + print('Waiting for {} current status: {}'.format(expected_status, + status)) + if status == expected_status: + break + self.assertNotIn(status, failures) + time.sleep(interval) + total_sleep += interval + + status = self.openstack('server show ' + self.NAME + opts) + status = status.rstrip() + self.assertEqual(status, expected_status) + # give it a little bit more time + time.sleep(5) diff --git a/functional/tests/compute/v2/test_server_group.py b/functional/tests/compute/v2/test_server_group.py new file mode 100644 index 0000000..ce4f97a --- /dev/null +++ b/functional/tests/compute/v2/test_server_group.py @@ -0,0 +1,46 @@ +# 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 functional.common import test + + +class ServerGroupTests(test.TestCase): + """Functional tests for servergroup. """ + + NAME = uuid.uuid4().hex + HEADERS = ['Name'] + FIELDS = ['name'] + + @classmethod + def setUpClass(cls): + opts = cls.get_show_opts(cls.FIELDS) + raw_output = cls.openstack('server group create --policy affinity ' + + cls.NAME + opts) + expected = cls.NAME + '\n' + cls.assertOutput(expected, raw_output) + + @classmethod + def tearDownClass(cls): + raw_output = cls.openstack('server group delete ' + cls.NAME) + cls.assertOutput('', raw_output) + + def test_server_group_list(self): + opts = self.get_list_opts(self.HEADERS) + raw_output = self.openstack('server group list' + opts) + self.assertIn(self.NAME, raw_output) + + def test_server_group_show(self): + opts = self.get_show_opts(self.FIELDS) + raw_output = self.openstack('server group show ' + self.NAME + opts) + self.assertEqual(self.NAME + "\n", raw_output) diff --git a/functional/tests/identity/v2/test_endpoint.py b/functional/tests/identity/v2/test_endpoint.py index 0aed322..8064365 100644 --- a/functional/tests/identity/v2/test_endpoint.py +++ b/functional/tests/identity/v2/test_endpoint.py @@ -27,7 +27,7 @@ class EndpointTests(test_identity.IdentityTests): def test_endpoint_list(self): endpoint_id = self._create_dummy_endpoint() raw_output = self.openstack('endpoint list') - self.assertInOutput(endpoint_id, raw_output) + self.assertIn(endpoint_id, raw_output) items = self.parse_listing(raw_output) self.assert_table_structure(items, self.ENDPOINT_LIST_HEADERS) diff --git a/functional/tests/identity/v2/test_identity.py b/functional/tests/identity/v2/test_identity.py index 91a77b6..9adbe49 100644 --- a/functional/tests/identity/v2/test_identity.py +++ b/functional/tests/identity/v2/test_identity.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -from tempest_lib.common.utils import data_utils +from tempest.lib.common.utils import data_utils from functional.common import test @@ -67,13 +67,13 @@ class IdentityTests(test.TestCase): '--description %(description)s ' '--enable %(name)s' % {'description': project_description, 'name': project_name}) - items = self.parse_show(raw_output) - self.assert_show_fields(items, self.PROJECT_FIELDS) project = self.parse_show_as_object(raw_output) if add_clean_up: self.addCleanup( self.openstack, 'project delete %s' % project['id']) + items = self.parse_show(raw_output) + self.assert_show_fields(items, self.PROJECT_FIELDS) return project_name def _create_dummy_user(self, add_clean_up=True): @@ -90,47 +90,47 @@ class IdentityTests(test.TestCase): 'email': email, 'password': password, 'name': username}) - items = self.parse_show(raw_output) - self.assert_show_fields(items, self.USER_FIELDS) if add_clean_up: self.addCleanup( self.openstack, 'user delete %s' % self.parse_show_as_object(raw_output)['id']) + items = self.parse_show(raw_output) + self.assert_show_fields(items, self.USER_FIELDS) return username def _create_dummy_role(self, add_clean_up=True): role_name = data_utils.rand_name('TestRole') raw_output = self.openstack('role create %s' % role_name) - items = self.parse_show(raw_output) - self.assert_show_fields(items, self.ROLE_FIELDS) role = self.parse_show_as_object(raw_output) - self.assertEqual(role_name, role['name']) if add_clean_up: self.addCleanup( self.openstack, 'role delete %s' % role['id']) + items = self.parse_show(raw_output) + self.assert_show_fields(items, self.ROLE_FIELDS) + self.assertEqual(role_name, role['name']) return role_name def _create_dummy_ec2_credentials(self, add_clean_up=True): raw_output = self.openstack('ec2 credentials create') - items = self.parse_show(raw_output) - self.assert_show_fields(items, self.EC2_CREDENTIALS_FIELDS) ec2_credentials = self.parse_show_as_object(raw_output) access_key = ec2_credentials['access'] if add_clean_up: self.addCleanup( self.openstack, 'ec2 credentials delete %s' % access_key) + items = self.parse_show(raw_output) + self.assert_show_fields(items, self.EC2_CREDENTIALS_FIELDS) return access_key def _create_dummy_token(self, add_clean_up=True): raw_output = self.openstack('token issue') - items = self.parse_show(raw_output) - self.assert_show_fields(items, self.TOKEN_FIELDS) token = self.parse_show_as_object(raw_output) if add_clean_up: self.addCleanup(self.openstack, 'token revoke %s' % token['id']) + items = self.parse_show(raw_output) + self.assert_show_fields(items, self.TOKEN_FIELDS) return token['id'] def _create_dummy_service(self, add_clean_up=True): @@ -144,12 +144,12 @@ class IdentityTests(test.TestCase): '%(type)s' % {'name': service_name, 'description': description, 'type': type_name}) - items = self.parse_show(raw_output) - self.assert_show_fields(items, self.SERVICE_FIELDS) if add_clean_up: service = self.parse_show_as_object(raw_output) self.addCleanup(self.openstack, 'service delete %s' % service['id']) + items = self.parse_show(raw_output) + self.assert_show_fields(items, self.SERVICE_FIELDS) return service_name def _create_dummy_endpoint(self, add_clean_up=True): @@ -169,11 +169,11 @@ class IdentityTests(test.TestCase): 'internalurl': internal_url, 'region': region_id, 'service': service_name}) - items = self.parse_show(raw_output) - self.assert_show_fields(items, self.ENDPOINT_FIELDS) endpoint = self.parse_show_as_object(raw_output) if add_clean_up: self.addCleanup( self.openstack, 'endpoint delete %s' % endpoint['id']) + items = self.parse_show(raw_output) + self.assert_show_fields(items, self.ENDPOINT_FIELDS) return endpoint['id'] diff --git a/functional/tests/identity/v2/test_project.py b/functional/tests/identity/v2/test_project.py index 3a5e8e8..e9580ec 100644 --- a/functional/tests/identity/v2/test_project.py +++ b/functional/tests/identity/v2/test_project.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -from tempest_lib.common.utils import data_utils +from tempest.lib.common.utils import data_utils from functional.tests.identity.v2 import test_identity diff --git a/functional/tests/identity/v2/test_role.py b/functional/tests/identity/v2/test_role.py index e542a5f..9ee6006 100644 --- a/functional/tests/identity/v2/test_role.py +++ b/functional/tests/identity/v2/test_role.py @@ -40,8 +40,17 @@ class RoleTests(test_identity.IdentityTests): '%(role)s' % {'project': project_name, 'user': username, 'role': role_name}) + self.addCleanup( + self.openstack, + 'role remove ' + '--project %(project)s ' + '--user %(user)s ' + '%(role)s' % {'project': project_name, + 'user': username, + 'role': role_name}) items = self.parse_show(raw_output) self.assert_show_fields(items, self.ROLE_FIELDS) + raw_output = self.openstack( 'role list ' '--project %(project)s ' @@ -51,14 +60,6 @@ class RoleTests(test_identity.IdentityTests): items = self.parse_listing(raw_output) self.assert_table_structure(items, test_identity.BASIC_LIST_HEADERS) self.assertEqual(1, len(items)) - self.addCleanup( - self.openstack, - 'role remove ' - '--project %(project)s ' - '--user %(user)s ' - '%(role)s' % {'project': project_name, - 'user': username, - 'role': role_name}) def test_role_show(self): role_name = self._create_dummy_role() @@ -76,8 +77,6 @@ class RoleTests(test_identity.IdentityTests): '%(role)s' % {'project': self.project_name, 'user': username, 'role': role_name}) - items = self.parse_show(raw_output) - self.assert_show_fields(items, self.ROLE_FIELDS) self.addCleanup( self.openstack, 'role remove ' @@ -86,24 +85,26 @@ class RoleTests(test_identity.IdentityTests): '%(role)s' % {'project': self.project_name, 'user': username, 'role': role_name}) + items = self.parse_show(raw_output) + self.assert_show_fields(items, self.ROLE_FIELDS) def test_role_remove(self): role_name = self._create_dummy_role() username = self._create_dummy_user() - raw_output = self.openstack( + add_raw_output = self.openstack( 'role add ' '--project %(project)s ' '--user %(user)s ' '%(role)s' % {'project': self.project_name, 'user': username, 'role': role_name}) - items = self.parse_show(raw_output) - self.assert_show_fields(items, self.ROLE_FIELDS) - raw_output = self.openstack( + del_raw_output = self.openstack( 'role remove ' '--project %(project)s ' '--user %(user)s ' '%(role)s' % {'project': self.project_name, 'user': username, 'role': role_name}) - self.assertEqual(0, len(raw_output)) + items = self.parse_show(add_raw_output) + self.assert_show_fields(items, self.ROLE_FIELDS) + self.assertEqual(0, len(del_raw_output)) diff --git a/functional/tests/identity/v2/test_user.py b/functional/tests/identity/v2/test_user.py index 41895e7..34cabf7 100644 --- a/functional/tests/identity/v2/test_user.py +++ b/functional/tests/identity/v2/test_user.py @@ -10,9 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. -from tempest_lib.common.utils import data_utils +from tempest.lib.common.utils import data_utils +from tempest.lib import exceptions -from functional.common import exceptions from functional.tests.identity.v2 import test_identity diff --git a/functional/tests/identity/v3/test_domain.py b/functional/tests/identity/v3/test_domain.py index f3ae4e8..0708e42 100644 --- a/functional/tests/identity/v3/test_domain.py +++ b/functional/tests/identity/v3/test_domain.py @@ -10,9 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. -from tempest_lib.common.utils import data_utils +from tempest.lib.common.utils import data_utils +from tempest.lib import exceptions -from functional.common import exceptions from functional.tests.identity.v3 import test_identity @@ -21,13 +21,13 @@ class DomainTests(test_identity.IdentityTests): def test_domain_create(self): domain_name = data_utils.rand_name('TestDomain') raw_output = self.openstack('domain create %s' % domain_name) - items = self.parse_show(raw_output) - self.assert_show_fields(items, self.DOMAIN_FIELDS) # disable domain first before deleting it self.addCleanup(self.openstack, 'domain delete %s' % domain_name) self.addCleanup(self.openstack, 'domain set --disable %s' % domain_name) + items = self.parse_show(raw_output) + self.assert_show_fields(items, self.DOMAIN_FIELDS) def test_domain_list(self): self._create_dummy_domain() diff --git a/functional/tests/identity/v3/test_endpoint.py b/functional/tests/identity/v3/test_endpoint.py index a759078..ec11dea 100644 --- a/functional/tests/identity/v3/test_endpoint.py +++ b/functional/tests/identity/v3/test_endpoint.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -from tempest_lib.common.utils import data_utils +from tempest.lib.common.utils import data_utils from functional.tests.identity.v3 import test_identity @@ -31,7 +31,7 @@ class EndpointTests(test_identity.IdentityTests): def test_endpoint_list(self): endpoint_id = self._create_dummy_endpoint() raw_output = self.openstack('endpoint list') - self.assertInOutput(endpoint_id, raw_output) + self.assertIn(endpoint_id, raw_output) items = self.parse_listing(raw_output) self.assert_table_structure(items, self.ENDPOINT_LIST_HEADERS) diff --git a/functional/tests/identity/v3/test_group.py b/functional/tests/identity/v3/test_group.py index 8e39cd5..3f58864 100644 --- a/functional/tests/identity/v3/test_group.py +++ b/functional/tests/identity/v3/test_group.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -from tempest_lib.common.utils import data_utils +from tempest.lib.common.utils import data_utils from functional.tests.identity.v3 import test_identity @@ -25,7 +25,7 @@ class GroupTests(test_identity.IdentityTests): raw_output = self.openstack('group list') items = self.parse_listing(raw_output) self.assert_table_structure(items, test_identity.BASIC_LIST_HEADERS) - self.assertInOutput(group_name, raw_output) + self.assertIn(group_name, raw_output) def test_group_list_with_domain(self): group_name = self._create_dummy_group() @@ -33,7 +33,7 @@ class GroupTests(test_identity.IdentityTests): 'group list --domain %s' % self.domain_name) items = self.parse_listing(raw_output) self.assert_table_structure(items, test_identity.BASIC_LIST_HEADERS) - self.assertInOutput(group_name, raw_output) + self.assertIn(group_name, raw_output) def test_group_delete(self): group_name = self._create_dummy_group(add_clean_up=False) @@ -93,11 +93,6 @@ class GroupTests(test_identity.IdentityTests): 'user_domain': self.domain_name, 'group': group_name, 'user': username}) - self.assertOutput( - '%(user)s added to group %(group)s\n' % {'user': username, - 'group': group_name}, - raw_output - ) self.addCleanup( self.openstack, 'group remove user ' @@ -107,6 +102,11 @@ class GroupTests(test_identity.IdentityTests): 'user_domain': self.domain_name, 'group': group_name, 'user': username}) + self.assertEqual( + '%(user)s added to group %(group)s\n' % {'user': username, + 'group': group_name}, + raw_output + ) def test_group_contains_user(self): group_name = self._create_dummy_group() @@ -119,7 +119,16 @@ class GroupTests(test_identity.IdentityTests): 'user_domain': self.domain_name, 'group': group_name, 'user': username}) - self.assertOutput( + self.addCleanup( + self.openstack, + 'group remove user ' + '--group-domain %(group_domain)s ' + '--user-domain %(user_domain)s ' + '%(group)s %(user)s' % {'group_domain': self.domain_name, + 'user_domain': self.domain_name, + 'group': group_name, + 'user': username}) + self.assertEqual( '%(user)s added to group %(group)s\n' % {'user': username, 'group': group_name}, raw_output @@ -132,24 +141,15 @@ class GroupTests(test_identity.IdentityTests): 'user_domain': self.domain_name, 'group': group_name, 'user': username}) - self.assertOutput( + self.assertEqual( '%(user)s in group %(group)s\n' % {'user': username, 'group': group_name}, raw_output) - self.addCleanup( - self.openstack, - 'group remove user ' - '--group-domain %(group_domain)s ' - '--user-domain %(user_domain)s ' - '%(group)s %(user)s' % {'group_domain': self.domain_name, - 'user_domain': self.domain_name, - 'group': group_name, - 'user': username}) def test_group_remove_user(self): group_name = self._create_dummy_group() username = self._create_dummy_user() - raw_output = self.openstack( + add_raw_output = self.openstack( 'group add user ' '--group-domain %(group_domain)s ' '--user-domain %(user_domain)s ' @@ -157,12 +157,7 @@ class GroupTests(test_identity.IdentityTests): 'user_domain': self.domain_name, 'group': group_name, 'user': username}) - self.assertOutput( - '%(user)s added to group %(group)s\n' % {'user': username, - 'group': group_name}, - raw_output - ) - raw_output = self.openstack( + remove_raw_output = self.openstack( 'group remove user ' '--group-domain %(group_domain)s ' '--user-domain %(user_domain)s ' @@ -170,9 +165,14 @@ class GroupTests(test_identity.IdentityTests): 'user_domain': self.domain_name, 'group': group_name, 'user': username}) - self.assertOutput( + self.assertEqual( + '%(user)s added to group %(group)s\n' % {'user': username, + 'group': group_name}, + add_raw_output + ) + self.assertEqual( '%(user)s removed from ' 'group %(group)s\n' % {'user': username, 'group': group_name}, - raw_output + remove_raw_output ) diff --git a/functional/tests/identity/v3/test_identity.py b/functional/tests/identity/v3/test_identity.py index 88f5196..3f98874 100644 --- a/functional/tests/identity/v3/test_identity.py +++ b/functional/tests/identity/v3/test_identity.py @@ -12,7 +12,7 @@ import os -from tempest_lib.common.utils import data_utils +from tempest.lib.common.utils import data_utils from functional.common import test @@ -44,6 +44,11 @@ class IdentityTests(test.TestCase): IDENTITY_PROVIDER_FIELDS = ['description', 'enabled', 'id', 'remote_ids'] IDENTITY_PROVIDER_LIST_HEADERS = ['ID', 'Enabled', 'Description'] + SERVICE_PROVIDER_FIELDS = ['auth_url', 'description', 'enabled', + 'id', 'relay_state_prefix', 'sp_url'] + SERVICE_PROVIDER_LIST_HEADERS = ['ID', 'Enabled', 'Description', + 'Auth URL'] + @classmethod def setUpClass(cls): if hasattr(super(IdentityTests, cls), 'setUpClass'): @@ -110,25 +115,25 @@ class IdentityTests(test.TestCase): 'password': password, 'description': description, 'name': username}) - items = self.parse_show(raw_output) - self.assert_show_fields(items, self.USER_FIELDS) if add_clean_up: self.addCleanup( self.openstack, 'user delete %s' % self.parse_show_as_object(raw_output)['id']) + items = self.parse_show(raw_output) + self.assert_show_fields(items, self.USER_FIELDS) return username def _create_dummy_role(self, add_clean_up=True): role_name = data_utils.rand_name('TestRole') raw_output = self.openstack('role create %s' % role_name) - items = self.parse_show(raw_output) - self.assert_show_fields(items, self.ROLE_FIELDS) role = self.parse_show_as_object(raw_output) - self.assertEqual(role_name, role['name']) if add_clean_up: self.addCleanup( self.openstack, 'role delete %s' % role['id']) + items = self.parse_show(raw_output) + self.assert_show_fields(items, self.ROLE_FIELDS) + self.assertEqual(role_name, role['name']) return role_name def _create_dummy_group(self, add_clean_up=True): @@ -141,8 +146,6 @@ class IdentityTests(test.TestCase): '%(name)s' % {'domain': self.domain_name, 'description': description, 'name': group_name}) - items = self.parse_show(raw_output) - self.assert_show_fields(items, self.GROUP_FIELDS) if add_clean_up: self.addCleanup( self.openstack, @@ -150,6 +153,8 @@ class IdentityTests(test.TestCase): '--domain %(domain)s ' '%(name)s' % {'domain': self.domain_name, 'name': group_name}) + items = self.parse_show(raw_output) + self.assert_show_fields(items, self.GROUP_FIELDS) return group_name def _create_dummy_domain(self, add_clean_up=True): @@ -203,11 +208,11 @@ class IdentityTests(test.TestCase): '%(id)s' % {'parent_region_arg': parent_region_arg, 'description': description, 'id': region_id}) - items = self.parse_show(raw_output) - self.assert_show_fields(items, self.REGION_FIELDS) if add_clean_up: self.addCleanup(self.openstack, 'region delete %s' % region_id) + items = self.parse_show(raw_output) + self.assert_show_fields(items, self.REGION_FIELDS) return region_id def _create_dummy_service(self, add_clean_up=True): @@ -222,12 +227,12 @@ class IdentityTests(test.TestCase): '%(type)s' % {'name': service_name, 'description': description, 'type': type_name}) - items = self.parse_show(raw_output) - self.assert_show_fields(items, self.SERVICE_FIELDS) if add_clean_up: service = self.parse_show_as_object(raw_output) self.addCleanup(self.openstack, 'service delete %s' % service['id']) + items = self.parse_show(raw_output) + self.assert_show_fields(items, self.SERVICE_FIELDS) return service_name def _create_dummy_endpoint(self, interface='public', add_clean_up=True): @@ -244,13 +249,13 @@ class IdentityTests(test.TestCase): 'service': service_name, 'interface': interface, 'url': endpoint_url}) - items = self.parse_show(raw_output) - self.assert_show_fields(items, self.ENDPOINT_FIELDS) endpoint = self.parse_show_as_object(raw_output) if add_clean_up: self.addCleanup( self.openstack, 'endpoint delete %s' % endpoint['id']) + items = self.parse_show(raw_output) + self.assert_show_fields(items, self.ENDPOINT_FIELDS) return endpoint['id'] def _create_dummy_idp(self, add_clean_up=True): @@ -262,10 +267,29 @@ class IdentityTests(test.TestCase): '--description %(description)s ' '--enable ' % {'name': identity_provider, 'description': description}) - items = self.parse_show(raw_output) - self.assert_show_fields(items, self.IDENTITY_PROVIDER_FIELDS) if add_clean_up: self.addCleanup( self.openstack, 'identity provider delete %s' % identity_provider) + items = self.parse_show(raw_output) + self.assert_show_fields(items, self.IDENTITY_PROVIDER_FIELDS) return identity_provider + + def _create_dummy_sp(self, add_clean_up=True): + service_provider = data_utils.rand_name('ServiceProvider') + description = data_utils.rand_name('description') + raw_output = self.openstack( + 'service provider create ' + ' %(name)s ' + '--description %(description)s ' + '--auth-url https://sp.example.com:35357 ' + '--service-provider-url https://sp.example.com:5000 ' + '--enable ' % {'name': service_provider, + 'description': description}) + if add_clean_up: + self.addCleanup( + self.openstack, + 'service provider delete %s' % service_provider) + items = self.parse_show(raw_output) + self.assert_show_fields(items, self.SERVICE_PROVIDER_FIELDS) + return service_provider diff --git a/functional/tests/identity/v3/test_idp.py b/functional/tests/identity/v3/test_idp.py index 3d6739d..08f660f 100644 --- a/functional/tests/identity/v3/test_idp.py +++ b/functional/tests/identity/v3/test_idp.py @@ -11,7 +11,7 @@ # under the License. from functional.tests.identity.v3 import test_identity -from tempest_lib.common.utils import data_utils +from tempest.lib.common.utils import data_utils class IdentityProviderTests(test_identity.IdentityTests): diff --git a/functional/tests/identity/v3/test_project.py b/functional/tests/identity/v3/test_project.py index 204a8d1..d060c7b 100644 --- a/functional/tests/identity/v3/test_project.py +++ b/functional/tests/identity/v3/test_project.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -from tempest_lib.common.utils import data_utils +from tempest.lib.common.utils import data_utils from functional.tests.identity.v3 import test_identity @@ -65,7 +65,7 @@ class ProjectTests(test_identity.IdentityTests): 'project list --domain %s' % self.domain_name) items = self.parse_listing(raw_output) self.assert_table_structure(items, test_identity.BASIC_LIST_HEADERS) - self.assertInOutput(project_name, raw_output) + self.assertIn(project_name, raw_output) self.assertTrue(len(items) > 0) def test_project_set(self): diff --git a/functional/tests/identity/v3/test_role.py b/functional/tests/identity/v3/test_role.py index 7e0cf76..29dc4b2 100644 --- a/functional/tests/identity/v3/test_role.py +++ b/functional/tests/identity/v3/test_role.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -from tempest_lib.common.utils import data_utils +from tempest.lib.common.utils import data_utils from functional.tests.identity.v3 import test_identity @@ -45,6 +45,18 @@ class RoleTests(test_identity.IdentityTests): 'user': username, 'user_domain': self.domain_name, 'role': role_name}) + self.addCleanup( + self.openstack, + 'role remove ' + '--project %(project)s ' + '--project-domain %(project_domain)s ' + '--user %(user)s ' + '--user-domain %(user_domain)s ' + '%(role)s' % {'project': self.project_name, + 'project_domain': self.domain_name, + 'user': username, + 'user_domain': self.domain_name, + 'role': role_name}) self.assertEqual(0, len(raw_output)) raw_output = self.openstack( 'role list ' @@ -59,18 +71,6 @@ class RoleTests(test_identity.IdentityTests): items = self.parse_listing(raw_output) self.assert_table_structure(items, test_identity.BASIC_LIST_HEADERS) self.assertEqual(1, len(items)) - self.addCleanup( - self.openstack, - 'role remove ' - '--project %(project)s ' - '--project-domain %(project_domain)s ' - '--user %(user)s ' - '--user-domain %(user_domain)s ' - '%(role)s' % {'project': self.project_name, - 'project_domain': self.domain_name, - 'user': username, - 'user_domain': self.domain_name, - 'role': role_name}) def test_role_show(self): role_name = self._create_dummy_role() @@ -102,7 +102,6 @@ class RoleTests(test_identity.IdentityTests): 'user': username, 'user_domain': self.domain_name, 'role': role_name}) - self.assertEqual(0, len(raw_output)) self.addCleanup( self.openstack, 'role remove ' @@ -115,11 +114,12 @@ class RoleTests(test_identity.IdentityTests): 'user': username, 'user_domain': self.domain_name, 'role': role_name}) + self.assertEqual(0, len(raw_output)) def test_role_remove(self): role_name = self._create_dummy_role() username = self._create_dummy_user() - raw_output = self.openstack( + add_raw_output = self.openstack( 'role add ' '--project %(project)s ' '--project-domain %(project_domain)s ' @@ -130,8 +130,7 @@ class RoleTests(test_identity.IdentityTests): 'user': username, 'user_domain': self.domain_name, 'role': role_name}) - self.assertEqual(0, len(raw_output)) - raw_output = self.openstack( + remove_raw_output = self.openstack( 'role remove ' '--project %(project)s ' '--project-domain %(project_domain)s ' @@ -142,4 +141,5 @@ class RoleTests(test_identity.IdentityTests): 'user': username, 'user_domain': self.domain_name, 'role': role_name}) - self.assertEqual(0, len(raw_output)) + self.assertEqual(0, len(add_raw_output)) + self.assertEqual(0, len(remove_raw_output)) diff --git a/functional/tests/identity/v3/test_service.py b/functional/tests/identity/v3/test_service.py index 147208a..684fa5f 100644 --- a/functional/tests/identity/v3/test_service.py +++ b/functional/tests/identity/v3/test_service.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -from tempest_lib.common.utils import data_utils +from tempest.lib.common.utils import data_utils from functional.tests.identity.v3 import test_identity diff --git a/functional/tests/identity/v3/test_service_provider.py b/functional/tests/identity/v3/test_service_provider.py new file mode 100644 index 0000000..936c662 --- /dev/null +++ b/functional/tests/identity/v3/test_service_provider.py @@ -0,0 +1,54 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from functional.tests.identity.v3 import test_identity +from tempest.lib.common.utils import data_utils + + +class ServiceProviderTests(test_identity.IdentityTests): + # Introduce functional test cases for command 'Service Provider' + + def test_sp_create(self): + self._create_dummy_sp(add_clean_up=True) + + def test_sp_delete(self): + service_provider = self._create_dummy_sp(add_clean_up=False) + raw_output = self.openstack('service provider delete %s' + % service_provider) + self.assertEqual(0, len(raw_output)) + + def test_sp_show(self): + service_provider = self._create_dummy_sp(add_clean_up=True) + raw_output = self.openstack('service provider show %s' + % service_provider) + items = self.parse_show(raw_output) + self.assert_show_fields(items, self.SERVICE_PROVIDER_FIELDS) + + def test_sp_list(self): + self._create_dummy_sp(add_clean_up=True) + raw_output = self.openstack('service provider list') + items = self.parse_listing(raw_output) + self.assert_table_structure(items, self.SERVICE_PROVIDER_LIST_HEADERS) + + def test_sp_set(self): + service_provider = self._create_dummy_sp(add_clean_up=True) + new_description = data_utils.rand_name('newDescription') + raw_output = self.openstack('service provider set ' + '%(service-provider)s ' + '--description %(description)s ' + % {'service-provider': service_provider, + 'description': new_description}) + self.assertEqual(0, len(raw_output)) + raw_output = self.openstack('service provider show %s' + % service_provider) + updated_value = self.parse_show_as_object(raw_output) + self.assertIn(new_description, updated_value['description']) diff --git a/functional/tests/identity/v3/test_user.py b/functional/tests/identity/v3/test_user.py index 00b9bdc..cc3e08a 100644 --- a/functional/tests/identity/v3/test_user.py +++ b/functional/tests/identity/v3/test_user.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -from tempest_lib.common.utils import data_utils +from tempest.lib.common.utils import data_utils from functional.tests.identity.v3 import test_identity diff --git a/functional/tests/network/v2/test_address_scope.py b/functional/tests/network/v2/test_address_scope.py new file mode 100644 index 0000000..8e25e46 --- /dev/null +++ b/functional/tests/network/v2/test_address_scope.py @@ -0,0 +1,49 @@ +# 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 functional.common import test + + +class AddressScopeTests(test.TestCase): + """Functional tests for address scope. """ + NAME = uuid.uuid4().hex + HEADERS = ['Name'] + FIELDS = ['name'] + + @classmethod + def setUpClass(cls): + opts = cls.get_show_opts(cls.FIELDS) + raw_output = cls.openstack('address scope create ' + cls.NAME + opts) + cls.assertOutput(cls.NAME + "\n", raw_output) + + @classmethod + def tearDownClass(cls): + raw_output = cls.openstack('address scope delete ' + cls.NAME) + cls.assertOutput('', raw_output) + + def test_address_scope_list(self): + opts = self.get_list_opts(self.HEADERS) + raw_output = self.openstack('address scope list' + opts) + self.assertIn(self.NAME, raw_output) + + def test_address_scope_show(self): + opts = self.get_show_opts(self.FIELDS) + raw_output = self.openstack('address scope show ' + self.NAME + opts) + self.assertEqual(self.NAME + "\n", raw_output) + + def test_address_scope_set(self): + self.openstack('address scope set --share ' + self.NAME) + opts = self.get_show_opts(['shared']) + raw_output = self.openstack('address scope show ' + self.NAME + opts) + self.assertEqual("True\n", raw_output) diff --git a/functional/tests/network/v2/test_floating_ip.py b/functional/tests/network/v2/test_floating_ip.py new file mode 100644 index 0000000..f9ecd92 --- /dev/null +++ b/functional/tests/network/v2/test_floating_ip.py @@ -0,0 +1,58 @@ +# 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 functional.common import test + + +class FloatingIpTests(test.TestCase): + """Functional tests for floating ip. """ + SUBNET_NAME = uuid.uuid4().hex + NETWORK_NAME = uuid.uuid4().hex + ID = None + HEADERS = ['ID'] + FIELDS = ['id'] + + @classmethod + def setUpClass(cls): + # Create a network for the floating ip. + cls.openstack('network create --external ' + cls.NETWORK_NAME) + # Create a subnet for the network. + cls.openstack( + 'subnet create --network ' + cls.NETWORK_NAME + + ' --subnet-range 10.10.10.0/24 ' + + cls.SUBNET_NAME + ) + opts = cls.get_show_opts(cls.FIELDS) + raw_output = cls.openstack( + 'ip floating create ' + cls.NETWORK_NAME + opts) + cls.ID = raw_output.strip('\n') + + @classmethod + def tearDownClass(cls): + raw_output = cls.openstack('ip floating delete ' + cls.ID) + cls.assertOutput('', raw_output) + raw_output = cls.openstack('subnet delete ' + cls.SUBNET_NAME) + cls.assertOutput('', raw_output) + raw_output = cls.openstack('network delete ' + cls.NETWORK_NAME) + cls.assertOutput('', raw_output) + + def test_floating_ip_list(self): + opts = self.get_list_opts(self.HEADERS) + raw_output = self.openstack('ip floating list' + opts) + self.assertIn(self.ID, raw_output) + + def test_floating_ip_show(self): + opts = self.get_show_opts(self.FIELDS) + raw_output = self.openstack('ip floating show ' + self.ID + opts) + self.assertEqual(self.ID + "\n", raw_output) diff --git a/functional/tests/network/v2/test_port.py b/functional/tests/network/v2/test_port.py new file mode 100644 index 0000000..5b358a3 --- /dev/null +++ b/functional/tests/network/v2/test_port.py @@ -0,0 +1,58 @@ +# 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 functional.common import test + + +class PortTests(test.TestCase): + """Functional tests for port. """ + NAME = uuid.uuid4().hex + NETWORK_NAME = uuid.uuid4().hex + HEADERS = ['Name'] + FIELDS = ['name'] + + @classmethod + def setUpClass(cls): + # Create a network for the subnet. + cls.openstack('network create ' + cls.NETWORK_NAME) + opts = cls.get_show_opts(cls.FIELDS) + raw_output = cls.openstack( + 'port create --network ' + cls.NETWORK_NAME + ' ' + + cls.NAME + opts + ) + expected = cls.NAME + '\n' + cls.assertOutput(expected, raw_output) + + @classmethod + def tearDownClass(cls): + raw_output = cls.openstack('port delete ' + cls.NAME) + cls.assertOutput('', raw_output) + raw_output = cls.openstack('network delete ' + cls.NETWORK_NAME) + cls.assertOutput('', raw_output) + + def test_port_list(self): + opts = self.get_list_opts(self.HEADERS) + raw_output = self.openstack('port list' + opts) + self.assertIn(self.NAME, raw_output) + + def test_port_set(self): + self.openstack('port set --disable ' + self.NAME) + opts = self.get_show_opts(['name', 'admin_state_up']) + raw_output = self.openstack('port show ' + self.NAME + opts) + self.assertEqual("DOWN\n" + self.NAME + "\n", raw_output) + + def test_port_show(self): + opts = self.get_show_opts(self.FIELDS) + raw_output = self.openstack('port show ' + self.NAME + opts) + self.assertEqual(self.NAME + "\n", raw_output) diff --git a/functional/tests/network/v2/test_security_group_rule.py b/functional/tests/network/v2/test_security_group_rule.py index 9c0b66e..64e1fcd 100644 --- a/functional/tests/network/v2/test_security_group_rule.py +++ b/functional/tests/network/v2/test_security_group_rule.py @@ -37,7 +37,8 @@ class SecurityGroupRuleTests(test.TestCase): opts = cls.get_show_opts(cls.ID_FIELD) raw_output = cls.openstack('security group rule create ' + cls.SECURITY_GROUP_NAME + - ' --proto tcp --dst-port 80:80' + + ' --protocol tcp --dst-port 80:80' + + ' --ingress --ethertype IPv4' + opts) cls.SECURITY_GROUP_RULE_ID = raw_output.strip('\n') diff --git a/functional/tests/network/v2/test_subnet.py b/functional/tests/network/v2/test_subnet.py new file mode 100644 index 0000000..7697e0f --- /dev/null +++ b/functional/tests/network/v2/test_subnet.py @@ -0,0 +1,59 @@ +# 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 functional.common import test + + +class SubnetTests(test.TestCase): + """Functional tests for subnet. """ + NAME = uuid.uuid4().hex + NETWORK_NAME = uuid.uuid4().hex + HEADERS = ['Name'] + FIELDS = ['name'] + + @classmethod + def setUpClass(cls): + # Create a network for the subnet. + cls.openstack('network create ' + cls.NETWORK_NAME) + opts = cls.get_show_opts(cls.FIELDS) + raw_output = cls.openstack( + 'subnet create --network ' + cls.NETWORK_NAME + + ' --subnet-range 10.10.10.0/24 ' + + cls.NAME + opts + ) + expected = cls.NAME + '\n' + cls.assertOutput(expected, raw_output) + + @classmethod + def tearDownClass(cls): + raw_output = cls.openstack('subnet delete ' + cls.NAME) + cls.assertOutput('', raw_output) + raw_output = cls.openstack('network delete ' + cls.NETWORK_NAME) + cls.assertOutput('', raw_output) + + def test_subnet_list(self): + opts = self.get_list_opts(self.HEADERS) + raw_output = self.openstack('subnet list' + opts) + self.assertIn(self.NAME, raw_output) + + def test_subnet_set(self): + self.openstack('subnet set --no-dhcp ' + self.NAME) + opts = self.get_show_opts(['name', 'enable_dhcp']) + raw_output = self.openstack('subnet show ' + self.NAME + opts) + self.assertEqual("False\n" + self.NAME + "\n", raw_output) + + def test_subnet_show(self): + opts = self.get_show_opts(self.FIELDS) + raw_output = self.openstack('subnet show ' + self.NAME + opts) + self.assertEqual(self.NAME + "\n", raw_output) diff --git a/functional/tests/network/v2/test_subnet_pool.py b/functional/tests/network/v2/test_subnet_pool.py new file mode 100644 index 0000000..1515487 --- /dev/null +++ b/functional/tests/network/v2/test_subnet_pool.py @@ -0,0 +1,55 @@ +# 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 functional.common import test + + +class SubnetPoolTests(test.TestCase): + """Functional tests for subnet pool. """ + NAME = uuid.uuid4().hex + CREATE_POOL_PREFIX = '10.100.0.0/24' + SET_POOL_PREFIX = '10.100.0.0/16' + HEADERS = ['Name'] + FIELDS = ['name'] + + @classmethod + def setUpClass(cls): + opts = cls.get_show_opts(cls.FIELDS) + raw_output = cls.openstack('subnet pool create --pool-prefix ' + + cls.CREATE_POOL_PREFIX + ' ' + + cls.NAME + opts) + cls.assertOutput(cls.NAME + '\n', raw_output) + + @classmethod + def tearDownClass(cls): + raw_output = cls.openstack('subnet pool delete ' + cls.NAME) + cls.assertOutput('', raw_output) + + def test_subnet_list(self): + opts = self.get_list_opts(self.HEADERS) + raw_output = self.openstack('subnet pool list' + opts) + self.assertIn(self.NAME, raw_output) + + def test_subnet_set(self): + self.openstack('subnet pool set --pool-prefix ' + + self.SET_POOL_PREFIX + ' ' + self.NAME) + opts = self.get_show_opts(['prefixes', 'name']) + raw_output = self.openstack('subnet pool show ' + self.NAME + opts) + self.assertEqual(self.NAME + '\n' + self.SET_POOL_PREFIX + '\n', + raw_output) + + def test_subnet_show(self): + opts = self.get_show_opts(self.FIELDS) + raw_output = self.openstack('subnet pool show ' + self.NAME + opts) + self.assertEqual(self.NAME + '\n', raw_output) diff --git a/functional/tests/object/v1/test_object.py b/functional/tests/object/v1/test_object.py index cd98012..8ea16da 100644 --- a/functional/tests/object/v1/test_object.py +++ b/functional/tests/object/v1/test_object.py @@ -11,6 +11,7 @@ # under the License. import os +import tempfile import uuid from functional.common import test @@ -24,17 +25,14 @@ class ObjectTests(test.TestCase): """Functional tests for Object commands. """ CONTAINER_NAME = uuid.uuid4().hex - OBJECT_NAME = uuid.uuid4().hex - TMP_FILE = 'tmp.txt' - - def setUp(self): - super(ObjectTests, self).setUp() - self.addCleanup(os.remove, self.OBJECT_NAME) - self.addCleanup(os.remove, self.TMP_FILE) - with open(self.OBJECT_NAME, 'w') as f: - f.write('test content') def test_object(self): + with tempfile.NamedTemporaryFile() as f: + f.write('test content') + f.flush() + self._test_object(f.name) + + def _test_object(self, object_file): raw_output = self.openstack('container create ' + self.CONTAINER_NAME) items = self.parse_listing(raw_output) self.assert_show_fields(items, CONTAINER_FIELDS) @@ -50,7 +48,7 @@ class ObjectTests(test.TestCase): # TODO(stevemar): Assert returned fields raw_output = self.openstack('object create ' + self.CONTAINER_NAME - + ' ' + self.OBJECT_NAME) + + ' ' + object_file) items = self.parse_listing(raw_output) self.assert_show_fields(items, OBJECT_FIELDS) @@ -59,23 +57,25 @@ class ObjectTests(test.TestCase): self.assert_table_structure(items, BASIC_LIST_HEADERS) self.openstack('object save ' + self.CONTAINER_NAME - + ' ' + self.OBJECT_NAME) + + ' ' + object_file) # TODO(stevemar): Assert returned fields + tmp_file = 'tmp.txt' + self.addCleanup(os.remove, tmp_file) self.openstack('object save ' + self.CONTAINER_NAME - + ' ' + self.OBJECT_NAME + ' --file ' + self.TMP_FILE) + + ' ' + object_file + ' --file ' + tmp_file) # TODO(stevemar): Assert returned fields self.openstack('object show ' + self.CONTAINER_NAME - + ' ' + self.OBJECT_NAME) + + ' ' + object_file) # TODO(stevemar): Assert returned fields raw_output = self.openstack('object delete ' + self.CONTAINER_NAME - + ' ' + self.OBJECT_NAME) + + ' ' + object_file) self.assertEqual(0, len(raw_output)) self.openstack('object create ' + self.CONTAINER_NAME - + ' ' + self.OBJECT_NAME) + + ' ' + object_file) raw_output = self.openstack('container delete -r ' + self.CONTAINER_NAME) self.assertEqual(0, len(raw_output)) diff --git a/functional/tests/volume/v2/test_qos.py b/functional/tests/volume/v2/test_qos.py new file mode 100644 index 0000000..24ce1b3 --- /dev/null +++ b/functional/tests/volume/v2/test_qos.py @@ -0,0 +1,61 @@ +# 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 functional.common import test + + +class VolumeTests(test.TestCase): + """Functional tests for volume qos. """ + + NAME = uuid.uuid4().hex + HEADERS = ['Name'] + FIELDS = ['id', 'name'] + ID = None + + @classmethod + def setUpClass(cls): + opts = cls.get_show_opts(cls.FIELDS) + raw_output = cls.openstack('volume qos create ' + cls.NAME + opts) + cls.ID, name, rol = raw_output.split('\n') + cls.assertOutput(cls.NAME, name) + + @classmethod + def tearDownClass(cls): + raw_output = cls.openstack('volume qos delete ' + cls.ID) + cls.assertOutput('', raw_output) + + def test_volume_qos_list(self): + opts = self.get_list_opts(self.HEADERS) + raw_output = self.openstack('volume qos list' + opts) + self.assertIn(self.NAME, raw_output) + + def test_volume_qos_show(self): + opts = self.get_show_opts(self.FIELDS) + raw_output = self.openstack('volume qos show ' + self.ID + opts) + self.assertEqual(self.ID + "\n" + self.NAME + "\n", raw_output) + + def test_volume_qos_metadata(self): + raw_output = self.openstack( + 'volume qos set --property a=b --property c=d ' + self.ID) + self.assertEqual("", raw_output) + opts = self.get_show_opts(['name', 'specs']) + raw_output = self.openstack('volume qos show ' + self.ID + opts) + self.assertEqual(self.NAME + "\na='b', c='d'\n", raw_output) + + raw_output = self.openstack( + 'volume qos unset --property a ' + self.ID) + self.assertEqual("", raw_output) + opts = self.get_show_opts(['name', 'specs']) + raw_output = self.openstack('volume qos show ' + self.ID + opts) + self.assertEqual(self.NAME + "\nc='d'\n", raw_output) diff --git a/functional/tests/volume/v2/test_volume.py b/functional/tests/volume/v2/test_volume.py index b077518..9c7f11a 100644 --- a/functional/tests/volume/v2/test_volume.py +++ b/functional/tests/volume/v2/test_volume.py @@ -11,6 +11,7 @@ # under the License. import os +import time import uuid from functional.common import test @@ -20,6 +21,8 @@ class VolumeTests(test.TestCase): """Functional tests for volume. """ NAME = uuid.uuid4().hex + SNAPSHOT_NAME = uuid.uuid4().hex + VOLUME_FROM_SNAPSHOT_NAME = uuid.uuid4().hex OTHER_NAME = uuid.uuid4().hex HEADERS = ['"Display Name"'] FIELDS = ['name'] @@ -28,17 +31,20 @@ class VolumeTests(test.TestCase): def setUpClass(cls): os.environ['OS_VOLUME_API_VERSION'] = '2' opts = cls.get_show_opts(cls.FIELDS) + + # Create test volume raw_output = cls.openstack('volume create --size 1 ' + cls.NAME + opts) expected = cls.NAME + '\n' cls.assertOutput(expected, raw_output) @classmethod def tearDownClass(cls): - # Rename test + # Rename test volume raw_output = cls.openstack( 'volume set --name ' + cls.OTHER_NAME + ' ' + cls.NAME) cls.assertOutput('', raw_output) - # Delete test + + # Delete test volume raw_output = cls.openstack('volume delete ' + cls.OTHER_NAME) cls.assertOutput('', raw_output) @@ -78,3 +84,47 @@ class VolumeTests(test.TestCase): opts = self.get_show_opts(["name", "size"]) raw_output = self.openstack('volume show ' + self.NAME + opts) self.assertEqual(self.NAME + "\n2\n", raw_output) + + def test_volume_snapshot(self): + opts = self.get_show_opts(self.FIELDS) + + # Create snapshot from test volume + raw_output = self.openstack('snapshot create ' + self.NAME + + ' --name ' + self.SNAPSHOT_NAME + opts) + expected = self.SNAPSHOT_NAME + '\n' + self.assertOutput(expected, raw_output) + self.wait_for("snapshot", self.SNAPSHOT_NAME, "available") + + # Create volume from snapshot + raw_output = self.openstack('volume create --size 2 --snapshot ' + + self.SNAPSHOT_NAME + ' ' + + self.VOLUME_FROM_SNAPSHOT_NAME + opts) + expected = self.VOLUME_FROM_SNAPSHOT_NAME + '\n' + self.assertOutput(expected, raw_output) + self.wait_for("volume", self.VOLUME_FROM_SNAPSHOT_NAME, "available") + + # Delete volume that create from snapshot + raw_output = self.openstack('volume delete ' + + self.VOLUME_FROM_SNAPSHOT_NAME) + self.assertOutput('', raw_output) + + # Delete test snapshot + raw_output = self.openstack('snapshot delete ' + self.SNAPSHOT_NAME) + self.assertOutput('', raw_output) + + def wait_for(self, check_type, check_name, desired_status, wait=120, + interval=5, failures=['ERROR']): + status = "notset" + total_sleep = 0 + opts = self.get_show_opts(['status']) + while total_sleep < wait: + status = self.openstack(check_type + ' show ' + check_name + opts) + status = status.rstrip() + print('Checking {} {} Waiting for {} current status: {}' + .format(check_type, check_name, desired_status, status)) + if status == desired_status: + break + self.assertNotIn(status, failures) + time.sleep(interval) + total_sleep += interval + self.assertEqual(desired_status, status) diff --git a/functional/tests/volume/v2/test_volume_type.py b/functional/tests/volume/v2/test_volume_type.py new file mode 100644 index 0000000..d8a7a7e --- /dev/null +++ b/functional/tests/volume/v2/test_volume_type.py @@ -0,0 +1,70 @@ +# 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 functional.common import test + + +class VolumeTypeTests(test.TestCase): + """Functional tests for volume type. """ + + NAME = uuid.uuid4().hex + HEADERS = ['"Name"'] + FIELDS = ['name'] + + @classmethod + def setUpClass(cls): + opts = cls.get_show_opts(cls.FIELDS) + raw_output = cls.openstack( + 'volume type create --private ' + cls.NAME + opts) + expected = cls.NAME + '\n' + cls.assertOutput(expected, raw_output) + + @classmethod + def tearDownClass(cls): + raw_output = cls.openstack('volume type delete ' + cls.NAME) + cls.assertOutput('', raw_output) + + def test_volume_type_list(self): + opts = self.get_list_opts(self.HEADERS) + raw_output = self.openstack('volume type list' + opts) + self.assertIn(self.NAME, raw_output) + + def test_volume_type_show(self): + opts = self.get_show_opts(self.FIELDS) + raw_output = self.openstack('volume type show ' + self.NAME + opts) + self.assertEqual(self.NAME + "\n", raw_output) + + def test_volume_type_set_unset_properties(self): + raw_output = self.openstack( + 'volume type set --property a=b --property c=d ' + self.NAME) + self.assertEqual("", raw_output) + + opts = self.get_show_opts(["properties"]) + raw_output = self.openstack('volume type show ' + self.NAME + opts) + self.assertEqual("a='b', c='d'\n", raw_output) + + raw_output = self.openstack('volume type unset --property a ' + + self.NAME) + self.assertEqual("", raw_output) + raw_output = self.openstack('volume type show ' + self.NAME + opts) + self.assertEqual("c='d'\n", raw_output) + + def test_volume_type_set_unset_project(self): + raw_output = self.openstack( + 'volume type set --project admin ' + self.NAME) + self.assertEqual("", raw_output) + + raw_output = self.openstack( + 'volume type unset --project admin ' + self.NAME) + self.assertEqual("", raw_output) diff --git a/openstackclient/api/api.py b/openstackclient/api/api.py index 6a88e7f..bd2abd8 100644 --- a/openstackclient/api/api.py +++ b/openstackclient/api/api.py @@ -331,7 +331,7 @@ class BaseAPI(KeystoneSession): :param string path: The API-specific portion of the URL path - :param string search: + :param string value: search expression :param string attr: name of attribute for secondary search diff --git a/openstackclient/api/auth.py b/openstackclient/api/auth.py index 3d6f7bc..c74e800 100644 --- a/openstackclient/api/auth.py +++ b/openstackclient/api/auth.py @@ -142,14 +142,14 @@ def check_valid_auth_options(options, auth_plugin_name, required_scope=True): """ - msg = '' + msgs = [] if auth_plugin_name.endswith('password'): if not options.auth.get('username'): - msg += _('Set a username with --os-username, OS_USERNAME,' - ' or auth.username\n') + msgs.append(_('Set a username with --os-username, OS_USERNAME,' + ' or auth.username')) if not options.auth.get('auth_url'): - msg += _('Set an authentication URL, with --os-auth-url,' - ' OS_AUTH_URL or auth.auth_url\n') + msgs.append(_('Set an authentication URL, with --os-auth-url,' + ' OS_AUTH_URL or auth.auth_url')) if (required_scope and not options.auth.get('project_id') and not options.auth.get('domain_id') and not @@ -157,24 +157,28 @@ def check_valid_auth_options(options, auth_plugin_name, required_scope=True): options.auth.get('project_name') and not options.auth.get('tenant_id') and not options.auth.get('tenant_name')): - msg += _('Set a scope, such as a project or domain, set a ' - 'project scope with --os-project-name, OS_PROJECT_NAME ' - 'or auth.project_name, set a domain scope with ' - '--os-domain-name, OS_DOMAIN_NAME or auth.domain_name') + msgs.append(_('Set a scope, such as a project or domain, set a ' + 'project scope with --os-project-name, ' + 'OS_PROJECT_NAME or auth.project_name, set a domain ' + 'scope with --os-domain-name, OS_DOMAIN_NAME or ' + 'auth.domain_name')) elif auth_plugin_name.endswith('token'): if not options.auth.get('token'): - msg += _('Set a token with --os-token, OS_TOKEN or auth.token\n') + msgs.append(_('Set a token with --os-token, OS_TOKEN or ' + 'auth.token')) if not options.auth.get('auth_url'): - msg += _('Set a service AUTH_URL, with --os-auth-url, ' - 'OS_AUTH_URL or auth.auth_url\n') + msgs.append(_('Set a service AUTH_URL, with --os-auth-url, ' + 'OS_AUTH_URL or auth.auth_url')) elif auth_plugin_name == 'token_endpoint': if not options.auth.get('token'): - msg += _('Set a token with --os-token, OS_TOKEN or auth.token\n') + msgs.append(_('Set a token with --os-token, OS_TOKEN or ' + 'auth.token')) if not options.auth.get('url'): - msg += _('Set a service URL, with --os-url, OS_URL or auth.url\n') + msgs.append(_('Set a service URL, with --os-url, OS_URL or ' + 'auth.url')) - if msg: - raise exc.CommandError('Missing parameter(s): \n%s' % msg) + if msgs: + raise exc.CommandError('Missing parameter(s): \n%s' % '\n'.join(msgs)) def build_auth_plugins_option_parser(parser): diff --git a/openstackclient/api/utils.py b/openstackclient/api/utils.py index ab0e23c..6407cd4 100644 --- a/openstackclient/api/utils.py +++ b/openstackclient/api/utils.py @@ -29,7 +29,7 @@ def simple_filter( The name of the attribute to filter. If attr does not exist no match will succeed and no rows will be returned. If attr is None no filtering will be performed and all rows will be returned. - :param sring value: + :param string value: The value to filter. None is considered to be a 'no filter' value. '' matches against a Python empty string. :param string property_field: diff --git a/openstackclient/common/availability_zone.py b/openstackclient/common/availability_zone.py index a6d11b7..3b0270a 100644 --- a/openstackclient/common/availability_zone.py +++ b/openstackclient/common/availability_zone.py @@ -20,7 +20,7 @@ import six from openstackclient.common import command from openstackclient.common import utils -from openstackclient.i18n import _ # noqa +from openstackclient.i18n import _ def _xform_common_availability_zone(az, zone_info): diff --git a/openstackclient/common/clientmanager.py b/openstackclient/common/clientmanager.py index 938dd05..9c2b320 100644 --- a/openstackclient/common/clientmanager.py +++ b/openstackclient/common/clientmanager.py @@ -22,8 +22,10 @@ import sys from oslo_utils import strutils import requests +import six from openstackclient.api import auth +from openstackclient.common import exceptions from openstackclient.common import session as osc_session from openstackclient.identity import client as identity_client @@ -45,7 +47,13 @@ class ClientCache(object): def __get__(self, instance, owner): # Tell the ClientManager to login to keystone if self._handle is None: - self._handle = self.factory(instance) + try: + self._handle = self.factory(instance) + except AttributeError as err: + # Make sure the failure propagates. Otherwise, the plugin just + # quietly isn't there. + new_err = exceptions.PluginAttributeError(err) + six.reraise(new_err.__class__, new_err, sys.exc_info()[2]) return self._handle @@ -110,6 +118,15 @@ class ClientManager(object): self._cacert = verify self._insecure = False + # Set up client certificate and key + # NOTE(cbrandily): This converts client certificate/key to requests + # cert argument: None (no client certificate), a path + # to client certificate or a tuple with client + # certificate/key paths. + self._cert = self._cli_options.cert + if self._cert and self._cli_options.key: + self._cert = self._cert, self._cli_options.key + # Get logging from root logger root_logger = logging.getLogger('') LOG.setLevel(root_logger.getEffectiveLevel()) @@ -178,6 +195,18 @@ class ClientManager(object): not self._auth_params.get('user_domain_name')): self._auth_params['user_domain_id'] = default_domain + # NOTE(hieulq): If USER_DOMAIN_NAME, USER_DOMAIN_ID, PROJECT_DOMAIN_ID + # or PROJECT_DOMAIN_NAME is present and API_VERSION is 2.0, then + # ignore all domain related configs. + if (self._api_version.get('identity') == '2.0' and + self.auth_plugin_name.endswith('password')): + domain_props = ['project_domain_name', 'project_domain_id', + 'user_domain_name', 'user_domain_id'] + for prop in domain_props: + if self._auth_params.pop(prop, None) is not None: + LOG.warning("Ignoring domain related configs " + + prop + " because identity API version is 2.0") + # For compatibility until all clients can be updated if 'project_name' in self._auth_params: self._project_name = self._auth_params['project_name'] @@ -194,13 +223,12 @@ class ClientManager(object): auth=self.auth, session=request_session, verify=self._verify, + cert=self._cert, user_agent=USER_AGENT, ) self._auth_setup_completed = True - return - @property def auth_ref(self): """Dereference will trigger an auth if it hasn't already""" diff --git a/openstackclient/common/commandmanager.py b/openstackclient/common/commandmanager.py index b809d63..c190e33 100644 --- a/openstackclient/common/commandmanager.py +++ b/openstackclient/common/commandmanager.py @@ -56,4 +56,4 @@ class CommandManager(cliff.commandmanager.CommandManager): ) group_list.append(cmd_name) return group_list - return self.commands.keys() + return list(self.commands.keys()) diff --git a/openstackclient/common/exceptions.py b/openstackclient/common/exceptions.py index 5f5f5ab..bdc33dd 100644 --- a/openstackclient/common/exceptions.py +++ b/openstackclient/common/exceptions.py @@ -24,6 +24,13 @@ class AuthorizationFailure(Exception): pass +class PluginAttributeError(Exception): + """A plugin threw an AttributeError while being lazily loaded.""" + # This *must not* inherit from AttributeError; + # that would defeat the whole purpose. + pass + + class NoTokenLookupException(Exception): """This does not support looking up endpoints from an existing token.""" pass @@ -108,28 +115,3 @@ _code_map = dict((c.http_status, c) for c in [ OverLimit, HTTPNotImplemented ]) - - -def from_response(response, body): - """Return an instance of a ClientException based on an httplib2 response. - - Usage:: - - resp, body = http.request(...) - if resp.status != 200: - raise exception_from_response(resp, body) - """ - cls = _code_map.get(response.status, ClientException) - if body: - if hasattr(body, 'keys'): - error = body[body.keys()[0]] - message = error.get('message') - details = error.get('details') - else: - # If we didn't get back a properly formed error message we - # probably couldn't communicate with Keystone at all. - message = "Unable to communicate with image service: %s." % body - details = None - return cls(code=response.status, message=message, details=details) - else: - return cls(code=response.status) diff --git a/openstackclient/common/limits.py b/openstackclient/common/limits.py index bd546c0..1f87abf 100644 --- a/openstackclient/common/limits.py +++ b/openstackclient/common/limits.py @@ -55,8 +55,8 @@ class ShowLimits(command.Lister): parser.add_argument( '--domain', metavar='<domain>', - help='Domain that owns --project (name or ID)' - ' [only valid with --absolute]', + help='Domain the project belongs to (name or ID)' + ' [only valid with --absolute]', ) return parser diff --git a/openstackclient/common/module.py b/openstackclient/common/module.py index a3dea5d..30c67c6 100644 --- a/openstackclient/common/module.py +++ b/openstackclient/common/module.py @@ -19,6 +19,7 @@ import six import sys from openstackclient.common import command +from openstackclient.common import utils class ListCommand(command.Lister): @@ -29,9 +30,24 @@ class ListCommand(command.Lister): def take_action(self, parsed_args): cm = self.app.command_manager groups = cm.get_command_groups() - + groups = sorted(groups) columns = ('Command Group', 'Commands') - return (columns, ((c, cm.get_command_names(group=c)) for c in groups)) + + commands = [] + for group in groups: + command_names = cm.get_command_names(group) + command_names = sorted(command_names) + + if command_names != []: + + # TODO(bapalm): Fix this when cliff properly supports + # handling the detection rather than using the hard-code below. + if parsed_args.formatter == 'table': + command_names = utils.format_list(command_names, "\n") + + commands.append((group, command_names)) + + return (columns, commands) class ListModule(command.ShowOne): diff --git a/openstackclient/common/parseractions.py b/openstackclient/common/parseractions.py index c30058c..77798f9 100644 --- a/openstackclient/common/parseractions.py +++ b/openstackclient/common/parseractions.py @@ -155,9 +155,8 @@ class NonNegativeAction(argparse.Action): """ def __call__(self, parser, namespace, values, option_string=None): - try: - assert(int(values) >= 0) + if int(values) >= 0: setattr(namespace, self.dest, values) - except Exception: + else: msg = "%s expected a non-negative integer" % (str(option_string)) raise argparse.ArgumentTypeError(msg) diff --git a/openstackclient/common/quota.py b/openstackclient/common/quota.py index b3d4c3b..e177fbc 100644 --- a/openstackclient/common/quota.py +++ b/openstackclient/common/quota.py @@ -29,7 +29,6 @@ from openstackclient.common import utils COMPUTE_QUOTAS = { 'cores': 'cores', 'fixed_ips': 'fixed-ips', - 'floating_ips': 'floating-ips', 'injected_file_content_bytes': 'injected-file-size', 'injected_file_path_bytes': 'injected-path-size', 'injected_files': 'injected-files', @@ -37,8 +36,6 @@ COMPUTE_QUOTAS = { 'key_pairs': 'key-pairs', 'metadata_items': 'properties', 'ram': 'ram', - 'security_group_rules': 'secgroup-rules', - 'security_groups': 'secgroups', } VOLUME_QUOTAS = { @@ -47,16 +44,41 @@ VOLUME_QUOTAS = { 'volumes': 'volumes', } +NOVA_NETWORK_QUOTAS = { + 'floating_ips': 'floating-ips', + 'security_group_rules': 'secgroup-rules', + 'security_groups': 'secgroups', +} + NETWORK_QUOTAS = { 'floatingip': 'floating-ips', 'security_group_rule': 'secgroup-rules', 'security_group': 'secgroups', + 'network': 'networks', + 'subnet': 'subnets', + 'port': 'ports', + 'router': 'routers', + 'rbac_policy': 'rbac-policies', + 'vip': 'vips', + 'subnetpool': 'subnetpools', + 'member': 'members', + 'health_monitor': 'health-monitors', } class SetQuota(command.Command): """Set quotas for project or class""" + def _build_options_list(self): + if self.app.client_manager.is_network_endpoint_enabled(): + return itertools.chain(COMPUTE_QUOTAS.items(), + VOLUME_QUOTAS.items(), + NETWORK_QUOTAS.items()) + else: + return itertools.chain(COMPUTE_QUOTAS.items(), + VOLUME_QUOTAS.items(), + NOVA_NETWORK_QUOTAS.items()) + def get_parser(self, prog_name): parser = super(SetQuota, self).get_parser(prog_name) parser.add_argument( @@ -71,8 +93,7 @@ class SetQuota(command.Command): default=False, help='Set quotas for <class>', ) - for k, v in itertools.chain( - COMPUTE_QUOTAS.items(), VOLUME_QUOTAS.items()): + for k, v in self._build_options_list(): parser.add_argument( '--%s' % v, metavar='<%s>' % v, @@ -92,7 +113,7 @@ class SetQuota(command.Command): identity_client = self.app.client_manager.identity compute_client = self.app.client_manager.compute volume_client = self.app.client_manager.volume - + network_client = self.app.client_manager.network compute_kwargs = {} for k, v in COMPUTE_QUOTAS.items(): value = getattr(parsed_args, k, None) @@ -107,7 +128,20 @@ class SetQuota(command.Command): k = k + '_%s' % parsed_args.volume_type volume_kwargs[k] = value - if compute_kwargs == {} and volume_kwargs == {}: + network_kwargs = {} + if self.app.client_manager.is_network_endpoint_enabled(): + for k, v in NETWORK_QUOTAS.items(): + value = getattr(parsed_args, k, None) + if value is not None: + network_kwargs[k] = value + else: + for k, v in NOVA_NETWORK_QUOTAS.items(): + value = getattr(parsed_args, k, None) + if value is not None: + compute_kwargs[k] = value + + if (compute_kwargs == {} and volume_kwargs == {} + and network_kwargs == {}): sys.stderr.write("No quotas updated") return @@ -126,6 +160,9 @@ class SetQuota(command.Command): volume_client.quota_classes.update( project.id, **volume_kwargs) + if network_kwargs: + sys.stderr.write("Network quotas are ignored since quota class" + "is not supported.") else: if compute_kwargs: compute_client.quotas.update( @@ -135,6 +172,10 @@ class SetQuota(command.Command): volume_client.quotas.update( project.id, **volume_kwargs) + if network_kwargs: + network_client.update_quota( + project.id, + **network_kwargs) class ShowQuota(command.ShowOne): @@ -145,7 +186,8 @@ class ShowQuota(command.ShowOne): parser.add_argument( 'project', metavar='<project/class>', - help='Show this project or class (name/ID)', + nargs='?', + help='Show quotas for this project or class (name or ID)', ) type_group = parser.add_mutually_exclusive_group() type_group.add_argument( @@ -164,12 +206,22 @@ class ShowQuota(command.ShowOne): ) return parser + def _get_project(self, parsed_args): + if parsed_args.project is not None: + identity_client = self.app.client_manager.identity + project = utils.find_resource( + identity_client.projects, + parsed_args.project, + ).id + elif self.app.client_manager.auth_ref: + # Get the project from the current auth + project = self.app.client_manager.auth_ref.project_id + else: + project = None + return project + def get_compute_volume_quota(self, client, parsed_args): - identity_client = self.app.client_manager.identity - project = utils.find_resource( - identity_client.projects, - parsed_args.project, - ).id + project = self._get_project(parsed_args) try: if parsed_args.quota_class: @@ -189,11 +241,7 @@ class ShowQuota(command.ShowOne): if parsed_args.quota_class or parsed_args.default: return {} if self.app.client_manager.is_network_endpoint_enabled(): - identity_client = self.app.client_manager.identity - project = utils.find_resource( - identity_client.projects, - parsed_args.project, - ).id + project = self._get_project(parsed_args) return self.app.client_manager.network.get_quota(project) else: return {} @@ -225,8 +273,8 @@ class ShowQuota(command.ShowOne): # neutron is enabled, quotas of these three resources # in nova will be replaced by neutron's. for k, v in itertools.chain( - COMPUTE_QUOTAS.items(), VOLUME_QUOTAS.items(), - NETWORK_QUOTAS.items()): + COMPUTE_QUOTAS.items(), NOVA_NETWORK_QUOTAS.items(), + VOLUME_QUOTAS.items(), NETWORK_QUOTAS.items()): if not k == v and info.get(k): info[v] = info[k] info.pop(k) diff --git a/openstackclient/common/utils.py b/openstackclient/common/utils.py index 840da40..daa65c2 100644 --- a/openstackclient/common/utils.py +++ b/openstackclient/common/utils.py @@ -100,22 +100,15 @@ def find_resource(manager, name_or_id, **kwargs): else: pass - try: - for resource in manager.list(): - # short circuit and return the first match - if (resource.get('id') == name_or_id or - resource.get('name') == name_or_id): - return resource - else: - # we found no match, keep going to bomb out - pass - except Exception: - # in case the list fails for some reason - pass - - # if we hit here, we've failed, report back this error: - msg = "Could not find resource %s" % name_or_id - raise exceptions.CommandError(msg) + for resource in manager.list(): + # short circuit and return the first match + if (resource.get('id') == name_or_id or + resource.get('name') == name_or_id): + return resource + else: + # we found no match, report back this error: + msg = "Could not find resource %s" % name_or_id + raise exceptions.CommandError(msg) def format_dict(data): @@ -281,7 +274,7 @@ def get_client_class(api_name, version, version_map): client_path = version_map[str(version)] except (KeyError, ValueError): msg = "Invalid %s client version '%s'. must be one of: %s" % ( - (api_name, version, ', '.join(version_map.keys()))) + (api_name, version, ', '.join(list(version_map.keys())))) raise exceptions.UnsupportedVersion(msg) return importutils.import_class(client_path) diff --git a/openstackclient/compute/client.py b/openstackclient/compute/client.py index 1481ed6..82f09ce 100644 --- a/openstackclient/compute/client.py +++ b/openstackclient/compute/client.py @@ -41,8 +41,18 @@ def make_client(instance): version = _compute_api_version else: version = instance._api_version[API_NAME] + from novaclient import api_versions + # convert to APIVersion object + version = api_versions.get_api_version(version) + + if version.is_latest(): + import novaclient + # NOTE(RuiChen): executing version discovery make sense, but that need + # an initialized REST client, it's not available now, + # fallback to use the max version of novaclient side. + version = novaclient.API_MAX_VERSION - LOG.debug('Instantiating compute client for V%s', version) + LOG.debug('Instantiating compute client for %s', version) # Set client http_log_debug to True if verbosity level is high enough http_log_debug = utils.get_effective_log_level() <= logging.DEBUG @@ -91,30 +101,27 @@ def check_api_version(check_version): """ # Defer client imports until we actually need them - try: - from novaclient import api_versions - except ImportError: - # Retain previous behaviour - return False - import novaclient + from novaclient import api_versions global _compute_api_version - # Copy some logic from novaclient 2.27.0 for basic version detection + # Copy some logic from novaclient 3.3.0 for basic version detection # NOTE(dtroyer): This is only enough to resume operations using API # version 2.0 or any valid version supplied by the user. _compute_api_version = api_versions.get_api_version(check_version) - if _compute_api_version > api_versions.APIVersion("2.0"): - if not _compute_api_version.matches( - novaclient.API_MIN_VERSION, - novaclient.API_MAX_VERSION, - ): - raise exceptions.CommandError( - "versions supported by client: %s - %s" % ( - novaclient.API_MIN_VERSION.get_string(), - novaclient.API_MAX_VERSION.get_string(), - ), - ) + # Bypass X.latest format microversion + if not _compute_api_version.is_latest(): + if _compute_api_version > api_versions.APIVersion("2.0"): + if not _compute_api_version.matches( + novaclient.API_MIN_VERSION, + novaclient.API_MAX_VERSION, + ): + raise exceptions.CommandError( + "versions supported by client: %s - %s" % ( + novaclient.API_MIN_VERSION.get_string(), + novaclient.API_MAX_VERSION.get_string(), + ), + ) return True diff --git a/openstackclient/compute/v2/agent.py b/openstackclient/compute/v2/agent.py index d5e8603..e358399 100644 --- a/openstackclient/compute/v2/agent.py +++ b/openstackclient/compute/v2/agent.py @@ -19,6 +19,7 @@ import six from openstackclient.common import command from openstackclient.common import utils +from openstackclient.i18n import _ class CreateAgent(command.ShowOne): @@ -29,28 +30,34 @@ class CreateAgent(command.ShowOne): parser.add_argument( "os", metavar="<os>", - help="Type of OS") + help=_("Type of OS") + ) parser.add_argument( "architecture", metavar="<architecture>", - help="Type of architecture") + help=_("Type of architecture") + ) parser.add_argument( "version", metavar="<version>", - help="Version") + help=_("Version") + ) parser.add_argument( "url", metavar="<url>", - help="URL") + help=_("URL") + ) parser.add_argument( "md5hash", metavar="<md5hash>", - help="MD5 hash") + help=_("MD5 hash") + ) parser.add_argument( "hypervisor", metavar="<hypervisor>", - help="Type of hypervisor", - default="xen") + default="xen", + help=_("Type of hypervisor") + ) return parser def take_action(self, parsed_args): @@ -75,7 +82,8 @@ class DeleteAgent(command.Command): parser.add_argument( "id", metavar="<id>", - help="ID of agent to delete") + help=_("ID of agent to delete") + ) return parser def take_action(self, parsed_args): @@ -91,7 +99,8 @@ class ListAgent(command.Lister): parser.add_argument( "--hypervisor", metavar="<hypervisor>", - help="Type of hypervisor") + help=_("Type of hypervisor") + ) return parser def take_action(self, parsed_args): @@ -120,19 +129,23 @@ class SetAgent(command.Command): parser.add_argument( "id", metavar="<id>", - help="ID of the agent") + help=_("ID of the agent") + ) parser.add_argument( "version", metavar="<version>", - help="Version of the agent") + help=_("Version of the agent") + ) parser.add_argument( "url", metavar="<url>", - help="URL") + help=_("URL") + ) parser.add_argument( "md5hash", metavar="<md5hash>", - help="MD5 hash") + help=_("MD5 hash") + ) return parser def take_action(self, parsed_args): diff --git a/openstackclient/compute/v2/aggregate.py b/openstackclient/compute/v2/aggregate.py index e47c13a..752e0fd 100644 --- a/openstackclient/compute/v2/aggregate.py +++ b/openstackclient/compute/v2/aggregate.py @@ -21,6 +21,7 @@ import six from openstackclient.common import command from openstackclient.common import parseractions from openstackclient.common import utils +from openstackclient.i18n import _ class AddAggregateHost(command.ShowOne): @@ -31,12 +32,12 @@ class AddAggregateHost(command.ShowOne): parser.add_argument( 'aggregate', metavar='<aggregate>', - help='Aggregate (name or ID)', + help=_("Aggregate (name or ID)") ) parser.add_argument( 'host', metavar='<host>', - help='Host to add to <aggregate>', + help=_("Host to add to <aggregate>") ) return parser @@ -62,19 +63,19 @@ class CreateAggregate(command.ShowOne): parser.add_argument( "name", metavar="<name>", - help="New aggregate name", + help=_("New aggregate name") ) parser.add_argument( "--zone", metavar="<availability-zone>", - help="Availability zone name", + help=_("Availability zone name") ) parser.add_argument( "--property", metavar="<key=value>", action=parseractions.KeyValueAction, - help='Property to add to this aggregate ' - '(repeat option to set multiple properties)', + help=_("Property to add to this aggregate " + "(repeat option to set multiple properties)") ) return parser @@ -105,7 +106,7 @@ class DeleteAggregate(command.Command): parser.add_argument( 'aggregate', metavar='<aggregate>', - help='Aggregate to delete (name or ID)', + help=_("Aggregate to delete (name or ID)") ) return parser @@ -128,7 +129,8 @@ class ListAggregate(command.Lister): '--long', action='store_true', default=False, - help='List additional fields in output') + help=_("List additional fields in output") + ) return parser def take_action(self, parsed_args): @@ -175,12 +177,12 @@ class RemoveAggregateHost(command.ShowOne): parser.add_argument( 'aggregate', metavar='<aggregate>', - help='Aggregate (name or ID)', + help=_("Aggregate (name or ID)") ) parser.add_argument( 'host', metavar='<host>', - help='Host to remove from <aggregate>', + help=_("Host to remove from <aggregate>") ) return parser @@ -209,24 +211,24 @@ class SetAggregate(command.Command): parser.add_argument( 'aggregate', metavar='<aggregate>', - help='Aggregate to modify (name or ID)', + help=_("Aggregate to modify (name or ID)") ) parser.add_argument( '--name', metavar='<name>', - help='Set aggregate name', + help=_("Set aggregate name") ) parser.add_argument( "--zone", metavar="<availability-zone>", - help="Set availability zone name", + help=_("Set availability zone name") ) parser.add_argument( "--property", metavar="<key=value>", action=parseractions.KeyValueAction, - help='Property to set on <aggregate> ' - '(repeat option to set multiple properties)', + help=_("Property to set on <aggregate> " + "(repeat option to set multiple properties)") ) return parser @@ -264,7 +266,7 @@ class ShowAggregate(command.ShowOne): parser.add_argument( 'aggregate', metavar='<aggregate>', - help='Aggregate to display (name or ID)', + help=_("Aggregate to display (name or ID)") ) return parser @@ -290,3 +292,34 @@ class ShowAggregate(command.ShowOne): info = {} info.update(data._info) return zip(*sorted(six.iteritems(info))) + + +class UnsetAggregate(command.Command): + """Unset aggregate properties""" + + def get_parser(self, prog_name): + parser = super(UnsetAggregate, self).get_parser(prog_name) + parser.add_argument( + "aggregate", + metavar="<aggregate>", + help=_("Aggregate to modify (name or ID)") + ) + parser.add_argument( + "--property", + metavar="<key>", + action='append', + required=True, + help=_("Property to remove from aggregate " + "(repeat option to remove multiple properties)") + ) + return parser + + def take_action(self, parsed_args): + compute_client = self.app.client_manager.compute + aggregate = utils.find_resource( + compute_client.aggregates, + parsed_args.aggregate) + + unset_property = {key: None for key in parsed_args.property} + compute_client.aggregates.set_metadata(aggregate, + unset_property) diff --git a/openstackclient/compute/v2/console.py b/openstackclient/compute/v2/console.py index 6c0ec31..1165862 100644 --- a/openstackclient/compute/v2/console.py +++ b/openstackclient/compute/v2/console.py @@ -21,6 +21,7 @@ import sys from openstackclient.common import command from openstackclient.common import parseractions from openstackclient.common import utils +from openstackclient.i18n import _ class ShowConsoleLog(command.Command): @@ -31,7 +32,7 @@ class ShowConsoleLog(command.Command): parser.add_argument( 'server', metavar='<server>', - help='Server to show console log (name or ID)', + help=_("Server to show console log (name or ID)") ) parser.add_argument( '--lines', @@ -39,8 +40,8 @@ class ShowConsoleLog(command.Command): type=int, default=None, action=parseractions.NonNegativeAction, - help='Number of lines to display from the end of the log ' - '(default=all)', + help=_("Number of lines to display from the end of the log " + "(default=all)") ) return parser @@ -69,7 +70,7 @@ class ShowConsoleURL(command.ShowOne): parser.add_argument( 'server', metavar='<server>', - help='Server to show URL (name or ID)', + help=_("Server to show URL (name or ID)") ) type_group = parser.add_mutually_exclusive_group() type_group.add_argument( @@ -78,21 +79,21 @@ class ShowConsoleURL(command.ShowOne): action='store_const', const='novnc', default='novnc', - help='Show noVNC console URL (default)', + help=_("Show noVNC console URL (default)") ) type_group.add_argument( '--xvpvnc', dest='url_type', action='store_const', const='xvpvnc', - help='Show xpvnc console URL', + help=_("Show xpvnc console URL") ) type_group.add_argument( '--spice', dest='url_type', action='store_const', const='spice', - help='Show SPICE console URL', + help=_("Show SPICE console URL") ) return parser diff --git a/openstackclient/compute/v2/flavor.py b/openstackclient/compute/v2/flavor.py index b5a7c60..37ff831 100644 --- a/openstackclient/compute/v2/flavor.py +++ b/openstackclient/compute/v2/flavor.py @@ -18,8 +18,28 @@ import six from openstackclient.common import command +from openstackclient.common import exceptions from openstackclient.common import parseractions from openstackclient.common import utils +from openstackclient.i18n import _ + + +def _find_flavor(compute_client, flavor): + try: + return compute_client.flavors.get(flavor) + except Exception as ex: + if type(ex).__name__ == 'NotFound': + pass + else: + raise + try: + return compute_client.flavors.find(name=flavor, is_public=None) + except Exception as ex: + if type(ex).__name__ == 'NotFound': + msg = _("No flavor with a name or ID of '%s' exists.") % flavor + raise exceptions.CommandError(msg) + else: + raise class CreateFlavor(command.ShowOne): @@ -30,56 +50,56 @@ class CreateFlavor(command.ShowOne): parser.add_argument( "name", metavar="<flavor-name>", - help="New flavor name", + help=_("New flavor name") ) parser.add_argument( "--id", metavar="<id>", default='auto', - help="Unique flavor ID; 'auto' creates a UUID " - "(default: auto)", + help=_("Unique flavor ID; 'auto' creates a UUID " + "(default: auto)") ) parser.add_argument( "--ram", type=int, metavar="<size-mb>", default=256, - help="Memory size in MB (default 256M)", + help=_("Memory size in MB (default 256M)") ) parser.add_argument( "--disk", type=int, metavar="<size-gb>", default=0, - help="Disk size in GB (default 0G)", + help=_("Disk size in GB (default 0G)") ) parser.add_argument( "--ephemeral", type=int, metavar="<size-gb>", default=0, - help="Ephemeral disk size in GB (default 0G)", + help=_("Ephemeral disk size in GB (default 0G)") ) parser.add_argument( "--swap", type=int, metavar="<size-gb>", default=0, - help="Swap space size in GB (default 0G)", + help=_("Swap space size in GB (default 0G)") ) parser.add_argument( "--vcpus", type=int, metavar="<vcpus>", default=1, - help="Number of vcpus (default 1)", + help=_("Number of vcpus (default 1)") ) parser.add_argument( "--rxtx-factor", - type=int, + type=float, metavar="<factor>", - default=1, - help="RX/TX factor (default 1)", + default=1.0, + help=_("RX/TX factor (default 1.0)") ) public_group = parser.add_mutually_exclusive_group() public_group.add_argument( @@ -87,13 +107,13 @@ class CreateFlavor(command.ShowOne): dest="public", action="store_true", default=True, - help="Flavor is available to other projects (default)", + help=_("Flavor is available to other projects (default)") ) public_group.add_argument( "--private", dest="public", action="store_false", - help="Flavor is not available to other projects", + help=_("Flavor is not available to other projects") ) return parser @@ -126,14 +146,13 @@ class DeleteFlavor(command.Command): parser.add_argument( "flavor", metavar="<flavor>", - help="Flavor to delete (name or ID)", + help=_("Flavor to delete (name or ID)") ) return parser def take_action(self, parsed_args): compute_client = self.app.client_manager.compute - flavor = utils.find_resource(compute_client.flavors, - parsed_args.flavor) + flavor = _find_flavor(compute_client, parsed_args.flavor) compute_client.flavors.delete(flavor.id) @@ -148,35 +167,38 @@ class ListFlavor(command.Lister): dest="public", action="store_true", default=True, - help="List only public flavors (default)", + help=_("List only public flavors (default)") ) public_group.add_argument( "--private", dest="public", action="store_false", - help="List only private flavors", + help=_("List only private flavors") ) public_group.add_argument( "--all", dest="all", action="store_true", default=False, - help="List all flavors, whether public or private", + help=_("List all flavors, whether public or private") ) parser.add_argument( '--long', action='store_true', default=False, - help='List additional fields in output') + help=_("List additional fields in output") + ) parser.add_argument( '--marker', metavar="<marker>", - help='The last flavor ID of the previous page') + help=_("The last flavor ID of the previous page") + ) parser.add_argument( '--limit', type=int, metavar="<limit>", - help='Maximum number of flavors to display') + help=_("Maximum number of flavors to display") + ) return parser def take_action(self, parsed_args): @@ -227,19 +249,19 @@ class SetFlavor(command.Command): "--property", metavar="<key=value>", action=parseractions.KeyValueAction, - help='Property to add or modify for this flavor ' - '(repeat option to set multiple properties)', + help=_("Property to add or modify for this flavor " + "(repeat option to set multiple properties)") ) parser.add_argument( "flavor", metavar="<flavor>", - help="Flavor to modify (name or ID)", + help=_("Flavor to modify (name or ID)") ) return parser def take_action(self, parsed_args): compute_client = self.app.client_manager.compute - flavor = compute_client.flavors.find(name=parsed_args.flavor) + flavor = _find_flavor(compute_client, parsed_args.flavor) flavor.set_keys(parsed_args.property) @@ -251,14 +273,13 @@ class ShowFlavor(command.ShowOne): parser.add_argument( "flavor", metavar="<flavor>", - help="Flavor to display (name or ID)", + help=_("Flavor to display (name or ID)") ) return parser def take_action(self, parsed_args): compute_client = self.app.client_manager.compute - resource_flavor = utils.find_resource(compute_client.flavors, - parsed_args.flavor) + resource_flavor = _find_flavor(compute_client, parsed_args.flavor) flavor = resource_flavor._info.copy() flavor.pop("links", None) @@ -276,18 +297,18 @@ class UnsetFlavor(command.Command): "--property", metavar="<key>", action='append', - help='Property to remove from flavor ' - '(repeat option to unset multiple properties)', required=True, + help=_("Property to remove from flavor " + "(repeat option to unset multiple properties)") ) parser.add_argument( "flavor", metavar="<flavor>", - help="Flavor to modify (name or ID)", + help=_("Flavor to modify (name or ID)") ) return parser def take_action(self, parsed_args): compute_client = self.app.client_manager.compute - flavor = compute_client.flavors.find(name=parsed_args.flavor) + flavor = _find_flavor(compute_client, parsed_args.flavor) flavor.unset_keys(parsed_args.property) diff --git a/openstackclient/compute/v2/floatingip.py b/openstackclient/compute/v2/floatingip.py index 6212989..fac4d2e 100644 --- a/openstackclient/compute/v2/floatingip.py +++ b/openstackclient/compute/v2/floatingip.py @@ -15,8 +15,6 @@ """Floating IP action implementations""" -import six - from openstackclient.common import command from openstackclient.common import utils @@ -47,27 +45,6 @@ class AddFloatingIP(command.Command): server.add_floating_ip(parsed_args.ip_address) -class CreateFloatingIP(command.ShowOne): - """Create new floating IP address""" - - def get_parser(self, prog_name): - parser = super(CreateFloatingIP, self).get_parser(prog_name) - parser.add_argument( - 'pool', - metavar='<pool>', - help='Pool to fetch IP address from (name or ID)', - ) - return parser - - def take_action(self, parsed_args): - compute_client = self.app.client_manager.compute - floating_ip = compute_client.floating_ips.create(parsed_args.pool) - - info = {} - info.update(floating_ip._info) - return zip(*sorted(six.iteritems(info))) - - class RemoveFloatingIP(command.Command): """Remove floating IP address from server""" diff --git a/openstackclient/compute/v2/host.py b/openstackclient/compute/v2/host.py index f2257d1..73e2cdf 100644 --- a/openstackclient/compute/v2/host.py +++ b/openstackclient/compute/v2/host.py @@ -17,6 +17,7 @@ from openstackclient.common import command from openstackclient.common import utils +from openstackclient.i18n import _ class ListHost(command.Lister): @@ -27,7 +28,8 @@ class ListHost(command.Lister): parser.add_argument( "--zone", metavar="<zone>", - help="Only return hosts in the availability zone.") + help=_("Only return hosts in the availability zone") + ) return parser def take_action(self, parsed_args): @@ -44,6 +46,63 @@ class ListHost(command.Lister): ) for s in data)) +class SetHost(command.Command): + """Set host properties""" + def get_parser(self, prog_name): + parser = super(SetHost, self).get_parser(prog_name) + parser.add_argument( + "host", + metavar="<host>", + help=_("The host to modify (name or ID)") + ) + status = parser.add_mutually_exclusive_group() + status.add_argument( + '--enable', + action='store_true', + help=_("Enable the host") + ) + status.add_argument( + '--disable', + action='store_true', + help=_("Disable the host") + ) + maintenance = parser.add_mutually_exclusive_group() + maintenance.add_argument( + '--enable-maintenance', + action='store_true', + help=_("Enable maintenance mode for the host") + ) + maintenance.add_argument( + '--disable-maintenance', + action='store_true', + help=_("Disable maintenance mode for the host") + ) + return parser + + def take_action(self, parsed_args): + kwargs = {} + + if parsed_args.enable: + kwargs['status'] = True + if parsed_args.disable: + kwargs['status'] = False + if parsed_args.enable_maintenance: + kwargs['maintenance_mode'] = True + if parsed_args.disable_maintenance: + kwargs['maintenance_mode'] = False + + compute_client = self.app.client_manager.compute + foundhost = utils.find_resource( + compute_client.hosts, + parsed_args.host + ) + + compute_client.hosts.update( + foundhost.id, + kwargs + ) + + class ShowHost(command.Lister): """Show host command""" @@ -52,7 +111,8 @@ class ShowHost(command.Lister): parser.add_argument( "host", metavar="<host>", - help="Name of host") + help=_("Name of host") + ) return parser def take_action(self, parsed_args): diff --git a/openstackclient/compute/v2/hypervisor.py b/openstackclient/compute/v2/hypervisor.py index f5288a3..333a7de 100644 --- a/openstackclient/compute/v2/hypervisor.py +++ b/openstackclient/compute/v2/hypervisor.py @@ -20,6 +20,7 @@ import six from openstackclient.common import command from openstackclient.common import utils +from openstackclient.i18n import _ class ListHypervisor(command.Lister): @@ -30,7 +31,7 @@ class ListHypervisor(command.Lister): parser.add_argument( "--matching", metavar="<hostname>", - help="Filter hypervisors using <hostname> substring", + help=_("Filter hypervisors using <hostname> substring") ) return parser @@ -60,7 +61,8 @@ class ShowHypervisor(command.ShowOne): parser.add_argument( "hypervisor", metavar="<hypervisor>", - help="Hypervisor to display (name or ID)") + help=_("Hypervisor to display (name or ID)") + ) return parser def take_action(self, parsed_args): diff --git a/openstackclient/compute/v2/keypair.py b/openstackclient/compute/v2/keypair.py index 71c9d67..8a58e8f 100644 --- a/openstackclient/compute/v2/keypair.py +++ b/openstackclient/compute/v2/keypair.py @@ -15,6 +15,7 @@ """Keypair action implementations""" +import io import os import six import sys @@ -22,6 +23,7 @@ import sys from openstackclient.common import command from openstackclient.common import exceptions from openstackclient.common import utils +from openstackclient.i18n import _ class CreateKeypair(command.ShowOne): @@ -32,12 +34,12 @@ class CreateKeypair(command.ShowOne): parser.add_argument( 'name', metavar='<name>', - help='New public key name', + help=_("New public key name") ) parser.add_argument( '--public-key', metavar='<file>', - help='Filename for public key to add', + help=_("Filename for public key to add") ) return parser @@ -47,12 +49,14 @@ class CreateKeypair(command.ShowOne): public_key = parsed_args.public_key if public_key: try: - with open(os.path.expanduser(parsed_args.public_key)) as p: + with io.open(os.path.expanduser(parsed_args.public_key)) as p: public_key = p.read() except IOError as e: - msg = "Key file %s not found: %s" - raise exceptions.CommandError(msg - % (parsed_args.public_key, e)) + msg = _("Key file %(public_key)s not found: %(exception)s") + raise exceptions.CommandError( + msg % {"public_key": parsed_args.public_key, + "exception": e} + ) keypair = compute_client.keypairs.create( parsed_args.name, @@ -80,7 +84,7 @@ class DeleteKeypair(command.Command): parser.add_argument( 'name', metavar='<key>', - help='Public key to delete', + help=_("Public key to delete") ) return parser @@ -114,13 +118,13 @@ class ShowKeypair(command.ShowOne): parser.add_argument( 'name', metavar='<key>', - help='Public key to display', + help=_("Public key to display") ) parser.add_argument( '--public-key', action='store_true', default=False, - help='Show only bare public key', + help=_("Show only bare public key") ) return parser diff --git a/openstackclient/compute/v2/security_group.py b/openstackclient/compute/v2/security_group.py deleted file mode 100644 index 907175f..0000000 --- a/openstackclient/compute/v2/security_group.py +++ /dev/null @@ -1,295 +0,0 @@ -# Copyright 2012 OpenStack Foundation -# Copyright 2013 Nebula Inc -# -# 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. -# - -"""Compute v2 Security Group action implementations""" - -import six - -try: - from novaclient.v2 import security_group_rules -except ImportError: - from novaclient.v1_1 import security_group_rules - -from openstackclient.common import command -from openstackclient.common import parseractions -from openstackclient.common import utils - - -def _xform_security_group_rule(sgroup): - info = {} - info.update(sgroup) - from_port = info.pop('from_port') - to_port = info.pop('to_port') - if isinstance(from_port, int) and isinstance(to_port, int): - port_range = {'port_range': "%u:%u" % (from_port, to_port)} - elif from_port is None and to_port is None: - port_range = {'port_range': ""} - else: - port_range = {'port_range': "%s:%s" % (from_port, to_port)} - info.update(port_range) - if 'cidr' in info['ip_range']: - info['ip_range'] = info['ip_range']['cidr'] - else: - info['ip_range'] = '' - if info['ip_protocol'] is None: - info['ip_protocol'] = '' - elif info['ip_protocol'].lower() == 'icmp': - info['port_range'] = '' - group = info.pop('group') - if 'name' in group: - info['remote_security_group'] = group['name'] - else: - info['remote_security_group'] = '' - return info - - -def _xform_and_trim_security_group_rule(sgroup): - info = _xform_security_group_rule(sgroup) - # Trim parent security group ID since caller has this information. - info.pop('parent_group_id', None) - # Trim keys with empty string values. - keys_to_trim = [ - 'ip_protocol', - 'ip_range', - 'port_range', - 'remote_security_group', - ] - for key in keys_to_trim: - if key in info and not info[key]: - info.pop(key) - return info - - -class CreateSecurityGroup(command.ShowOne): - """Create a new security group""" - - def get_parser(self, prog_name): - parser = super(CreateSecurityGroup, self).get_parser(prog_name) - parser.add_argument( - "name", - metavar="<name>", - help="New security group name", - ) - parser.add_argument( - "--description", - metavar="<description>", - help="Security group description", - ) - return parser - - def take_action(self, parsed_args): - compute_client = self.app.client_manager.compute - - description = parsed_args.description or parsed_args.name - - data = compute_client.security_groups.create( - parsed_args.name, - description, - ) - - info = {} - info.update(data._info) - return zip(*sorted(six.iteritems(info))) - - -class CreateSecurityGroupRule(command.ShowOne): - """Create a new security group rule""" - - def get_parser(self, prog_name): - parser = super(CreateSecurityGroupRule, self).get_parser(prog_name) - parser.add_argument( - 'group', - metavar='<group>', - help='Create rule in this security group (name or ID)', - ) - parser.add_argument( - "--proto", - metavar="<proto>", - default="tcp", - help="IP protocol (icmp, tcp, udp; default: tcp)", - ) - source_group = parser.add_mutually_exclusive_group() - source_group.add_argument( - "--src-ip", - metavar="<ip-address>", - default="0.0.0.0/0", - help="Source IP address block (may use CIDR notation; default: " - "0.0.0.0/0)", - ) - source_group.add_argument( - "--src-group", - metavar="<group>", - help="Source security group (ID only)", - ) - parser.add_argument( - "--dst-port", - metavar="<port-range>", - default=(0, 0), - action=parseractions.RangeAction, - help="Destination port, may be a range: 137:139 (default: 0; " - "only required for proto tcp and udp)", - ) - return parser - - def take_action(self, parsed_args): - compute_client = self.app.client_manager.compute - group = utils.find_resource( - compute_client.security_groups, - parsed_args.group, - ) - if parsed_args.proto.lower() == 'icmp': - from_port, to_port = -1, -1 - else: - from_port, to_port = parsed_args.dst_port - data = compute_client.security_group_rules.create( - group.id, - parsed_args.proto, - from_port, - to_port, - parsed_args.src_ip, - parsed_args.src_group, - ) - - info = _xform_security_group_rule(data._info) - return zip(*sorted(six.iteritems(info))) - - -class ListSecurityGroupRule(command.Lister): - """List security group rules""" - - def get_parser(self, prog_name): - parser = super(ListSecurityGroupRule, self).get_parser(prog_name) - parser.add_argument( - 'group', - metavar='<group>', - nargs='?', - help='List all rules in this security group (name or ID)', - ) - return parser - - def take_action(self, parsed_args): - compute_client = self.app.client_manager.compute - columns = column_headers = ( - "ID", - "IP Protocol", - "IP Range", - "Port Range", - "Remote Security Group", - ) - - rules_to_list = [] - if parsed_args.group: - group = utils.find_resource( - compute_client.security_groups, - parsed_args.group, - ) - rules_to_list = group.rules - else: - columns = columns + ('parent_group_id',) - column_headers = column_headers + ('Security Group',) - for group in compute_client.security_groups.list(): - rules_to_list.extend(group.rules) - - # Argh, the rules are not Resources... - rules = [] - for rule in rules_to_list: - rules.append(security_group_rules.SecurityGroupRule( - compute_client.security_group_rules, - _xform_security_group_rule(rule), - )) - - return (column_headers, - (utils.get_item_properties( - s, columns, - ) for s in rules)) - - -class SetSecurityGroup(command.Command): - """Set security group properties""" - - def get_parser(self, prog_name): - parser = super(SetSecurityGroup, self).get_parser(prog_name) - parser.add_argument( - 'group', - metavar='<group>', - help='Security group to modify (name or ID)', - ) - parser.add_argument( - '--name', - metavar='<new-name>', - help='New security group name', - ) - parser.add_argument( - "--description", - metavar="<description>", - help="New security group description", - ) - return parser - - def take_action(self, parsed_args): - compute_client = self.app.client_manager.compute - data = utils.find_resource( - compute_client.security_groups, - parsed_args.group, - ) - - if parsed_args.name: - data.name = parsed_args.name - if parsed_args.description: - data.description = parsed_args.description - - compute_client.security_groups.update( - data, - data.name, - data.description, - ) - - -class ShowSecurityGroup(command.ShowOne): - """Display security group details""" - - def get_parser(self, prog_name): - parser = super(ShowSecurityGroup, self).get_parser(prog_name) - parser.add_argument( - 'group', - metavar='<group>', - help='Security group to display (name or ID)', - ) - return parser - - def take_action(self, parsed_args): - - compute_client = self.app.client_manager.compute - info = {} - info.update(utils.find_resource( - compute_client.security_groups, - parsed_args.group, - )._info) - rules = [] - for r in info['rules']: - formatted_rule = _xform_and_trim_security_group_rule(r) - rules.append(utils.format_dict(formatted_rule)) - - # Format rules into a list of strings - info.update( - {'rules': utils.format_list(rules, separator='\n')} - ) - # Map 'tenant_id' column to 'project_id' - info.update( - {'project_id': info.pop('tenant_id')} - ) - - return zip(*sorted(six.iteritems(info))) diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index ca239c5..781ccb1 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -32,7 +32,7 @@ except ImportError: from openstackclient.common import exceptions from openstackclient.common import parseractions from openstackclient.common import utils -from openstackclient.i18n import _ # noqa +from openstackclient.i18n import _ from openstackclient.identity import common as identity_common @@ -102,9 +102,10 @@ def _get_ip_address(addresses, address_type, ip_address_family): if addy['OS-EXT-IPS:type'] == new_address_type: if int(addy['version']) in ip_address_family: return addy['addr'] + msg = _("ERROR: No %(type)s IP version %(family)s address found") raise exceptions.CommandError( - "ERROR: No %s IP version %s address found" % - (address_type, ip_address_family) + msg % {"type": address_type, + "family": ip_address_family} ) @@ -117,10 +118,7 @@ def _prep_server_detail(compute_client, server): """ info = server._info.copy() - # Call .get() to retrieve all of the server information - # as findall(name=blah) and REST /details are not the same - # and do not return flavor and image information. - server = compute_client.servers.get(info['id']) + server = utils.find_resource(compute_client.servers, info['id']) info.update(server._info) # Convert the image blob to a name @@ -155,12 +153,34 @@ def _prep_server_detail(compute_client, server): if 'tenant_id' in info: info['project_id'] = info.pop('tenant_id') + # Map power state num to meanful string + if 'OS-EXT-STS:power_state' in info: + info['OS-EXT-STS:power_state'] = _format_servers_list_power_state( + info['OS-EXT-STS:power_state']) + # Remove values that are long and not too useful info.pop('links', None) return info +def _prep_image_detail(image_client, image_id): + """Prepare the detailed image dict for printing + + :param image_client: an image client instance + :param image_id: id of image created + :rtype: a dict of image details + """ + + info = utils.find_resource( + image_client.images, + image_id, + ) + # Glance client V2 doesn't have _info attribute + # The following condition deals with it. + return getattr(info, "_info", info) + + def _show_progress(progress): if progress: sys.stdout.write('\rProgress: %s' % progress) @@ -276,7 +296,7 @@ class CreateServer(command.ShowOne): action='append', default=[], help=_('Security group to assign to this server (name or ID) ' - '(repeat for multiple groups)'), + '(repeat option to set multiple groups)'), ) parser.add_argument( '--key-name', @@ -288,7 +308,7 @@ class CreateServer(command.ShowOne): metavar='<key=value>', action=parseractions.KeyValueAction, help=_('Set a property on this server ' - '(repeat for multiple values)'), + '(repeat option to set multiple values)'), ) parser.add_argument( '--file', @@ -296,7 +316,7 @@ class CreateServer(command.ShowOne): action='append', default=[], help=_('File to inject into image before boot ' - '(repeat for multiple files)'), + '(repeat option to set multiple files)'), ) parser.add_argument( '--user-data', @@ -398,7 +418,11 @@ class CreateServer(command.ShowOne): try: files[dst] = io.open(src, 'rb') except IOError as e: - raise exceptions.CommandError("Can't open '%s': %s" % (src, e)) + msg = _("Can't open '%(source)s': %(exception)s") + raise exceptions.CommandError( + msg % {"source": src, + "exception": e} + ) if parsed_args.min > parsed_args.max: msg = _("min instances should be <= max instances") @@ -415,8 +439,11 @@ class CreateServer(command.ShowOne): try: userdata = io.open(parsed_args.user_data) except IOError as e: - msg = "Can't open '%s': %s" - raise exceptions.CommandError(msg % (parsed_args.user_data, e)) + msg = _("Can't open '%(data)s': %(exception)s") + raise exceptions.CommandError( + msg % {"data": parsed_args.user_data, + "exception": e} + ) block_device_mapping = {} if volume: @@ -617,17 +644,14 @@ class CreateServerImage(command.ShowOne): ): sys.stdout.write('\n') else: - self.log.error(_('Error creating server snapshot: %s'), - parsed_args.image_name) + self.log.error(_('Error creating snapshot of server: %s'), + parsed_args.server) sys.stdout.write(_('\nError creating server snapshot')) raise SystemExit - image = utils.find_resource( - image_client.images, - image_id, - ) + image = _prep_image_detail(image_client, image_id) - return zip(*sorted(six.iteritems(image._info))) + return zip(*sorted(six.iteritems(image))) class DeleteServer(command.Command): @@ -728,7 +752,8 @@ class ListServer(command.Lister): parser.add_argument( '--project', metavar='<project>', - help="Search by project (admin only) (name or ID)") + help=_("Search by project (admin only) (name or ID)") + ) identity_common.add_project_domain_option_to_parser(parser) parser.add_argument( '--user', @@ -746,19 +771,19 @@ class ListServer(command.Lister): '--marker', metavar='<marker>', default=None, - help=('The last server (name or ID) of the previous page. Display' - ' list of servers after marker. Display all servers if not' - ' specified.') + help=_('The last server (name or ID) of the previous page. Display' + ' list of servers after marker. Display all servers if not' + ' specified.') ) parser.add_argument( '--limit', metavar='<limit>', type=int, default=None, - help=("Maximum number of servers to display. If limit equals -1," - " all servers will be displayed. If limit is greater than" - " 'osapi_max_limit' option of Nova API," - " 'osapi_max_limit' will be used instead."), + help=_("Maximum number of servers to display. If limit equals -1," + " all servers will be displayed. If limit is greater than" + " 'osapi_max_limit' option of Nova API," + " 'osapi_max_limit' will be used instead."), ) return parser @@ -1085,7 +1110,7 @@ class RebuildServer(command.ShowOne): parser.add_argument( '--password', metavar='<password>', - help="Set the password on the rebuilt instance", + help=_("Set the password on the rebuilt instance"), ) parser.add_argument( '--wait', @@ -1273,6 +1298,28 @@ class ResizeServer(command.Command): compute_client.servers.revert_resize(server) +class RestoreServer(command.Command): + """Restore server(s)""" + + def get_parser(self, prog_name): + parser = super(RestoreServer, self).get_parser(prog_name) + parser.add_argument( + 'server', + metavar='<server>', + nargs='+', + help=_('Server(s) to restore (name or ID)'), + ) + return parser + + def take_action(self, parsed_args): + compute_client = self.app.client_manager.compute + for server in parsed_args.server: + utils.find_resource( + compute_client.servers, + server + ).restore() + + class ResumeServer(command.Command): """Resume server(s)""" @@ -1409,7 +1456,7 @@ class ShowServer(command.ShowOne): class SshServer(command.Command): - """Ssh to server""" + """SSH to server""" def get_parser(self, prog_name): parser = super(SshServer, self).get_parser(prog_name) @@ -1702,7 +1749,7 @@ class UnsetServer(command.Command): action='append', default=[], help=_('Property key to remove from server ' - '(repeat to unset multiple values)'), + '(repeat option to remove multiple values)'), ) return parser diff --git a/openstackclient/compute/v2/server_group.py b/openstackclient/compute/v2/server_group.py new file mode 100644 index 0000000..973095c --- /dev/null +++ b/openstackclient/compute/v2/server_group.py @@ -0,0 +1,186 @@ +# Copyright 2016 Huawei, Inc. 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. +# + +"""Compute v2 Server Group action implementations""" + +from openstackclient.common import command +from openstackclient.common import exceptions +from openstackclient.common import utils +from openstackclient.i18n import _ + + +_formatters = { + 'policies': utils.format_list, + 'members': utils.format_list, +} + + +def _get_columns(info): + columns = list(info.keys()) + if 'metadata' in columns: + # NOTE(RuiChen): The metadata of server group is always empty since API + # compatible, so hide it in order to avoid confusion. + columns.remove('metadata') + return tuple(sorted(columns)) + + +class CreateServerGroup(command.ShowOne): + """Create a new server group.""" + + def get_parser(self, prog_name): + parser = super(CreateServerGroup, self).get_parser(prog_name) + parser.add_argument( + 'name', + metavar='<name>', + help=_("New server group name") + ) + parser.add_argument( + '--policy', + metavar='<policy>', + action='append', + required=True, + help=_("Add a policy to <name> " + "(repeat option to add multiple policies)") + ) + return parser + + def take_action(self, parsed_args): + compute_client = self.app.client_manager.compute + info = {} + server_group = compute_client.server_groups.create( + name=parsed_args.name, + policies=parsed_args.policy) + info.update(server_group._info) + + columns = _get_columns(info) + data = utils.get_dict_properties(info, columns, + formatters=_formatters) + return columns, data + + +class DeleteServerGroup(command.Command): + """Delete an existing server group.""" + + def get_parser(self, prog_name): + parser = super(DeleteServerGroup, self).get_parser(prog_name) + parser.add_argument( + 'server_group', + metavar='<server-group>', + nargs='+', + help=_("server group(s) to delete (name or ID)") + ) + return parser + + def take_action(self, parsed_args): + compute_client = self.app.client_manager.compute + result = 0 + for group in parsed_args.server_group: + try: + group_obj = utils.find_resource(compute_client.server_groups, + group) + compute_client.server_groups.delete(group_obj.id) + # Catch all exceptions in order to avoid to block the next deleting + except Exception as e: + result += 1 + self.app.log.error(e) + + if result > 0: + total = len(parsed_args.server_group) + msg = _("%(result)s of %(total)s server groups failed to delete.") + raise exceptions.CommandError( + msg % {"result": result, + "total": total} + ) + + +class ListServerGroup(command.Lister): + """List all server groups.""" + + def get_parser(self, prog_name): + parser = super(ListServerGroup, self).get_parser(prog_name) + parser.add_argument( + '--all-projects', + action='store_true', + default=False, + help=_("Display information from all projects (admin only)") + ) + parser.add_argument( + '--long', + action='store_true', + default=False, + help=_("List additional fields in output") + ) + return parser + + def take_action(self, parsed_args): + compute_client = self.app.client_manager.compute + data = compute_client.server_groups.list(parsed_args.all_projects) + + if parsed_args.long: + column_headers = ( + 'ID', + 'Name', + 'Policies', + 'Members', + 'Project Id', + 'User Id', + ) + columns = ( + 'ID', + 'Name', + 'Policies', + 'Members', + 'Project Id', + 'User Id', + ) + else: + column_headers = columns = ( + 'ID', + 'Name', + 'Policies', + ) + + return (column_headers, + (utils.get_item_properties( + s, columns, + formatters={ + 'Policies': utils.format_list, + 'Members': utils.format_list, + } + ) for s in data)) + + +class ShowServerGroup(command.ShowOne): + """Display server group details.""" + + def get_parser(self, prog_name): + parser = super(ShowServerGroup, self).get_parser(prog_name) + parser.add_argument( + 'server_group', + metavar='<server-group>', + help=_("server group to display (name or ID)") + ) + return parser + + def take_action(self, parsed_args): + compute_client = self.app.client_manager.compute + group = utils.find_resource(compute_client.server_groups, + parsed_args.server_group) + info = {} + info.update(group._info) + columns = _get_columns(info) + data = utils.get_dict_properties(info, columns, + formatters=_formatters) + return columns, data diff --git a/openstackclient/compute/v2/service.py b/openstackclient/compute/v2/service.py index 3c06272..6093b8c 100644 --- a/openstackclient/compute/v2/service.py +++ b/openstackclient/compute/v2/service.py @@ -17,6 +17,7 @@ from openstackclient.common import command from openstackclient.common import utils +from openstackclient.i18n import _ class DeleteService(command.Command): @@ -27,7 +28,8 @@ class DeleteService(command.Command): parser.add_argument( "service", metavar="<service>", - help="Compute service to delete (ID only)") + help=_("Compute service to delete (ID only)") + ) return parser def take_action(self, parsed_args): @@ -44,24 +46,44 @@ class ListService(command.Lister): parser.add_argument( "--host", metavar="<host>", - help="Name of host") + help=_("List services on specified host (name only)") + ) parser.add_argument( "--service", metavar="<service>", - help="Name of service") + help=_("List only specified service (name only)") + ) + parser.add_argument( + "--long", + action="store_true", + default=False, + help=_("List additional fields in output") + ) return parser def take_action(self, parsed_args): compute_client = self.app.client_manager.compute - columns = ( - "Id", - "Binary", - "Host", - "Zone", - "Status", - "State", - "Updated At" - ) + if parsed_args.long: + columns = ( + "Id", + "Binary", + "Host", + "Zone", + "Status", + "State", + "Updated At", + "Disabled Reason" + ) + else: + columns = ( + "Id", + "Binary", + "Host", + "Zone", + "Status", + "State", + "Updated At" + ) data = compute_client.services.list(parsed_args.host, parsed_args.service) return (columns, @@ -78,31 +100,50 @@ class SetService(command.Command): parser.add_argument( "host", metavar="<host>", - help="Name of host") + help=_("Name of host") + ) parser.add_argument( "service", metavar="<service>", - help="Name of service") + help=_("Name of service") + ) enabled_group = parser.add_mutually_exclusive_group() enabled_group.add_argument( "--enable", dest="enabled", default=True, - help="Enable a service (default)", - action="store_true") + action="store_true", + help=_("Enable a service (default)") + ) enabled_group.add_argument( "--disable", dest="enabled", - help="Disable a service", - action="store_false") + action="store_false", + help=_("Disable a service") + ) + parser.add_argument( + "--disable-reason", + default=None, + metavar="<reason>", + help=_("Reason for disabling the service (in quotas). Note that " + "when the service is enabled, this option is ignored.") + ) return parser def take_action(self, parsed_args): compute_client = self.app.client_manager.compute - - if parsed_args.enabled: - action = compute_client.services.enable + cs = compute_client.services + + if not parsed_args.enabled: + if parsed_args.disable_reason: + cs.disable_log_reason(parsed_args.host, + parsed_args.service, + parsed_args.disable_reason) + else: + cs.disable(parsed_args.host, parsed_args.service) else: - action = compute_client.services.disable + if parsed_args.disable_reason: + msg = _("argument --disable-reason has been ignored") + self.log.info(msg) - action(parsed_args.host, parsed_args.service) + cs.enable(parsed_args.host, parsed_args.service) diff --git a/openstackclient/compute/v2/usage.py b/openstackclient/compute/v2/usage.py index baa5017..b83bef1 100644 --- a/openstackclient/compute/v2/usage.py +++ b/openstackclient/compute/v2/usage.py @@ -22,6 +22,7 @@ import six from openstackclient.common import command from openstackclient.common import utils +from openstackclient.i18n import _ class ListUsage(command.Lister): @@ -33,14 +34,14 @@ class ListUsage(command.Lister): "--start", metavar="<start>", default=None, - help="Usage range start date, ex 2012-01-20" - " (default: 4 weeks ago)" + help=_("Usage range start date, ex 2012-01-20" + " (default: 4 weeks ago)") ) parser.add_argument( "--end", metavar="<end>", default=None, - help="Usage range end date, ex 2012-01-20 (default: tomorrow)" + help=_("Usage range end date, ex 2012-01-20 (default: tomorrow)") ) return parser @@ -95,10 +96,10 @@ class ListUsage(command.Lister): pass if parsed_args.formatter == 'table' and len(usage_list) > 0: - sys.stdout.write("Usage from %s to %s: \n" % ( - start.strftime(dateformat), - end.strftime(dateformat), - )) + sys.stdout.write(_("Usage from %(start)s to %(end)s: \n") % { + "start": start.strftime(dateformat), + "end": end.strftime(dateformat), + }) return (column_headers, (utils.get_item_properties( @@ -122,20 +123,20 @@ class ShowUsage(command.ShowOne): "--project", metavar="<project>", default=None, - help="Name or ID of project to show usage for" + help=_("Name or ID of project to show usage for") ) parser.add_argument( "--start", metavar="<start>", default=None, - help="Usage range start date, ex 2012-01-20" - " (default: 4 weeks ago)" + help=_("Usage range start date, ex 2012-01-20" + " (default: 4 weeks ago)") ) parser.add_argument( "--end", metavar="<end>", default=None, - help="Usage range end date, ex 2012-01-20 (default: tomorrow)" + help=_("Usage range end date, ex 2012-01-20 (default: tomorrow)") ) return parser @@ -167,11 +168,12 @@ class ShowUsage(command.ShowOne): usage = compute_client.usage.get(project, start, end) if parsed_args.formatter == 'table': - sys.stdout.write("Usage from %s to %s on project %s: \n" % ( - start.strftime(dateformat), - end.strftime(dateformat), - project - )) + sys.stdout.write(_("Usage from %(start)s to %(end)s on " + "project %(project)s: \n") % { + "start": start.strftime(dateformat), + "end": end.strftime(dateformat), + "project": project, + }) info = {} info['Servers'] = ( diff --git a/openstackclient/identity/v2_0/catalog.py b/openstackclient/identity/v2_0/catalog.py index c927943..53a6fe3 100644 --- a/openstackclient/identity/v2_0/catalog.py +++ b/openstackclient/identity/v2_0/catalog.py @@ -17,7 +17,7 @@ import six from openstackclient.common import command from openstackclient.common import utils -from openstackclient.i18n import _ # noqa +from openstackclient.i18n import _ def _format_endpoints(eps=None): @@ -25,7 +25,9 @@ def _format_endpoints(eps=None): return "" ret = '' for index, ep in enumerate(eps): - region = eps[index].get('region', '<none>') + region = eps[index].get('region') + if region is None: + region = '<none>' ret += region + '\n' for endpoint_type in ['publicURL', 'internalURL', 'adminURL']: url = eps[index].get(endpoint_type) diff --git a/openstackclient/identity/v2_0/ec2creds.py b/openstackclient/identity/v2_0/ec2creds.py index a16b3d9..dfd6759 100644 --- a/openstackclient/identity/v2_0/ec2creds.py +++ b/openstackclient/identity/v2_0/ec2creds.py @@ -20,7 +20,7 @@ import six from openstackclient.common import command from openstackclient.common import utils -from openstackclient.i18n import _ # noqa +from openstackclient.i18n import _ class CreateEC2Creds(command.ShowOne): diff --git a/openstackclient/identity/v2_0/endpoint.py b/openstackclient/identity/v2_0/endpoint.py index 08c0956..e515fc9 100644 --- a/openstackclient/identity/v2_0/endpoint.py +++ b/openstackclient/identity/v2_0/endpoint.py @@ -19,7 +19,7 @@ import six from openstackclient.common import command from openstackclient.common import utils -from openstackclient.i18n import _ # noqa +from openstackclient.i18n import _ from openstackclient.identity import common @@ -87,7 +87,6 @@ class DeleteEndpoint(command.Command): def take_action(self, parsed_args): identity_client = self.app.client_manager.identity identity_client.endpoints.delete(parsed_args.endpoint) - return class ListEndpoint(command.Lister): diff --git a/openstackclient/identity/v2_0/project.py b/openstackclient/identity/v2_0/project.py index 9e26c30..d90162c 100644 --- a/openstackclient/identity/v2_0/project.py +++ b/openstackclient/identity/v2_0/project.py @@ -22,7 +22,7 @@ from keystoneauth1 import exceptions as ks_exc from openstackclient.common import command from openstackclient.common import parseractions from openstackclient.common import utils -from openstackclient.i18n import _ # noqa +from openstackclient.i18n import _ class CreateProject(command.ShowOne): @@ -119,7 +119,6 @@ class DeleteProject(command.Command): project, ) identity_client.tenants.delete(project_obj.id) - return class ListProject(command.Lister): @@ -222,7 +221,6 @@ class SetProject(command.Command): del kwargs['name'] identity_client.tenants.update(project.id, **kwargs) - return class ShowProject(command.ShowOne): @@ -317,4 +315,3 @@ class UnsetProject(command.Command): if key in kwargs: kwargs[key] = None identity_client.tenants.update(project.id, **kwargs) - return diff --git a/openstackclient/identity/v2_0/role.py b/openstackclient/identity/v2_0/role.py index 892ce00..1fcee15 100644 --- a/openstackclient/identity/v2_0/role.py +++ b/openstackclient/identity/v2_0/role.py @@ -22,7 +22,7 @@ from keystoneauth1 import exceptions as ks_exc from openstackclient.common import command from openstackclient.common import exceptions from openstackclient.common import utils -from openstackclient.i18n import _ # noqa +from openstackclient.i18n import _ class AddRole(command.ShowOne): @@ -126,7 +126,6 @@ class DeleteRole(command.Command): role, ) identity_client.roles.delete(role_obj.id) - return class ListRole(command.Lister): diff --git a/openstackclient/identity/v2_0/service.py b/openstackclient/identity/v2_0/service.py index 3af85d7..7fe66d9 100644 --- a/openstackclient/identity/v2_0/service.py +++ b/openstackclient/identity/v2_0/service.py @@ -21,7 +21,7 @@ import six from openstackclient.common import command from openstackclient.common import exceptions from openstackclient.common import utils -from openstackclient.i18n import _ # noqa +from openstackclient.i18n import _ from openstackclient.identity import common @@ -101,7 +101,6 @@ class DeleteService(command.Command): identity_client = self.app.client_manager.identity service = common.find_service(identity_client, parsed_args.service) identity_client.services.delete(service.id) - return class ListService(command.Lister): diff --git a/openstackclient/identity/v2_0/token.py b/openstackclient/identity/v2_0/token.py index 6a66a1c..f435d7c 100644 --- a/openstackclient/identity/v2_0/token.py +++ b/openstackclient/identity/v2_0/token.py @@ -18,7 +18,7 @@ import six from openstackclient.common import command -from openstackclient.i18n import _ # noqa +from openstackclient.i18n import _ class IssueToken(command.ShowOne): @@ -55,4 +55,3 @@ class RevokeToken(command.Command): identity_client = self.app.client_manager.identity identity_client.tokens.delete(parsed_args.token) - return diff --git a/openstackclient/identity/v2_0/user.py b/openstackclient/identity/v2_0/user.py index 3d84873..bc9bf83 100644 --- a/openstackclient/identity/v2_0/user.py +++ b/openstackclient/identity/v2_0/user.py @@ -21,7 +21,7 @@ from keystoneauth1 import exceptions as ks_exc from openstackclient.common import command from openstackclient.common import utils -from openstackclient.i18n import _ # noqa +from openstackclient.i18n import _ class CreateUser(command.ShowOne): @@ -143,7 +143,6 @@ class DeleteUser(command.Command): user, ) identity_client.users.delete(user_obj.id) - return class ListUser(command.Lister): @@ -330,7 +329,6 @@ class SetUser(command.Command): kwargs['enabled'] = False identity_client.users.update(user.id, **kwargs) - return class ShowUser(command.ShowOne): diff --git a/openstackclient/identity/v3/catalog.py b/openstackclient/identity/v3/catalog.py index 795f5d5..78d71f5 100644 --- a/openstackclient/identity/v3/catalog.py +++ b/openstackclient/identity/v3/catalog.py @@ -17,7 +17,7 @@ import six from openstackclient.common import command from openstackclient.common import utils -from openstackclient.i18n import _ # noqa +from openstackclient.i18n import _ def _format_endpoints(eps=None): @@ -25,7 +25,7 @@ def _format_endpoints(eps=None): return "" ret = '' for ep in eps: - region = ep.get('region_id') or ep.get('region', '<none>') + region = ep.get('region_id') or ep.get('region') or '<none>' ret += region + '\n' ret += " %s: %s\n" % (ep['interface'], ep['url']) return ret diff --git a/openstackclient/identity/v3/domain.py b/openstackclient/identity/v3/domain.py index bf248fa..7fcab4f 100644 --- a/openstackclient/identity/v3/domain.py +++ b/openstackclient/identity/v3/domain.py @@ -22,7 +22,7 @@ from keystoneauth1 import exceptions as ks_exc from openstackclient.common import command from openstackclient.common import utils -from openstackclient.i18n import _ # noqa +from openstackclient.i18n import _ class CreateDomain(command.ShowOne): diff --git a/openstackclient/identity/v3/ec2creds.py b/openstackclient/identity/v3/ec2creds.py index 777c043..a12b2d3 100644 --- a/openstackclient/identity/v3/ec2creds.py +++ b/openstackclient/identity/v3/ec2creds.py @@ -16,7 +16,7 @@ import six from openstackclient.common import command from openstackclient.common import utils -from openstackclient.i18n import _ # noqa +from openstackclient.i18n import _ from openstackclient.identity import common diff --git a/openstackclient/identity/v3/group.py b/openstackclient/identity/v3/group.py index a4cdd58..3c24353 100644 --- a/openstackclient/identity/v3/group.py +++ b/openstackclient/identity/v3/group.py @@ -22,7 +22,7 @@ from keystoneauth1 import exceptions as ks_exc from openstackclient.common import command from openstackclient.common import utils -from openstackclient.i18n import _ # noqa +from openstackclient.i18n import _ from openstackclient.identity import common diff --git a/openstackclient/identity/v3/identity_provider.py b/openstackclient/identity/v3/identity_provider.py index 37f79ed..39f440f 100644 --- a/openstackclient/identity/v3/identity_provider.py +++ b/openstackclient/identity/v3/identity_provider.py @@ -35,7 +35,7 @@ class CreateIdentityProvider(command.ShowOne): metavar='<remote-id>', action='append', help='Remote IDs to associate with the Identity Provider ' - '(repeat to provide multiple values)' + '(repeat option to provide multiple values)' ) identity_remote_id_provider.add_argument( '--remote-id-file', @@ -139,7 +139,7 @@ class SetIdentityProvider(command.Command): metavar='<remote-id>', action='append', help='Remote IDs to associate with the Identity Provider ' - '(repeat to provide multiple values)' + '(repeat option to provide multiple values)' ) identity_remote_id_provider.add_argument( '--remote-id-file', @@ -214,7 +214,8 @@ class ShowIdentityProvider(command.ShowOne): identity_client = self.app.client_manager.identity idp = utils.find_resource( identity_client.federation.identity_providers, - parsed_args.identity_provider) + parsed_args.identity_provider, + id=parsed_args.identity_provider) idp._info.pop('links', None) remote_ids = utils.format_list(idp._info.pop('remote_ids', [])) diff --git a/openstackclient/identity/v3/project.py b/openstackclient/identity/v3/project.py index a379c6f..4990b1b 100644 --- a/openstackclient/identity/v3/project.py +++ b/openstackclient/identity/v3/project.py @@ -22,7 +22,7 @@ from keystoneauth1 import exceptions as ks_exc from openstackclient.common import command from openstackclient.common import parseractions from openstackclient.common import utils -from openstackclient.i18n import _ # noqa +from openstackclient.i18n import _ from openstackclient.identity import common diff --git a/openstackclient/identity/v3/region.py b/openstackclient/identity/v3/region.py index ec50422..053e4b3 100644 --- a/openstackclient/identity/v3/region.py +++ b/openstackclient/identity/v3/region.py @@ -17,7 +17,7 @@ import six from openstackclient.common import command from openstackclient.common import utils -from openstackclient.i18n import _ # noqa +from openstackclient.i18n import _ class CreateRegion(command.ShowOne): diff --git a/openstackclient/identity/v3/role.py b/openstackclient/identity/v3/role.py index 1195ab2..f93c9d8 100644 --- a/openstackclient/identity/v3/role.py +++ b/openstackclient/identity/v3/role.py @@ -22,7 +22,7 @@ from keystoneauth1 import exceptions as ks_exc from openstackclient.common import command from openstackclient.common import utils -from openstackclient.i18n import _ # noqa +from openstackclient.i18n import _ from openstackclient.identity import common diff --git a/openstackclient/identity/v3/service.py b/openstackclient/identity/v3/service.py index 355583c..f43ada5 100644 --- a/openstackclient/identity/v3/service.py +++ b/openstackclient/identity/v3/service.py @@ -82,7 +82,7 @@ class DeleteService(command.Command): parser.add_argument( 'service', metavar='<service>', - help='Service to delete (type or ID)', + help='Service to delete (type, name or ID)', ) return parser diff --git a/openstackclient/identity/v3/service_provider.py b/openstackclient/identity/v3/service_provider.py index e3a22eb..8b433b4 100644 --- a/openstackclient/identity/v3/service_provider.py +++ b/openstackclient/identity/v3/service_provider.py @@ -192,7 +192,8 @@ class ShowServiceProvider(command.ShowOne): service_client = self.app.client_manager.identity service_provider = utils.find_resource( service_client.federation.service_providers, - parsed_args.service_provider) + parsed_args.service_provider, + id=parsed_args.service_provider) service_provider._info.pop('links', None) return zip(*sorted(six.iteritems(service_provider._info))) diff --git a/openstackclient/identity/v3/token.py b/openstackclient/identity/v3/token.py index bf039d2..bdc5e95 100644 --- a/openstackclient/identity/v3/token.py +++ b/openstackclient/identity/v3/token.py @@ -18,6 +18,7 @@ import six from openstackclient.common import command +from openstackclient.common import exceptions from openstackclient.common import utils from openstackclient.identity import common @@ -39,7 +40,7 @@ class AuthorizeRequestToken(command.ShowOne): action='append', default=[], help='Roles to authorize (name or ID) ' - '(repeat to set multiple values) (required)', + '(repeat option to set multiple values, required)', required=True ) return parser @@ -172,6 +173,9 @@ class IssueToken(command.ShowOne): return parser def take_action(self, parsed_args): + if not self.app.client_manager.auth_ref: + raise exceptions.AuthorizationFailure( + "Only an authorized user may issue a new token.") token = self.app.client_manager.auth_ref.service_catalog.get_token() if 'tenant_id' in token: token['project_id'] = token.pop('tenant_id') diff --git a/openstackclient/identity/v3/trust.py b/openstackclient/identity/v3/trust.py index 26fb833..b6f5c6b 100644 --- a/openstackclient/identity/v3/trust.py +++ b/openstackclient/identity/v3/trust.py @@ -48,7 +48,7 @@ class CreateTrust(command.ShowOne): action='append', default=[], help='Roles to authorize (name or ID) ' - '(repeat to set multiple values) (required)', + '(repeat option to set multiple values, required)', required=True ) parser.add_argument( diff --git a/openstackclient/identity/v3/user.py b/openstackclient/identity/v3/user.py index 93b3309..9a7ced9 100644 --- a/openstackclient/identity/v3/user.py +++ b/openstackclient/identity/v3/user.py @@ -23,7 +23,7 @@ from keystoneauth1 import exceptions as ks_exc from openstackclient.common import command from openstackclient.common import utils -from openstackclient.i18n import _ # noqa +from openstackclient.i18n import _ from openstackclient.identity import common diff --git a/openstackclient/image/client.py b/openstackclient/image/client.py index 8dd146e..9c45a63 100644 --- a/openstackclient/image/client.py +++ b/openstackclient/image/client.py @@ -16,6 +16,7 @@ import logging from openstackclient.common import utils +from openstackclient.i18n import _ LOG = logging.getLogger(__name__) @@ -82,7 +83,7 @@ def build_option_parser(parser): '--os-image-api-version', metavar='<image-api-version>', default=utils.env('OS_IMAGE_API_VERSION'), - help='Image API version, default=' + - DEFAULT_API_VERSION + - ' (Env: OS_IMAGE_API_VERSION)') + help=_('Image API version, default=%s (Env: OS_IMAGE_API_VERSION)') % + DEFAULT_API_VERSION, + ) return parser diff --git a/openstackclient/image/v1/image.py b/openstackclient/image/v1/image.py index 0d08559..e2109fc 100644 --- a/openstackclient/image/v1/image.py +++ b/openstackclient/image/v1/image.py @@ -31,7 +31,7 @@ from openstackclient.api import utils as api_utils from openstackclient.common import command from openstackclient.common import parseractions from openstackclient.common import utils -from openstackclient.i18n import _ # noqa +from openstackclient.i18n import _ DEFAULT_CONTAINER_FORMAT = 'bare' @@ -61,112 +61,112 @@ class CreateImage(command.ShowOne): parser.add_argument( "name", metavar="<image-name>", - help="New image name", + help=_("New image name"), ) parser.add_argument( "--id", metavar="<id>", - help="Image ID to reserve", + help=_("Image ID to reserve"), ) parser.add_argument( "--store", metavar="<store>", - help="Upload image to this store", + help=_("Upload image to this store"), ) parser.add_argument( "--container-format", default=DEFAULT_CONTAINER_FORMAT, metavar="<container-format>", - help="Image container format " - "(default: %s)" % DEFAULT_CONTAINER_FORMAT, + help=_("Image container format " + "(default: %s)") % DEFAULT_CONTAINER_FORMAT, ) parser.add_argument( "--disk-format", default=DEFAULT_DISK_FORMAT, metavar="<disk-format>", - help="Image disk format " - "(default: %s)" % DEFAULT_DISK_FORMAT, + help=_("Image disk format " + "(default: %s)") % DEFAULT_DISK_FORMAT, ) parser.add_argument( "--size", metavar="<size>", - help="Image size, in bytes (only used with --location and" - " --copy-from)", + help=_("Image size, in bytes (only used with --location and" + " --copy-from)"), ) parser.add_argument( "--min-disk", metavar="<disk-gb>", type=int, - help="Minimum disk size needed to boot image, in gigabytes", + help=_("Minimum disk size needed to boot image, in gigabytes"), ) parser.add_argument( "--min-ram", metavar="<ram-mb>", type=int, - help="Minimum RAM size needed to boot image, in megabytes", + help=_("Minimum RAM size needed to boot image, in megabytes"), ) parser.add_argument( "--location", metavar="<image-url>", - help="Download image from an existing URL", + help=_("Download image from an existing URL"), ) parser.add_argument( "--copy-from", metavar="<image-url>", - help="Copy image from the data store (similar to --location)", + help=_("Copy image from the data store (similar to --location)"), ) parser.add_argument( "--file", metavar="<file>", - help="Upload image from local file", + help=_("Upload image from local file"), ) parser.add_argument( "--volume", metavar="<volume>", - help="Create image from a volume", + help=_("Create image from a volume"), ) parser.add_argument( "--force", dest='force', action='store_true', default=False, - help="Force image creation if volume is in use " - "(only meaningful with --volume)", + help=_("Force image creation if volume is in use " + "(only meaningful with --volume)"), ) parser.add_argument( "--checksum", metavar="<checksum>", - help="Image hash used for verification", + help=_("Image hash used for verification"), ) protected_group = parser.add_mutually_exclusive_group() protected_group.add_argument( "--protected", action="store_true", - help="Prevent image from being deleted", + help=_("Prevent image from being deleted"), ) protected_group.add_argument( "--unprotected", action="store_true", - help="Allow image to be deleted (default)", + help=_("Allow image to be deleted (default)"), ) public_group = parser.add_mutually_exclusive_group() public_group.add_argument( "--public", action="store_true", - help="Image is accessible to the public", + help=_("Image is accessible to the public"), ) public_group.add_argument( "--private", action="store_true", - help="Image is inaccessible to the public (default)", + help=_("Image is inaccessible to the public (default)"), ) parser.add_argument( "--property", dest="properties", metavar="<key=value>", action=parseractions.KeyValueAction, - help="Set a property on this image " - "(repeat option to set multiple properties)", + help=_("Set a property on this image " + "(repeat option to set multiple properties)"), ) # NOTE(dtroyer): --owner is deprecated in Jan 2016 in an early # 2.x release. Do not remove before Jan 2017 @@ -175,7 +175,7 @@ class CreateImage(command.ShowOne): project_group.add_argument( "--project", metavar="<project>", - help="Set an alternate project on this image (name or ID)", + help=_("Set an alternate project on this image (name or ID)"), ) project_group.add_argument( "--owner", @@ -282,7 +282,7 @@ class DeleteImage(command.Command): "images", metavar="<image>", nargs="+", - help="Image(s) to delete (name or ID)", + help=_("Image(s) to delete (name or ID)"), ) return parser @@ -307,14 +307,14 @@ class ListImage(command.Lister): dest="public", action="store_true", default=False, - help="List only public images", + help=_("List only public images"), ) public_group.add_argument( "--private", dest="private", action="store_true", default=False, - help="List only private images", + help=_("List only private images"), ) # Included for silent CLI compatibility with v2 public_group.add_argument( @@ -328,13 +328,13 @@ class ListImage(command.Lister): '--property', metavar='<key=value>', action=parseractions.KeyValueAction, - help='Filter output based on property', + help=_('Filter output based on property'), ) parser.add_argument( '--long', action='store_true', default=False, - help='List additional fields in output', + help=_('List additional fields in output'), ) # --page-size has never worked, leave here for silent compatibility @@ -347,9 +347,9 @@ class ListImage(command.Lister): parser.add_argument( '--sort', metavar="<key>[:<direction>]", - help="Sort output by selected keys and directions(asc or desc) " - "(default: asc), multiple keys and directions can be " - "specified separated by comma", + help=_("Sort output by selected keys and directions(asc or desc) " + "(default: asc), multiple keys and directions can be " + "specified separated by comma"), ) return parser @@ -442,12 +442,12 @@ class SaveImage(command.Command): parser.add_argument( "--file", metavar="<filename>", - help="Downloaded image save filename (default: stdout)", + help=_("Downloaded image save filename (default: stdout)"), ) parser.add_argument( "image", metavar="<image>", - help="Image to save (name or ID)", + help=_("Image to save (name or ID)"), ) return parser @@ -470,31 +470,31 @@ class SetImage(command.Command): parser.add_argument( "image", metavar="<image>", - help="Image to modify (name or ID)", + help=_("Image to modify (name or ID)"), ) parser.add_argument( "--name", metavar="<name>", - help="New image name", + help=_("New image name"), ) parser.add_argument( "--min-disk", metavar="<disk-gb>", type=int, - help="Minimum disk size needed to boot image, in gigabytes", + help=_("Minimum disk size needed to boot image, in gigabytes"), ) parser.add_argument( "--min-ram", metavar="<disk-ram>", type=int, - help="Minimum RAM size needed to boot image, in megabytes", + help=_("Minimum RAM size needed to boot image, in megabytes"), ) container_choices = ["ami", "ari", "aki", "bare", "ovf"] parser.add_argument( "--container-format", metavar="<container-format>", - help=("Container format of image. Acceptable formats: %s" % - container_choices), + help=_("Container format of image. Acceptable formats: %s") % + container_choices, choices=container_choices ) disk_choices = ["ami", "ari", "aki", "vhd", "vmdk", "raw", "qcow2", @@ -502,89 +502,90 @@ class SetImage(command.Command): parser.add_argument( "--disk-format", metavar="<disk-format>", - help="Disk format of image. Acceptable formats: %s" % disk_choices, + help=_("Disk format of image. Acceptable formats: %s") % + disk_choices, choices=disk_choices ) parser.add_argument( "--size", metavar="<size>", type=int, - help="Size of image data (in bytes)" + help=_("Size of image data (in bytes)") ) protected_group = parser.add_mutually_exclusive_group() protected_group.add_argument( "--protected", action="store_true", - help="Prevent image from being deleted", + help=_("Prevent image from being deleted"), ) protected_group.add_argument( "--unprotected", action="store_true", - help="Allow image to be deleted (default)", + help=_("Allow image to be deleted (default)"), ) public_group = parser.add_mutually_exclusive_group() public_group.add_argument( "--public", action="store_true", - help="Image is accessible to the public", + help=_("Image is accessible to the public"), ) public_group.add_argument( "--private", action="store_true", - help="Image is inaccessible to the public (default)", + help=_("Image is inaccessible to the public (default)"), ) parser.add_argument( "--property", dest="properties", metavar="<key=value>", action=parseractions.KeyValueAction, - help="Set a property on this image " - "(repeat option to set multiple properties)", + help=_("Set a property on this image " + "(repeat option to set multiple properties)"), ) parser.add_argument( "--store", metavar="<store>", - help="Upload image to this store", + help=_("Upload image to this store"), ) parser.add_argument( "--location", metavar="<image-url>", - help="Download image from an existing URL", + help=_("Download image from an existing URL"), ) parser.add_argument( "--copy-from", metavar="<image-url>", - help="Copy image from the data store (similar to --location)", + help=_("Copy image from the data store (similar to --location)"), ) parser.add_argument( "--file", metavar="<file>", - help="Upload image from local file", + help=_("Upload image from local file"), ) parser.add_argument( "--volume", metavar="<volume>", - help="Create image from a volume", + help=_("Create image from a volume"), ) parser.add_argument( "--force", dest='force', action='store_true', default=False, - help="Force image change if volume is in use " - "(only meaningful with --volume)", + help=_("Force image change if volume is in use " + "(only meaningful with --volume)"), ) parser.add_argument( "--stdin", dest='stdin', action='store_true', default=False, - help="Read image data from standard input", + help=_("Read image data from standard input"), ) parser.add_argument( "--checksum", metavar="<checksum>", - help="Image hash used for verification", + help=_("Image hash used for verification"), ) # NOTE(dtroyer): --owner is deprecated in Jan 2016 in an early # 2.x release. Do not remove before Jan 2017 @@ -593,7 +594,7 @@ class SetImage(command.Command): project_group.add_argument( "--project", metavar="<project>", - help="Set an alternate project on this image (name or ID)", + help=_("Set an alternate project on this image (name or ID)"), ) project_group.add_argument( "--owner", @@ -682,8 +683,9 @@ class SetImage(command.Command): # will do a chunked transfer kwargs["data"] = sys.stdin else: - self.log.warning('Use --stdin to enable read image' - ' data from standard input') + self.log.warning(_('Use --stdin to enable read ' + 'image data from standard ' + 'input')) if image.properties and parsed_args.properties: image.properties.update(kwargs['properties']) @@ -709,7 +711,7 @@ class ShowImage(command.ShowOne): parser.add_argument( "image", metavar="<image>", - help="Image to display (name or ID)", + help=_("Image to display (name or ID)"), ) return parser diff --git a/openstackclient/image/v2/image.py b/openstackclient/image/v2/image.py index 3f16218..a9c0f1f 100644 --- a/openstackclient/image/v2/image.py +++ b/openstackclient/image/v2/image.py @@ -25,7 +25,7 @@ from openstackclient.common import command from openstackclient.common import exceptions from openstackclient.common import parseractions from openstackclient.common import utils -from openstackclient.i18n import _ # noqa +from openstackclient.i18n import _ from openstackclient.identity import common @@ -72,12 +72,12 @@ class AddProjectToImage(command.ShowOne): parser.add_argument( "image", metavar="<image>", - help="Image to share (name or ID)", + help=_("Image to share (name or ID)"), ) parser.add_argument( "project", metavar="<project>", - help="Project to associate with image (name or ID)", + help=_("Project to associate with image (name or ID)"), ) common.add_project_domain_option_to_parser(parser) return parser @@ -119,94 +119,94 @@ class CreateImage(command.ShowOne): parser.add_argument( "name", metavar="<image-name>", - help="New image name", + help=_("New image name"), ) parser.add_argument( "--id", metavar="<id>", - help="Image ID to reserve", + help=_("Image ID to reserve"), ) parser.add_argument( "--container-format", default=DEFAULT_CONTAINER_FORMAT, metavar="<container-format>", - help="Image container format " - "(default: %s)" % DEFAULT_CONTAINER_FORMAT, + help=_("Image container format " + "(default: %s)") % DEFAULT_CONTAINER_FORMAT, ) parser.add_argument( "--disk-format", default=DEFAULT_DISK_FORMAT, metavar="<disk-format>", - help="Image disk format " - "(default: %s)" % DEFAULT_DISK_FORMAT, + help=_("Image disk format " + "(default: %s)") % DEFAULT_DISK_FORMAT, ) parser.add_argument( "--min-disk", metavar="<disk-gb>", type=int, - help="Minimum disk size needed to boot image, in gigabytes", + help=_("Minimum disk size needed to boot image, in gigabytes"), ) parser.add_argument( "--min-ram", metavar="<ram-mb>", type=int, - help="Minimum RAM size needed to boot image, in megabytes", + help=_("Minimum RAM size needed to boot image, in megabytes"), ) parser.add_argument( "--file", metavar="<file>", - help="Upload image from local file", + help=_("Upload image from local file"), ) parser.add_argument( "--volume", metavar="<volume>", - help="Create image from a volume", + help=_("Create image from a volume"), ) parser.add_argument( "--force", dest='force', action='store_true', default=False, - help="Force image creation if volume is in use " - "(only meaningful with --volume)", + help=_("Force image creation if volume is in use " + "(only meaningful with --volume)"), ) protected_group = parser.add_mutually_exclusive_group() protected_group.add_argument( "--protected", action="store_true", - help="Prevent image from being deleted", + help=_("Prevent image from being deleted"), ) protected_group.add_argument( "--unprotected", action="store_true", - help="Allow image to be deleted (default)", + help=_("Allow image to be deleted (default)"), ) public_group = parser.add_mutually_exclusive_group() public_group.add_argument( "--public", action="store_true", - help="Image is accessible to the public", + help=_("Image is accessible to the public"), ) public_group.add_argument( "--private", action="store_true", - help="Image is inaccessible to the public (default)", + help=_("Image is inaccessible to the public (default)"), ) parser.add_argument( "--property", dest="properties", metavar="<key=value>", action=parseractions.KeyValueAction, - help="Set a property on this image " - "(repeat option to set multiple properties)", + help=_("Set a property on this image " + "(repeat option to set multiple properties)"), ) parser.add_argument( "--tag", dest="tags", metavar="<tag>", action='append', - help="Set a tag on this image " - "(repeat option to set multiple tags)", + help=_("Set a tag on this image " + "(repeat option to set multiple tags)"), ) # NOTE(dtroyer): --owner is deprecated in Jan 2016 in an early # 2.x release. Do not remove before Jan 2017 @@ -215,7 +215,7 @@ class CreateImage(command.ShowOne): project_group.add_argument( "--project", metavar="<project>", - help="Set an alternate project on this image (name or ID)", + help=_("Set an alternate project on this image (name or ID)"), ) project_group.add_argument( "--owner", @@ -239,8 +239,8 @@ class CreateImage(command.ShowOne): for deadopt in self.deadopts: if getattr(parsed_args, deadopt.replace('-', '_'), None): raise exceptions.CommandError( - "ERROR: --%s was given, which is an Image v1 option" - " that is no longer supported in Image v2" % deadopt) + _("ERROR: --%s was given, which is an Image v1 option" + " that is no longer supported in Image v2") % deadopt) # Build an attribute dict from the parsed args, only include # attributes that were actually set on the command line @@ -296,11 +296,12 @@ class CreateImage(command.ShowOne): fp = gc_utils.get_data_file(parsed_args) info = {} if fp is not None and parsed_args.volume: - raise exceptions.CommandError("Uploading data and using container " - "are not allowed at the same time") + raise exceptions.CommandError(_("Uploading data and using " + "container are not allowed at " + "the same time")) if fp is None and parsed_args.file: - self.log.warning("Failed to get an image file.") + self.log.warning(_("Failed to get an image file.")) return {}, {} if parsed_args.owner: @@ -325,7 +326,10 @@ class CreateImage(command.ShowOne): parsed_args.disk_format, ) info = body['os-volume_upload_image'] - info['volume_type'] = info['volume_type']['name'] + try: + info['volume_type'] = info['volume_type']['name'] + except TypeError: + info['volume_type'] = None else: image = image_client.images.create(**kwargs) @@ -363,7 +367,7 @@ class DeleteImage(command.Command): "images", metavar="<image>", nargs="+", - help="Image(s) to delete (name or ID)", + help=_("Image(s) to delete (name or ID)"), ) return parser @@ -388,33 +392,33 @@ class ListImage(command.Lister): dest="public", action="store_true", default=False, - help="List only public images", + help=_("List only public images"), ) public_group.add_argument( "--private", dest="private", action="store_true", default=False, - help="List only private images", + help=_("List only private images"), ) public_group.add_argument( "--shared", dest="shared", action="store_true", default=False, - help="List only shared images", + help=_("List only shared images"), ) parser.add_argument( '--property', metavar='<key=value>', action=parseractions.KeyValueAction, - help='Filter output based on property', + help=_('Filter output based on property'), ) parser.add_argument( '--long', action='store_true', default=False, - help='List additional fields in output', + help=_('List additional fields in output'), ) # --page-size has never worked, leave here for silent compatibility @@ -427,23 +431,23 @@ class ListImage(command.Lister): parser.add_argument( '--sort', metavar="<key>[:<direction>]", - help="Sort output by selected keys and directions(asc or desc) " - "(default: asc), multiple keys and directions can be " - "specified separated by comma", + help=_("Sort output by selected keys and directions(asc or desc) " + "(default: asc), multiple keys and directions can be " + "specified separated by comma"), ) parser.add_argument( "--limit", metavar="<limit>", type=int, - help="Maximum number of images to display.", + help=_("Maximum number of images to display."), ) parser.add_argument( '--marker', metavar='<marker>', default=None, - help="The last image (name or ID) of the previous page. Display " - "list of images after marker. Display all images if not " - "specified." + help=_("The last image (name or ID) of the previous page. Display " + "list of images after marker. Display all images if not " + "specified."), ) return parser @@ -527,12 +531,12 @@ class RemoveProjectImage(command.Command): parser.add_argument( "image", metavar="<image>", - help="Image to unshare (name or ID)", + help=_("Image to unshare (name or ID)"), ) parser.add_argument( "project", metavar="<project>", - help="Project to disassociate with image (name or ID)", + help=_("Project to disassociate with image (name or ID)"), ) common.add_project_domain_option_to_parser(parser) return parser @@ -560,12 +564,12 @@ class SaveImage(command.Command): parser.add_argument( "--file", metavar="<filename>", - help="Downloaded image save filename (default: stdout)", + help=_("Downloaded image save filename (default: stdout)"), ) parser.add_argument( "image", metavar="<image>", - help="Image to save (name or ID)", + help=_("Image to save (name or ID)"), ) return parser @@ -600,66 +604,66 @@ class SetImage(command.Command): parser.add_argument( "image", metavar="<image>", - help="Image to modify (name or ID)" + help=_("Image to modify (name or ID)") ) parser.add_argument( "--name", metavar="<name>", - help="New image name" + help=_("New image name") ) parser.add_argument( "--min-disk", type=int, metavar="<disk-gb>", - help="Minimum disk size needed to boot image, in gigabytes" + help=_("Minimum disk size needed to boot image, in gigabytes") ) parser.add_argument( "--min-ram", type=int, metavar="<ram-mb>", - help="Minimum RAM size needed to boot image, in megabytes", + help=_("Minimum RAM size needed to boot image, in megabytes"), ) parser.add_argument( "--container-format", metavar="<container-format>", - help="Image container format " - "(default: %s)" % DEFAULT_CONTAINER_FORMAT, + help=_("Image container format " + "(default: %s)") % DEFAULT_CONTAINER_FORMAT, ) parser.add_argument( "--disk-format", metavar="<disk-format>", - help="Image disk format " - "(default: %s)" % DEFAULT_DISK_FORMAT, + help=_("Image disk format " + "(default: %s)") % DEFAULT_DISK_FORMAT, ) protected_group = parser.add_mutually_exclusive_group() protected_group.add_argument( "--protected", action="store_true", - help="Prevent image from being deleted", + help=_("Prevent image from being deleted"), ) protected_group.add_argument( "--unprotected", action="store_true", - help="Allow image to be deleted (default)", + help=_("Allow image to be deleted (default)"), ) public_group = parser.add_mutually_exclusive_group() public_group.add_argument( "--public", action="store_true", - help="Image is accessible to the public", + help=_("Image is accessible to the public"), ) public_group.add_argument( "--private", action="store_true", - help="Image is inaccessible to the public (default)", + help=_("Image is inaccessible to the public (default)"), ) parser.add_argument( "--property", dest="properties", metavar="<key=value>", action=parseractions.KeyValueAction, - help="Set a property on this image " - "(repeat option to set multiple properties)", + help=_("Set a property on this image " + "(repeat option to set multiple properties)"), ) parser.add_argument( "--tag", @@ -667,18 +671,18 @@ class SetImage(command.Command): metavar="<tag>", default=[], action='append', - help="Set a tag on this image " - "(repeat option to set multiple tags)", + help=_("Set a tag on this image " + "(repeat option to set multiple tags)"), ) parser.add_argument( "--architecture", metavar="<architecture>", - help="Operating system architecture", + help=_("Operating system architecture"), ) parser.add_argument( "--instance-id", metavar="<instance-id>", - help="ID of server instance used to create this image", + help=_("ID of server instance used to create this image"), ) parser.add_argument( "--instance-uuid", @@ -689,33 +693,33 @@ class SetImage(command.Command): parser.add_argument( "--kernel-id", metavar="<kernel-id>", - help="ID of kernel image used to boot this disk image", + help=_("ID of kernel image used to boot this disk image"), ) parser.add_argument( "--os-distro", metavar="<os-distro>", - help="Operating system distribution name", + help=_("Operating system distribution name"), ) parser.add_argument( "--os-version", metavar="<os-version>", - help="Operating system distribution version", + help=_("Operating system distribution version"), ) parser.add_argument( "--ramdisk-id", metavar="<ramdisk-id>", - help="ID of ramdisk image used to boot this disk image", + help=_("ID of ramdisk image used to boot this disk image"), ) deactivate_group = parser.add_mutually_exclusive_group() deactivate_group.add_argument( "--deactivate", action="store_true", - help="Deactivate the image", + help=_("Deactivate the image"), ) deactivate_group.add_argument( "--activate", action="store_true", - help="Activate the image", + help=_("Activate the image"), ) # NOTE(dtroyer): --owner is deprecated in Jan 2016 in an early # 2.x release. Do not remove before Jan 2017 @@ -724,7 +728,7 @@ class SetImage(command.Command): project_group.add_argument( "--project", metavar="<project>", - help="Set an alternate project on this image (name or ID)", + help=_("Set an alternate project on this image (name or ID)"), ) project_group.add_argument( "--owner", @@ -748,8 +752,8 @@ class SetImage(command.Command): for deadopt in self.deadopts: if getattr(parsed_args, deadopt.replace('-', '_'), None): raise exceptions.CommandError( - "ERROR: --%s was given, which is an Image v1 option" - " that is no longer supported in Image v2" % deadopt) + _("ERROR: --%s was given, which is an Image v1 option" + " that is no longer supported in Image v2") % deadopt) kwargs = {} copy_attrs = ('architecture', 'container_format', 'disk_format', @@ -801,7 +805,7 @@ class SetImage(command.Command): # Checks if anything that requires getting the image if not (kwargs or parsed_args.deactivate or parsed_args.activate): - self.log.warning("No arguments specified") + self.log.warning(_("No arguments specified")) return {}, {} image = utils.find_resource( @@ -839,7 +843,7 @@ class ShowImage(command.ShowOne): parser.add_argument( "image", metavar="<image>", - help="Image to display (name or ID)", + help=_("Image to display (name or ID)"), ) return parser diff --git a/openstackclient/locale/de/LC_MESSAGES/openstackclient.po b/openstackclient/locale/de/LC_MESSAGES/openstackclient.po index 1c94b9d..f7445b1 100644 --- a/openstackclient/locale/de/LC_MESSAGES/openstackclient.po +++ b/openstackclient/locale/de/LC_MESSAGES/openstackclient.po @@ -5,13 +5,12 @@ # # Translators: # Ettore Atalan <atalanttore@googlemail.com>, 2014-2015 -# Andreas Jaeger <jaegerandi@gmail.com>, 2015. #zanata -# OpenStack Infra <zanata@openstack.org>, 2015. #zanata +# Andreas Jaeger <jaegerandi@gmail.com>, 2016. #zanata msgid "" msgstr "" -"Project-Id-Version: python-openstackclient 2.0.1.dev168\n" -"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2016-01-19 02:20+0000\n" +"Project-Id-Version: python-openstackclient 2.2.1.dev235\n" +"Report-Msgid-Bugs-To: https://bugs.launchpad.net/openstack-i18n/\n" +"POT-Creation-Date: 2016-04-19 05:02+0000\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" @@ -167,10 +166,6 @@ msgid "Endpoint ID to display" msgstr "Anzuzeigende Endpunktkennung" #, python-format -msgid "Error creating server snapshot: %s" -msgstr "Fehler beim Erstellen der Server-Schattenkopie: %s" - -#, python-format msgid "Error creating server: %s" msgstr "Fehler beim Erstellen des Servers: %s" @@ -181,11 +176,6 @@ msgstr "Fehler beim Löschen des Servers: %s" msgid "Error retrieving diagnostics data" msgstr "Fehler beim Abrufen der Diagnosedaten" -msgid "File to inject into image before boot (repeat for multiple files)" -msgstr "" -"Vor dem Start auf diesem Abbild einzufügende Datei (für mehrere Dateien " -"wiederholen)" - msgid "Filter by parent region ID" msgstr "Nach übergeordneter Regionskennung filtern" @@ -338,11 +328,6 @@ msgstr "Zu löschende(s) Projekt(e) (Name oder Kennung)" msgid "Prompt interactively for password" msgstr "Interaktiv nach dem Passwort abfragen" -msgid "Property key to remove from server (repeat to unset multiple values)" -msgstr "" -"Vom Server zu entfernender Eigenschaftsschlüssel (zum Aufheben von mehreren " -"Werten wiederholen)" - msgid "" "Property to add/change for this server (repeat option to set multiple " "properties)" @@ -446,11 +431,6 @@ msgstr "" "Legen Sie eine Projekteigenschaft fest (wiederholen Sie die Option, um " "mehrere Eigenschaften festzulegen)" -msgid "Set a property on this server (repeat for multiple values)" -msgstr "" -"Legen Sie eine Eigenschaft auf diesem Server fest (für mehrere Werte " -"wiederholen)" - msgid "" "Set a scope, such as a project or domain, set a project scope with --os-" "project-name, OS_PROJECT_NAME or auth.project_name, set a domain scope with " diff --git a/openstackclient/locale/openstackclient.pot b/openstackclient/locale/openstackclient.pot index afc8926..e197fa7 100644 --- a/openstackclient/locale/openstackclient.pot +++ b/openstackclient/locale/openstackclient.pot @@ -7,9 +7,9 @@ #, fuzzy msgid "" msgstr "" -"Project-Id-Version: python-openstackclient 2.0.1.dev168\n" -"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2016-01-19 06:03+0000\n" +"Project-Id-Version: python-openstackclient 2.2.1.dev235\n" +"Report-Msgid-Bugs-To: https://bugs.launchpad.net/openstack-i18n/\n" +"POT-Creation-Date: 2016-04-19 06:14+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Language-Team: LANGUAGE <LL@li.org>\n" @@ -18,123 +18,142 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.2.0\n" -#: openstackclient/api/auth.py:144 +#: openstackclient/api/auth.py:148 msgid "Set a username with --os-username, OS_USERNAME, or auth.username\n" msgstr "" -#: openstackclient/api/auth.py:147 +#: openstackclient/api/auth.py:151 msgid "" "Set an authentication URL, with --os-auth-url, OS_AUTH_URL or " "auth.auth_url\n" msgstr "" -#: openstackclient/api/auth.py:155 +#: openstackclient/api/auth.py:160 msgid "" "Set a scope, such as a project or domain, set a project scope with --os-" "project-name, OS_PROJECT_NAME or auth.project_name, set a domain scope " "with --os-domain-name, OS_DOMAIN_NAME or auth.domain_name" msgstr "" -#: openstackclient/api/auth.py:161 openstackclient/api/auth.py:167 +#: openstackclient/api/auth.py:166 openstackclient/api/auth.py:172 msgid "Set a token with --os-token, OS_TOKEN or auth.token\n" msgstr "" -#: openstackclient/api/auth.py:163 +#: openstackclient/api/auth.py:168 msgid "Set a service AUTH_URL, with --os-auth-url, OS_AUTH_URL or auth.auth_url\n" msgstr "" -#: openstackclient/api/auth.py:169 +#: openstackclient/api/auth.py:174 msgid "Set a service URL, with --os-url, OS_URL or auth.url\n" msgstr "" -#: openstackclient/compute/v2/availability_zone.py:72 -#: openstackclient/compute/v2/server.py:737 -#: openstackclient/identity/v2_0/endpoint.py:114 -#: openstackclient/identity/v2_0/project.py:145 -#: openstackclient/identity/v2_0/service.py:128 -#: openstackclient/identity/v2_0/user.py:174 +#: openstackclient/common/availability_zone.py:110 +#: openstackclient/compute/v2/server.py:757 +#: openstackclient/identity/v2_0/endpoint.py:101 +#: openstackclient/identity/v2_0/project.py:133 +#: openstackclient/identity/v2_0/service.py:115 +#: openstackclient/identity/v2_0/user.py:162 +#: openstackclient/network/v2/router.py:230 +#: openstackclient/network/v2/subnet.py:299 +#: openstackclient/network/v2/subnet_pool.py:165 msgid "List additional fields in output" msgstr "" -#: openstackclient/compute/v2/server.py:183 -#: openstackclient/compute/v2/server.py:219 -#: openstackclient/compute/v2/server.py:569 -#: openstackclient/compute/v2/server.py:923 -#: openstackclient/compute/v2/server.py:1031 -#: openstackclient/compute/v2/server.py:1086 -#: openstackclient/compute/v2/server.py:1179 -#: openstackclient/compute/v2/server.py:1219 -#: openstackclient/compute/v2/server.py:1245 -#: openstackclient/compute/v2/server.py:1336 -#: openstackclient/compute/v2/server.py:1420 -#: openstackclient/compute/v2/server.py:1457 -#: openstackclient/compute/v2/server.py:1732 -#: openstackclient/compute/v2/server.py:1756 +#: openstackclient/common/parseractions.py:99 +#, python-format +msgid "" +"Invalid keys %(invalid_keys)s specified.\n" +"Valid keys are: %(valid_keys)s." +msgstr "" + +#: openstackclient/common/parseractions.py:109 +#, python-format +msgid "" +"Missing required keys %(missing_keys)s.\n" +"Required keys are: %(required_keys)s." +msgstr "" + +#: openstackclient/compute/v2/server.py:195 +#: openstackclient/compute/v2/server.py:227 +#: openstackclient/compute/v2/server.py:598 +#: openstackclient/compute/v2/server.py:937 +#: openstackclient/compute/v2/server.py:1039 +#: openstackclient/compute/v2/server.py:1091 +#: openstackclient/compute/v2/server.py:1177 +#: openstackclient/compute/v2/server.py:1213 +#: openstackclient/compute/v2/server.py:1236 +#: openstackclient/compute/v2/server.py:1343 +#: openstackclient/compute/v2/server.py:1421 +#: openstackclient/compute/v2/server.py:1455 +#: openstackclient/compute/v2/server.py:1712 +#: openstackclient/compute/v2/server.py:1733 msgid "Server (name or ID)" msgstr "" -#: openstackclient/compute/v2/server.py:188 +#: openstackclient/compute/v2/server.py:200 msgid "Security group to add (name or ID)" msgstr "" -#: openstackclient/compute/v2/server.py:224 +#: openstackclient/compute/v2/server.py:232 msgid "Volume to add (name or ID)" msgstr "" -#: openstackclient/compute/v2/server.py:229 +#: openstackclient/compute/v2/server.py:237 msgid "Server internal device name for volume" msgstr "" -#: openstackclient/compute/v2/server.py:265 -#: openstackclient/compute/v2/server.py:1341 +#: openstackclient/compute/v2/server.py:269 +#: openstackclient/compute/v2/server.py:1348 msgid "New server name" msgstr "" -#: openstackclient/compute/v2/server.py:273 +#: openstackclient/compute/v2/server.py:277 msgid "Create server from this image (name or ID)" msgstr "" -#: openstackclient/compute/v2/server.py:278 +#: openstackclient/compute/v2/server.py:282 msgid "Create server from this volume (name or ID)" msgstr "" -#: openstackclient/compute/v2/server.py:284 +#: openstackclient/compute/v2/server.py:288 msgid "Create server with this flavor (name or ID)" msgstr "" -#: openstackclient/compute/v2/server.py:291 +#: openstackclient/compute/v2/server.py:295 msgid "" -"Security group to assign to this server (name or ID) (repeat for multiple" -" groups)" +"Security group to assign to this server (name or ID) (repeat option to " +"set multiple groups)" msgstr "" -#: openstackclient/compute/v2/server.py:297 +#: openstackclient/compute/v2/server.py:301 msgid "Keypair to inject into this server (optional extension)" msgstr "" -#: openstackclient/compute/v2/server.py:303 -msgid "Set a property on this server (repeat for multiple values)" +#: openstackclient/compute/v2/server.py:307 +msgid "Set a property on this server (repeat option to set multiple values)" msgstr "" -#: openstackclient/compute/v2/server.py:311 -msgid "File to inject into image before boot (repeat for multiple files)" +#: openstackclient/compute/v2/server.py:315 +msgid "" +"File to inject into image before boot (repeat option to set multiple " +"files)" msgstr "" -#: openstackclient/compute/v2/server.py:317 +#: openstackclient/compute/v2/server.py:321 msgid "User data file to serve from the metadata server" msgstr "" -#: openstackclient/compute/v2/server.py:322 +#: openstackclient/compute/v2/server.py:326 msgid "Select an availability zone for the server" msgstr "" -#: openstackclient/compute/v2/server.py:329 +#: openstackclient/compute/v2/server.py:333 msgid "" "Map block devices; map is <id>:<type>:<size(GB)>:<delete_on_terminate> " "(optional extension)" msgstr "" -#: openstackclient/compute/v2/server.py:339 +#: openstackclient/compute/v2/server.py:343 msgid "" "Create a NIC on the server. Specify option multiple times to create " "multiple NICs. Either net-id or port-id must be provided, but not both. " @@ -143,296 +162,304 @@ msgid "" "-fixed-ip: IPv6 fixed address for NIC (optional)." msgstr "" -#: openstackclient/compute/v2/server.py:352 +#: openstackclient/compute/v2/server.py:356 msgid "Hints for the scheduler (optional extension)" msgstr "" -#: openstackclient/compute/v2/server.py:358 +#: openstackclient/compute/v2/server.py:362 msgid "" "Use specified volume as the config drive, or 'True' to use an ephemeral " "drive" msgstr "" -#: openstackclient/compute/v2/server.py:366 +#: openstackclient/compute/v2/server.py:370 msgid "Minimum number of servers to launch (default=1)" msgstr "" -#: openstackclient/compute/v2/server.py:373 +#: openstackclient/compute/v2/server.py:377 msgid "Maximum number of servers to launch (default=1)" msgstr "" -#: openstackclient/compute/v2/server.py:378 +#: openstackclient/compute/v2/server.py:382 msgid "Wait for build to complete" msgstr "" -#: openstackclient/compute/v2/server.py:418 +#: openstackclient/compute/v2/server.py:421 msgid "min instances should be <= max instances" msgstr "" -#: openstackclient/compute/v2/server.py:421 +#: openstackclient/compute/v2/server.py:424 msgid "min instances should be > 0" msgstr "" -#: openstackclient/compute/v2/server.py:424 +#: openstackclient/compute/v2/server.py:427 msgid "max instances should be > 0" msgstr "" -#: openstackclient/compute/v2/server.py:453 +#: openstackclient/compute/v2/server.py:456 msgid "Volume name or ID must be specified if --block-device-mapping is specified" msgstr "" -#: openstackclient/compute/v2/server.py:465 +#: openstackclient/compute/v2/server.py:468 msgid "either net-id or port-id should be specified but not both" msgstr "" -#: openstackclient/compute/v2/server.py:485 +#: openstackclient/compute/v2/server.py:488 msgid "can't create server with port specified since network endpoint not enabled" msgstr "" -#: openstackclient/compute/v2/server.py:550 +#: openstackclient/compute/v2/server.py:553 #, python-format msgid "Error creating server: %s" msgstr "" -#: openstackclient/compute/v2/server.py:552 +#: openstackclient/compute/v2/server.py:555 msgid "" "\n" "Error creating server" msgstr "" -#: openstackclient/compute/v2/server.py:574 +#: openstackclient/compute/v2/server.py:577 +msgid "Server(s) to create dump file (name or ID)" +msgstr "" + +#: openstackclient/compute/v2/server.py:603 msgid "Name of new image (default is server name)" msgstr "" -#: openstackclient/compute/v2/server.py:579 +#: openstackclient/compute/v2/server.py:608 msgid "Wait for image create to complete" msgstr "" -#: openstackclient/compute/v2/server.py:609 +#: openstackclient/compute/v2/server.py:637 #, python-format -msgid "Error creating server snapshot: %s" +msgid "Error creating snapshot of server: %s" msgstr "" -#: openstackclient/compute/v2/server.py:611 +#: openstackclient/compute/v2/server.py:639 msgid "" "\n" "Error creating server snapshot" msgstr "" -#: openstackclient/compute/v2/server.py:633 +#: openstackclient/compute/v2/server.py:656 msgid "Server(s) to delete (name or ID)" msgstr "" -#: openstackclient/compute/v2/server.py:638 +#: openstackclient/compute/v2/server.py:661 msgid "Wait for delete to complete" msgstr "" -#: openstackclient/compute/v2/server.py:657 +#: openstackclient/compute/v2/server.py:679 #, python-format msgid "Error deleting server: %s" msgstr "" -#: openstackclient/compute/v2/server.py:659 +#: openstackclient/compute/v2/server.py:681 msgid "" "\n" "Error deleting server" msgstr "" -#: openstackclient/compute/v2/server.py:673 +#: openstackclient/compute/v2/server.py:693 msgid "Only return instances that match the reservation" msgstr "" -#: openstackclient/compute/v2/server.py:678 +#: openstackclient/compute/v2/server.py:698 msgid "Regular expression to match IP addresses" msgstr "" -#: openstackclient/compute/v2/server.py:683 +#: openstackclient/compute/v2/server.py:703 msgid "Regular expression to match IPv6 addresses" msgstr "" -#: openstackclient/compute/v2/server.py:688 +#: openstackclient/compute/v2/server.py:708 msgid "Regular expression to match names" msgstr "" -#: openstackclient/compute/v2/server.py:693 +#: openstackclient/compute/v2/server.py:713 msgid "Regular expression to match instance name (admin only)" msgstr "" -#: openstackclient/compute/v2/server.py:699 +#: openstackclient/compute/v2/server.py:719 msgid "Search by server status" msgstr "" -#: openstackclient/compute/v2/server.py:704 +#: openstackclient/compute/v2/server.py:724 msgid "Search by flavor (name or ID)" msgstr "" -#: openstackclient/compute/v2/server.py:709 +#: openstackclient/compute/v2/server.py:729 msgid "Search by image (name or ID)" msgstr "" -#: openstackclient/compute/v2/server.py:714 +#: openstackclient/compute/v2/server.py:734 msgid "Search by hostname" msgstr "" -#: openstackclient/compute/v2/server.py:720 +#: openstackclient/compute/v2/server.py:740 msgid "Include all projects (admin only)" msgstr "" -#: openstackclient/compute/v2/server.py:730 +#: openstackclient/compute/v2/server.py:750 msgid "Search by user (admin only) (name or ID)" msgstr "" -#: openstackclient/compute/v2/server.py:888 +#: openstackclient/compute/v2/server.py:905 msgid "Server(s) to lock (name or ID)" msgstr "" -#: openstackclient/compute/v2/server.py:928 +#: openstackclient/compute/v2/server.py:942 msgid "Target hostname" msgstr "" -#: openstackclient/compute/v2/server.py:936 +#: openstackclient/compute/v2/server.py:950 msgid "Perform a shared live migration (default)" msgstr "" -#: openstackclient/compute/v2/server.py:942 +#: openstackclient/compute/v2/server.py:956 msgid "Perform a block live migration" msgstr "" -#: openstackclient/compute/v2/server.py:949 +#: openstackclient/compute/v2/server.py:963 msgid "Allow disk over-commit on the destination host" msgstr "" -#: openstackclient/compute/v2/server.py:956 +#: openstackclient/compute/v2/server.py:970 msgid "Do not over-commit disk on the destination host (default)" msgstr "" -#: openstackclient/compute/v2/server.py:962 -#: openstackclient/compute/v2/server.py:1265 +#: openstackclient/compute/v2/server.py:976 +#: openstackclient/compute/v2/server.py:1256 msgid "Wait for resize to complete" msgstr "" -#: openstackclient/compute/v2/server.py:990 -#: openstackclient/compute/v2/server.py:1290 +#: openstackclient/compute/v2/server.py:1003 +#: openstackclient/compute/v2/server.py:1280 msgid "Complete\n" msgstr "" -#: openstackclient/compute/v2/server.py:992 +#: openstackclient/compute/v2/server.py:1005 msgid "" "\n" "Error migrating server" msgstr "" -#: openstackclient/compute/v2/server.py:1007 +#: openstackclient/compute/v2/server.py:1018 msgid "Server(s) to pause (name or ID)" msgstr "" -#: openstackclient/compute/v2/server.py:1040 +#: openstackclient/compute/v2/server.py:1048 msgid "Perform a hard reboot" msgstr "" -#: openstackclient/compute/v2/server.py:1048 +#: openstackclient/compute/v2/server.py:1056 msgid "Perform a soft reboot" msgstr "" -#: openstackclient/compute/v2/server.py:1053 +#: openstackclient/compute/v2/server.py:1061 msgid "Wait for reboot to complete" msgstr "" -#: openstackclient/compute/v2/server.py:1070 +#: openstackclient/compute/v2/server.py:1077 msgid "" "\n" "Reboot complete\n" msgstr "" -#: openstackclient/compute/v2/server.py:1072 +#: openstackclient/compute/v2/server.py:1079 msgid "" "\n" "Error rebooting server\n" msgstr "" -#: openstackclient/compute/v2/server.py:1091 +#: openstackclient/compute/v2/server.py:1096 msgid "" "Recreate server from the specified image (name or ID). Defaults to the " "currently used one." msgstr "" -#: openstackclient/compute/v2/server.py:1102 +#: openstackclient/compute/v2/server.py:1107 msgid "Wait for rebuild to complete" msgstr "" -#: openstackclient/compute/v2/server.py:1124 +#: openstackclient/compute/v2/server.py:1128 msgid "" "\n" "Complete\n" msgstr "" -#: openstackclient/compute/v2/server.py:1126 +#: openstackclient/compute/v2/server.py:1130 msgid "" "\n" "Error rebuilding server" msgstr "" -#: openstackclient/compute/v2/server.py:1143 +#: openstackclient/compute/v2/server.py:1145 msgid "Name or ID of server to use" msgstr "" -#: openstackclient/compute/v2/server.py:1148 +#: openstackclient/compute/v2/server.py:1150 msgid "Name or ID of security group to remove from server" msgstr "" -#: openstackclient/compute/v2/server.py:1184 +#: openstackclient/compute/v2/server.py:1182 msgid "Volume to remove (name or ID)" msgstr "" -#: openstackclient/compute/v2/server.py:1250 +#: openstackclient/compute/v2/server.py:1241 msgid "Resize server to specified flavor" msgstr "" -#: openstackclient/compute/v2/server.py:1255 +#: openstackclient/compute/v2/server.py:1246 msgid "Confirm server resize is complete" msgstr "" -#: openstackclient/compute/v2/server.py:1260 +#: openstackclient/compute/v2/server.py:1251 msgid "Restore server state before resize" msgstr "" -#: openstackclient/compute/v2/server.py:1292 +#: openstackclient/compute/v2/server.py:1282 msgid "" "\n" "Error resizing server" msgstr "" -#: openstackclient/compute/v2/server.py:1311 +#: openstackclient/compute/v2/server.py:1299 +msgid "Server(s) to restore (name or ID)" +msgstr "" + +#: openstackclient/compute/v2/server.py:1321 msgid "Server(s) to resume (name or ID)" msgstr "" -#: openstackclient/compute/v2/server.py:1346 +#: openstackclient/compute/v2/server.py:1353 msgid "Set new root password (interactive only)" msgstr "" -#: openstackclient/compute/v2/server.py:1352 +#: openstackclient/compute/v2/server.py:1359 msgid "" "Property to add/change for this server (repeat option to set multiple " "properties)" msgstr "" -#: openstackclient/compute/v2/server.py:1376 +#: openstackclient/compute/v2/server.py:1382 msgid "New password: " msgstr "" -#: openstackclient/compute/v2/server.py:1377 +#: openstackclient/compute/v2/server.py:1383 msgid "Retype new password: " msgstr "" -#: openstackclient/compute/v2/server.py:1381 +#: openstackclient/compute/v2/server.py:1387 msgid "Passwords do not match, password unchanged" msgstr "" -#: openstackclient/compute/v2/server.py:1396 +#: openstackclient/compute/v2/server.py:1400 msgid "Server(s) to shelve (name or ID)" msgstr "" -#: openstackclient/compute/v2/server.py:1426 +#: openstackclient/compute/v2/server.py:1427 msgid "Display server diagnostics information" msgstr "" @@ -440,390 +467,944 @@ msgstr "" msgid "Error retrieving diagnostics data" msgstr "" -#: openstackclient/compute/v2/server.py:1462 +#: openstackclient/compute/v2/server.py:1460 msgid "Login name (ssh -l option)" msgstr "" -#: openstackclient/compute/v2/server.py:1474 +#: openstackclient/compute/v2/server.py:1472 msgid "Destination port (ssh -p option)" msgstr "" -#: openstackclient/compute/v2/server.py:1486 +#: openstackclient/compute/v2/server.py:1484 msgid "Private key file (ssh -i option)" msgstr "" -#: openstackclient/compute/v2/server.py:1497 +#: openstackclient/compute/v2/server.py:1495 msgid "Options in ssh_config(5) format (ssh -o option)" msgstr "" -#: openstackclient/compute/v2/server.py:1511 +#: openstackclient/compute/v2/server.py:1509 msgid "Use only IPv4 addresses" msgstr "" -#: openstackclient/compute/v2/server.py:1518 +#: openstackclient/compute/v2/server.py:1516 msgid "Use only IPv6 addresses" msgstr "" -#: openstackclient/compute/v2/server.py:1527 +#: openstackclient/compute/v2/server.py:1525 msgid "Use public IP address" msgstr "" -#: openstackclient/compute/v2/server.py:1535 +#: openstackclient/compute/v2/server.py:1533 msgid "Use private IP address" msgstr "" -#: openstackclient/compute/v2/server.py:1542 +#: openstackclient/compute/v2/server.py:1540 msgid "Use other IP address (public, private, etc)" msgstr "" -#: openstackclient/compute/v2/server.py:1605 +#: openstackclient/compute/v2/server.py:1600 msgid "Server(s) to start (name or ID)" msgstr "" -#: openstackclient/compute/v2/server.py:1630 +#: openstackclient/compute/v2/server.py:1622 msgid "Server(s) to stop (name or ID)" msgstr "" -#: openstackclient/compute/v2/server.py:1655 +#: openstackclient/compute/v2/server.py:1644 msgid "Server(s) to suspend (name or ID)" msgstr "" -#: openstackclient/compute/v2/server.py:1681 +#: openstackclient/compute/v2/server.py:1667 msgid "Server(s) to unlock (name or ID)" msgstr "" -#: openstackclient/compute/v2/server.py:1707 +#: openstackclient/compute/v2/server.py:1690 msgid "Server(s) to unpause (name or ID)" msgstr "" -#: openstackclient/compute/v2/server.py:1763 -msgid "Property key to remove from server (repeat to unset multiple values)" +#: openstackclient/compute/v2/server.py:1740 +msgid "" +"Property key to remove from server (repeat option to remove multiple " +"values)" msgstr "" -#: openstackclient/compute/v2/server.py:1794 +#: openstackclient/compute/v2/server.py:1768 msgid "Server(s) to unshelve (name or ID)" msgstr "" -#: openstackclient/identity/v2_0/catalog.py:75 -#: openstackclient/identity/v3/catalog.py:72 +#: openstackclient/compute/v2/service.py:138 +msgid "argument --disable-reason has been ignored" +msgstr "" + +#: openstackclient/identity/v2_0/catalog.py:67 +#: openstackclient/identity/v3/catalog.py:64 msgid "Service to display (type or name)" msgstr "" -#: openstackclient/identity/v2_0/ec2creds.py:40 -#: openstackclient/identity/v3/ec2creds.py:65 +#: openstackclient/identity/v2_0/ec2creds.py:34 +#: openstackclient/identity/v3/ec2creds.py:59 msgid "" "Create credentials in project (name or ID; default: current authenticated" " project)" msgstr "" -#: openstackclient/identity/v2_0/ec2creds.py:48 -#: openstackclient/identity/v3/ec2creds.py:73 +#: openstackclient/identity/v2_0/ec2creds.py:42 +#: openstackclient/identity/v3/ec2creds.py:67 msgid "" "Create credentials for user (name or ID; default: current authenticated " "user)" msgstr "" -#: openstackclient/identity/v2_0/ec2creds.py:99 -#: openstackclient/identity/v2_0/ec2creds.py:172 -#: openstackclient/identity/v3/ec2creds.py:129 -#: openstackclient/identity/v3/ec2creds.py:187 +#: openstackclient/identity/v2_0/ec2creds.py:90 +#: openstackclient/identity/v2_0/ec2creds.py:157 +#: openstackclient/identity/v3/ec2creds.py:120 +#: openstackclient/identity/v3/ec2creds.py:172 msgid "Credentials access key" msgstr "" -#: openstackclient/identity/v2_0/ec2creds.py:104 -#: openstackclient/identity/v3/ec2creds.py:134 +#: openstackclient/identity/v2_0/ec2creds.py:95 +#: openstackclient/identity/v3/ec2creds.py:125 msgid "Delete credentials for user (name or ID)" msgstr "" -#: openstackclient/identity/v2_0/ec2creds.py:134 -#: openstackclient/identity/v3/ec2creds.py:156 +#: openstackclient/identity/v2_0/ec2creds.py:122 +#: openstackclient/identity/v3/ec2creds.py:144 msgid "Filter list by user (name or ID)" msgstr "" -#: openstackclient/identity/v2_0/ec2creds.py:177 -#: openstackclient/identity/v3/ec2creds.py:192 +#: openstackclient/identity/v2_0/ec2creds.py:162 +#: openstackclient/identity/v3/ec2creds.py:177 msgid "Show credentials for user (name or ID)" msgstr "" -#: openstackclient/identity/v2_0/endpoint.py:40 +#: openstackclient/identity/v2_0/endpoint.py:34 msgid "New endpoint service (name or ID)" msgstr "" -#: openstackclient/identity/v2_0/endpoint.py:46 +#: openstackclient/identity/v2_0/endpoint.py:40 msgid "New endpoint public URL (required)" msgstr "" -#: openstackclient/identity/v2_0/endpoint.py:51 +#: openstackclient/identity/v2_0/endpoint.py:45 msgid "New endpoint admin URL" msgstr "" -#: openstackclient/identity/v2_0/endpoint.py:56 +#: openstackclient/identity/v2_0/endpoint.py:50 msgid "New endpoint internal URL" msgstr "" -#: openstackclient/identity/v2_0/endpoint.py:61 +#: openstackclient/identity/v2_0/endpoint.py:55 msgid "New endpoint region ID" msgstr "" -#: openstackclient/identity/v2_0/endpoint.py:93 +#: openstackclient/identity/v2_0/endpoint.py:84 msgid "Endpoint ID to delete" msgstr "" -#: openstackclient/identity/v2_0/endpoint.py:149 +#: openstackclient/identity/v2_0/endpoint.py:133 msgid "Endpoint ID to display" msgstr "" -#: openstackclient/identity/v2_0/project.py:41 +#: openstackclient/identity/v2_0/project.py:36 msgid "New project name" msgstr "" -#: openstackclient/identity/v2_0/project.py:46 +#: openstackclient/identity/v2_0/project.py:41 msgid "Project description" msgstr "" -#: openstackclient/identity/v2_0/project.py:52 +#: openstackclient/identity/v2_0/project.py:47 msgid "Enable project (default)" msgstr "" -#: openstackclient/identity/v2_0/project.py:57 -#: openstackclient/identity/v2_0/project.py:194 +#: openstackclient/identity/v2_0/project.py:52 +#: openstackclient/identity/v2_0/project.py:179 msgid "Disable project" msgstr "" -#: openstackclient/identity/v2_0/project.py:63 +#: openstackclient/identity/v2_0/project.py:58 msgid "Add a property to <name> (repeat option to set multiple properties)" msgstr "" -#: openstackclient/identity/v2_0/project.py:69 -#: openstackclient/identity/v3/project.py:80 +#: openstackclient/identity/v2_0/project.py:64 +#: openstackclient/identity/v3/project.py:75 msgid "Return existing project" msgstr "" -#: openstackclient/identity/v2_0/project.py:117 +#: openstackclient/identity/v2_0/project.py:109 msgid "Project(s) to delete (name or ID)" msgstr "" -#: openstackclient/identity/v2_0/project.py:173 -#: openstackclient/identity/v2_0/project.py:313 +#: openstackclient/identity/v2_0/project.py:158 +#: openstackclient/identity/v2_0/project.py:291 msgid "Project to modify (name or ID)" msgstr "" -#: openstackclient/identity/v2_0/project.py:178 +#: openstackclient/identity/v2_0/project.py:163 msgid "Set project name" msgstr "" -#: openstackclient/identity/v2_0/project.py:183 +#: openstackclient/identity/v2_0/project.py:168 msgid "Set project description" msgstr "" -#: openstackclient/identity/v2_0/project.py:189 +#: openstackclient/identity/v2_0/project.py:174 msgid "Enable project" msgstr "" -#: openstackclient/identity/v2_0/project.py:200 +#: openstackclient/identity/v2_0/project.py:185 msgid "Set a project property (repeat option to set multiple properties)" msgstr "" -#: openstackclient/identity/v2_0/project.py:253 +#: openstackclient/identity/v2_0/project.py:234 msgid "Project to display (name or ID)" msgstr "" -#: openstackclient/identity/v2_0/project.py:320 +#: openstackclient/identity/v2_0/project.py:298 msgid "Unset a project property (repeat option to unset multiple properties)" msgstr "" -#: openstackclient/identity/v2_0/role.py:41 +#: openstackclient/identity/v2_0/role.py:36 msgid "Role to add to <project>:<user> (name or ID)" msgstr "" -#: openstackclient/identity/v2_0/role.py:47 -#: openstackclient/identity/v2_0/role.py:309 +#: openstackclient/identity/v2_0/role.py:42 +#: openstackclient/identity/v2_0/role.py:288 msgid "Include <project> (name or ID)" msgstr "" -#: openstackclient/identity/v2_0/role.py:53 -#: openstackclient/identity/v2_0/role.py:315 +#: openstackclient/identity/v2_0/role.py:48 +#: openstackclient/identity/v2_0/role.py:294 msgid "Include <user> (name or ID)" msgstr "" -#: openstackclient/identity/v2_0/role.py:87 +#: openstackclient/identity/v2_0/role.py:79 msgid "New role name" msgstr "" -#: openstackclient/identity/v2_0/role.py:92 -#: openstackclient/identity/v3/role.py:165 +#: openstackclient/identity/v2_0/role.py:84 +#: openstackclient/identity/v3/role.py:155 msgid "Return existing role" msgstr "" -#: openstackclient/identity/v2_0/role.py:127 +#: openstackclient/identity/v2_0/role.py:116 msgid "Role(s) to delete (name or ID)" msgstr "" -#: openstackclient/identity/v2_0/role.py:194 -#: openstackclient/identity/v2_0/role.py:257 +#: openstackclient/identity/v2_0/role.py:178 +#: openstackclient/identity/v2_0/role.py:238 msgid "Project must be specified" msgstr "" -#: openstackclient/identity/v2_0/role.py:208 -#: openstackclient/identity/v2_0/role.py:263 +#: openstackclient/identity/v2_0/role.py:192 +#: openstackclient/identity/v2_0/role.py:244 msgid "User must be specified" msgstr "" -#: openstackclient/identity/v2_0/role.py:236 +#: openstackclient/identity/v2_0/role.py:218 msgid "User to list (name or ID)" msgstr "" -#: openstackclient/identity/v2_0/role.py:241 +#: openstackclient/identity/v2_0/role.py:223 msgid "Filter users by <project> (name or ID)" msgstr "" -#: openstackclient/identity/v2_0/role.py:303 +#: openstackclient/identity/v2_0/role.py:282 msgid "Role to remove (name or ID)" msgstr "" -#: openstackclient/identity/v2_0/role.py:344 +#: openstackclient/identity/v2_0/role.py:320 msgid "Role to display (name or ID)" msgstr "" -#: openstackclient/identity/v2_0/service.py:42 +#: openstackclient/identity/v2_0/service.py:36 msgid "New service type (compute, image, identity, volume, etc)" msgstr "" -#: openstackclient/identity/v2_0/service.py:53 +#: openstackclient/identity/v2_0/service.py:47 msgid "New service name" msgstr "" -#: openstackclient/identity/v2_0/service.py:58 +#: openstackclient/identity/v2_0/service.py:52 msgid "New service description" msgstr "" -#: openstackclient/identity/v2_0/service.py:78 +#: openstackclient/identity/v2_0/service.py:71 msgid "" "The argument --type is deprecated, use service create --name <service-" "name> type instead." msgstr "" -#: openstackclient/identity/v2_0/service.py:105 +#: openstackclient/identity/v2_0/service.py:96 msgid "Service to delete (name or ID)" msgstr "" -#: openstackclient/identity/v2_0/service.py:156 +#: openstackclient/identity/v2_0/service.py:140 msgid "Service to display (type, name or ID)" msgstr "" -#: openstackclient/identity/v2_0/service.py:162 +#: openstackclient/identity/v2_0/service.py:146 msgid "Show service catalog information" msgstr "" -#: openstackclient/identity/v2_0/service.py:180 +#: openstackclient/identity/v2_0/service.py:163 #, python-format msgid "No service catalog with a type, name or ID of '%s' exists." msgstr "" -#: openstackclient/identity/v2_0/token.py:55 +#: openstackclient/identity/v2_0/token.py:50 msgid "Token to be deleted" msgstr "" -#: openstackclient/identity/v2_0/user.py:40 +#: openstackclient/identity/v2_0/user.py:35 msgid "New user name" msgstr "" -#: openstackclient/identity/v2_0/user.py:45 +#: openstackclient/identity/v2_0/user.py:40 msgid "Default project (name or ID)" msgstr "" -#: openstackclient/identity/v2_0/user.py:50 -#: openstackclient/identity/v2_0/user.py:273 +#: openstackclient/identity/v2_0/user.py:45 +#: openstackclient/identity/v2_0/user.py:258 msgid "Set user password" msgstr "" -#: openstackclient/identity/v2_0/user.py:56 -#: openstackclient/identity/v2_0/user.py:279 +#: openstackclient/identity/v2_0/user.py:51 +#: openstackclient/identity/v2_0/user.py:264 msgid "Prompt interactively for password" msgstr "" -#: openstackclient/identity/v2_0/user.py:61 -#: openstackclient/identity/v2_0/user.py:284 +#: openstackclient/identity/v2_0/user.py:56 +#: openstackclient/identity/v2_0/user.py:269 msgid "Set user email address" msgstr "" -#: openstackclient/identity/v2_0/user.py:67 -#: openstackclient/identity/v2_0/user.py:290 +#: openstackclient/identity/v2_0/user.py:62 +#: openstackclient/identity/v2_0/user.py:275 msgid "Enable user (default)" msgstr "" -#: openstackclient/identity/v2_0/user.py:72 -#: openstackclient/identity/v2_0/user.py:295 +#: openstackclient/identity/v2_0/user.py:67 +#: openstackclient/identity/v2_0/user.py:280 msgid "Disable user" msgstr "" -#: openstackclient/identity/v2_0/user.py:77 -#: openstackclient/identity/v3/user.py:90 +#: openstackclient/identity/v2_0/user.py:72 +#: openstackclient/identity/v3/user.py:86 msgid "Return existing user" msgstr "" -#: openstackclient/identity/v2_0/user.py:141 +#: openstackclient/identity/v2_0/user.py:133 msgid "User(s) to delete (name or ID)" msgstr "" -#: openstackclient/identity/v2_0/user.py:168 +#: openstackclient/identity/v2_0/user.py:156 msgid "Filter users by project (name or ID)" msgstr "" -#: openstackclient/identity/v2_0/user.py:258 +#: openstackclient/identity/v2_0/user.py:243 msgid "User to change (name or ID)" msgstr "" -#: openstackclient/identity/v2_0/user.py:263 +#: openstackclient/identity/v2_0/user.py:248 msgid "Set user name" msgstr "" -#: openstackclient/identity/v2_0/user.py:268 +#: openstackclient/identity/v2_0/user.py:253 msgid "Set default project (name or ID)" msgstr "" -#: openstackclient/identity/v2_0/user.py:361 +#: openstackclient/identity/v2_0/user.py:342 msgid "User to display (name or ID)" msgstr "" -#: openstackclient/identity/v3/domain.py:62 +#: openstackclient/identity/v3/domain.py:57 msgid "Return existing domain" msgstr "" -#: openstackclient/identity/v3/group.py:141 +#: openstackclient/identity/v3/group.py:130 msgid "Return existing group" msgstr "" -#: openstackclient/identity/v3/region.py:39 +#: openstackclient/identity/v3/region.py:33 msgid "New region ID" msgstr "" -#: openstackclient/identity/v3/region.py:44 +#: openstackclient/identity/v3/region.py:38 msgid "Parent region ID" msgstr "" -#: openstackclient/identity/v3/region.py:49 -#: openstackclient/identity/v3/region.py:144 +#: openstackclient/identity/v3/region.py:43 +#: openstackclient/identity/v3/region.py:128 msgid "New region description" msgstr "" -#: openstackclient/identity/v3/region.py:79 +#: openstackclient/identity/v3/region.py:70 msgid "Region ID to delete" msgstr "" -#: openstackclient/identity/v3/region.py:101 +#: openstackclient/identity/v3/region.py:88 msgid "Filter by parent region ID" msgstr "" -#: openstackclient/identity/v3/region.py:134 +#: openstackclient/identity/v3/region.py:118 msgid "Region to modify" msgstr "" -#: openstackclient/identity/v3/region.py:139 +#: openstackclient/identity/v3/region.py:123 msgid "New parent region ID" msgstr "" -#: openstackclient/identity/v3/region.py:175 +#: openstackclient/identity/v3/region.py:154 msgid "Region to display" msgstr "" +#: openstackclient/image/v1/image.py:191 openstackclient/image/v1/image.py:609 +#: openstackclient/image/v2/image.py:283 openstackclient/image/v2/image.py:794 +msgid "The --owner option is deprecated, please use --project instead." +msgstr "" + +#: openstackclient/network/v2/floating_ip.py:67 +msgid "Network to allocate floating IP from (name or ID)" +msgstr "" + +#: openstackclient/network/v2/floating_ip.py:75 +msgid "Subnet on which you want to create the floating IP (name or ID)" +msgstr "" + +#: openstackclient/network/v2/floating_ip.py:81 +msgid "Port to be associated with the floating IP (name or ID)" +msgstr "" + +#: openstackclient/network/v2/floating_ip.py:88 +msgid "Floating IP address" +msgstr "" + +#: openstackclient/network/v2/floating_ip.py:94 +msgid "Fixed IP address mapped to the floating IP" +msgstr "" + +#: openstackclient/network/v2/floating_ip.py:119 +msgid "Floating IP to delete (IP address or ID)" +msgstr "" + +#: openstackclient/network/v2/floating_ip.py:193 +msgid "Floating IP to display (IP address or ID)" +msgstr "" + +#: openstackclient/network/v2/network.py:114 +msgid "New network name" +msgstr "" + +#: openstackclient/network/v2/network.py:121 +#: openstackclient/network/v2/network.py:385 +msgid "Share the network between projects" +msgstr "" + +#: openstackclient/network/v2/network.py:126 +#: openstackclient/network/v2/network.py:390 +msgid "Do not share the network between projects" +msgstr "" + +#: openstackclient/network/v2/network.py:136 +msgid "Enable network (default)" +msgstr "" + +#: openstackclient/network/v2/network.py:141 +#: openstackclient/network/v2/network.py:378 +msgid "Disable network" +msgstr "" + +#: openstackclient/network/v2/network.py:146 +#: openstackclient/network/v2/port.py:246 +#: openstackclient/network/v2/router.py:174 +#: openstackclient/network/v2/security_group.py:116 +#: openstackclient/network/v2/security_group_rule.py:127 +#: openstackclient/network/v2/subnet.py:183 +#: openstackclient/network/v2/subnet_pool.py:114 +msgid "Owner's project (name or ID)" +msgstr "" + +#: openstackclient/network/v2/network.py:154 +msgid "" +"Availability Zone in which to create this network (Network Availability " +"Zone extension required, repeat option to set multiple availability " +"zones)" +msgstr "" + +#: openstackclient/network/v2/network.py:162 +#: openstackclient/network/v2/network.py:396 +msgid "Set this network as an external network (external-net extension required)" +msgstr "" + +#: openstackclient/network/v2/network.py:168 +msgid "Set this network as an internal network (default)" +msgstr "" + +#: openstackclient/network/v2/network.py:174 +msgid "Specify if this network should be used as the default external network" +msgstr "" + +#: openstackclient/network/v2/network.py:180 +msgid "Do not use the network as the default external network. (default)" +msgstr "" + +#: openstackclient/network/v2/network.py:188 +msgid "" +"The physical mechanism by which the virtual network is implemented. The " +"supported options are: flat, gre, local, vlan, vxlan" +msgstr "" + +#: openstackclient/network/v2/network.py:196 +msgid "Name of the physical network over which the virtual network is implemented" +msgstr "" + +#: openstackclient/network/v2/network.py:203 +msgid "VLAN ID for VLAN networks or Tunnel ID for GRE/VXLAN networks" +msgstr "" + +#: openstackclient/network/v2/network.py:212 +msgid "IPv4 subnet for fixed IPs (in CIDR notation)" +msgstr "" + +#: openstackclient/network/v2/network.py:361 +msgid "Network to modify (name or ID)" +msgstr "" + +#: openstackclient/network/v2/network.py:366 +msgid "Set network name" +msgstr "" + +#: openstackclient/network/v2/network.py:373 +msgid "Enable network" +msgstr "" + +#: openstackclient/network/v2/network.py:402 +msgid "Set this network as an internal network" +msgstr "" + +#: openstackclient/network/v2/network.py:408 +msgid "Set the network as the default external network" +msgstr "" + +#: openstackclient/network/v2/network.py:413 +msgid "Do not use the network as the default external network" +msgstr "" + +#: openstackclient/network/v2/network.py:436 +msgid "Network to display (name or ID)" +msgstr "" + +#: openstackclient/network/v2/port.py:73 +msgid "The --device-id option is deprecated, please use --device instead." +msgstr "" + +#: openstackclient/network/v2/port.py:79 +msgid "The --host-id option is deprecated, please use --host instead." +msgstr "" + +#: openstackclient/network/v2/port.py:161 +msgid "Port device ID" +msgstr "" + +#: openstackclient/network/v2/port.py:171 +msgid "Device owner of this port" +msgstr "" + +#: openstackclient/network/v2/port.py:178 +msgid "" +"VNIC type for this port (direct | direct-physical | macvtap | normal | " +"baremetal, default: normal)" +msgstr "" + +#: openstackclient/network/v2/port.py:187 +msgid "Allocate port on host <host-id> (ID only)" +msgstr "" + +#: openstackclient/network/v2/port.py:206 +msgid "Network this port belongs to (name or ID)" +msgstr "" + +#: openstackclient/network/v2/port.py:214 +#: openstackclient/network/v2/port.py:365 +msgid "" +"Desired IP and/or subnet (name or ID) for this port: subnet=<subnet>,ip-" +"address=<ip-address> (repeat option to set multiple fixed IP addresses)" +msgstr "" + +#: openstackclient/network/v2/port.py:222 +#: openstackclient/network/v2/port.py:379 +msgid "" +"Custom data to be passed as binding:profile: <key>=<value> (repeat option" +" to set multiple binding:profile data)" +msgstr "" + +#: openstackclient/network/v2/port.py:231 +msgid "Enable port (default)" +msgstr "" + +#: openstackclient/network/v2/port.py:236 +#: openstackclient/network/v2/port.py:352 +msgid "Disable port" +msgstr "" + +#: openstackclient/network/v2/port.py:241 +msgid "MAC address of this port" +msgstr "" + +#: openstackclient/network/v2/port.py:280 +msgid "Port(s) to delete (name or ID)" +msgstr "" + +#: openstackclient/network/v2/port.py:301 +msgid "List only ports attached to this router (name or ID)" +msgstr "" + +#: openstackclient/network/v2/port.py:347 +msgid "Enable port" +msgstr "" + +#: openstackclient/network/v2/port.py:357 +msgid "Set port name" +msgstr "" + +#: openstackclient/network/v2/port.py:372 +msgid "Clear existing information of fixed IP addresses" +msgstr "" + +#: openstackclient/network/v2/port.py:386 +msgid "Clear existing information of binding:profile" +msgstr "" + +#: openstackclient/network/v2/port.py:391 +msgid "Port to modify (name or ID)" +msgstr "" + +#: openstackclient/network/v2/port.py:429 +msgid "Port to display (name or ID)" +msgstr "" + +#: openstackclient/network/v2/router.py:99 +msgid "Router to which port will be added (name or ID)" +msgstr "" + +#: openstackclient/network/v2/router.py:104 +msgid "Port to be added (name or ID)" +msgstr "" + +#: openstackclient/network/v2/router.py:123 +msgid "Router to which subnet will be added (name or ID)" +msgstr "" + +#: openstackclient/network/v2/router.py:128 +msgid "Subnet to be added (name or ID)" +msgstr "" + +#: openstackclient/network/v2/router.py:150 +msgid "New router name" +msgstr "" + +#: openstackclient/network/v2/router.py:157 +msgid "Enable router (default)" +msgstr "" + +#: openstackclient/network/v2/router.py:162 +#: openstackclient/network/v2/router.py:351 +msgid "Disable router" +msgstr "" + +#: openstackclient/network/v2/router.py:169 +msgid "Create a distributed router" +msgstr "" + +#: openstackclient/network/v2/router.py:182 +msgid "" +"Availability Zone in which to create this router (Router Availability " +"Zone extension required, repeat option to set multiple availability " +"zones)" +msgstr "" + +#: openstackclient/network/v2/router.py:210 +msgid "Router(s) to delete (name or ID)" +msgstr "" + +#: openstackclient/network/v2/router.py:283 +msgid "Router from which port will be removed (name or ID)" +msgstr "" + +#: openstackclient/network/v2/router.py:288 +msgid "Port to be removed (name or ID)" +msgstr "" + +#: openstackclient/network/v2/router.py:307 +msgid "Router from which the subnet will be removed (name or ID)" +msgstr "" + +#: openstackclient/network/v2/router.py:312 +msgid "Subnet to be removed (name or ID)" +msgstr "" + +#: openstackclient/network/v2/router.py:334 +msgid "Router to modify (name or ID)" +msgstr "" + +#: openstackclient/network/v2/router.py:339 +msgid "Set router name" +msgstr "" + +#: openstackclient/network/v2/router.py:346 +msgid "Enable router" +msgstr "" + +#: openstackclient/network/v2/router.py:357 +msgid "Set router to distributed mode (disabled router only)" +msgstr "" + +#: openstackclient/network/v2/router.py:362 +msgid "Set router to centralized mode (disabled router only)" +msgstr "" + +#: openstackclient/network/v2/router.py:372 +msgid "" +"Routes associated with the router destination: destination subnet (in " +"CIDR notation) gateway: nexthop IP address (repeat option to set multiple" +" routes)" +msgstr "" + +#: openstackclient/network/v2/router.py:380 +msgid "Clear routes associated with the router" +msgstr "" + +#: openstackclient/network/v2/router.py:412 +msgid "Router to display (name or ID)" +msgstr "" + +#: openstackclient/network/v2/security_group.py:103 +#: openstackclient/network/v2/security_group.py:249 +msgid "New security group name" +msgstr "" + +#: openstackclient/network/v2/security_group.py:108 +msgid "Security group description" +msgstr "" + +#: openstackclient/network/v2/security_group.py:173 +msgid "Security group to delete (name or ID)" +msgstr "" + +#: openstackclient/network/v2/security_group.py:208 +msgid "Display information from all projects (admin only)" +msgstr "" + +#: openstackclient/network/v2/security_group.py:244 +msgid "Security group to modify (name or ID)" +msgstr "" + +#: openstackclient/network/v2/security_group.py:254 +msgid "New security group description" +msgstr "" + +#: openstackclient/network/v2/security_group.py:299 +msgid "Security group to display (name or ID)" +msgstr "" + +#: openstackclient/network/v2/security_group_rule.py:82 +msgid "IP protocol (icmp, tcp, udp; default: tcp)" +msgstr "" + +#: openstackclient/network/v2/security_group_rule.py:88 +msgid "" +"Source IP address block (may use CIDR notation; default for IPv4 rule: " +"0.0.0.0/0)" +msgstr "" + +#: openstackclient/network/v2/security_group_rule.py:94 +msgid "Source security group (name or ID)" +msgstr "" + +#: openstackclient/network/v2/security_group_rule.py:101 +msgid "" +"Destination port, may be a single port or port range: 137:139 (only " +"required for IP protocols tcp and udp)" +msgstr "" + +#: openstackclient/network/v2/security_group_rule.py:111 +msgid "Rule applies to incoming network traffic (default)" +msgstr "" + +#: openstackclient/network/v2/security_group_rule.py:116 +msgid "Rule applies to outgoing network traffic" +msgstr "" + +#: openstackclient/network/v2/security_group_rule.py:122 +msgid "Ethertype of network traffic (IPv4, IPv6; default: IPv4)" +msgstr "" + +#: openstackclient/network/v2/security_group_rule.py:221 +msgid "Security group rule to delete (ID only)" +msgstr "" + +#: openstackclient/network/v2/security_group_rule.py:241 +msgid "List all rules in this security group (name or ID)" +msgstr "" + +#: openstackclient/network/v2/security_group_rule.py:336 +msgid "Security group rule to display (ID only)" +msgstr "" + +#: openstackclient/network/v2/subnet.py:54 +msgid "" +"Allocation pool IP addresses for this subnet e.g.: " +"start=192.168.199.2,end=192.168.199.254 (repeat option to add multiple IP" +" addresses)" +msgstr "" + +#: openstackclient/network/v2/subnet.py:63 +msgid "DNS server for this subnet (repeat option to set multiple DNS servers)" +msgstr "" + +#: openstackclient/network/v2/subnet.py:72 +msgid "" +"Additional route for this subnet e.g.: " +"destination=10.10.0.0/16,gateway=192.168.71.254 destination: destination " +"subnet (in CIDR notation) gateway: nexthop IP address (repeat option to " +"add multiple routes)" +msgstr "" + +#: openstackclient/network/v2/subnet.py:178 +msgid "New subnet name" +msgstr "" + +#: openstackclient/network/v2/subnet.py:190 +msgid "Subnet pool from which this subnet will obtain a CIDR (Name or ID)" +msgstr "" + +#: openstackclient/network/v2/subnet.py:196 +msgid "Use default subnet pool for --ip-version" +msgstr "" + +#: openstackclient/network/v2/subnet.py:201 +msgid "Prefix length for subnet allocation from subnet pool" +msgstr "" + +#: openstackclient/network/v2/subnet.py:206 +msgid "" +"Subnet range in CIDR notation (required if --subnet-pool is not " +"specified, optional otherwise)" +msgstr "" + +#: openstackclient/network/v2/subnet.py:215 +msgid "Enable DHCP (default)" +msgstr "" + +#: openstackclient/network/v2/subnet.py:220 +#: openstackclient/network/v2/subnet.py:348 +msgid "Disable DHCP" +msgstr "" + +#: openstackclient/network/v2/subnet.py:226 +msgid "" +"Specify a gateway for the subnet. The three options are: <ip-address>: " +"Specific IP address to use as the gateway, 'auto': Gateway address should" +" automatically be chosen from within the subnet itself, 'none': This " +"subnet will not use a gateway, e.g.: --gateway 192.168.9.1, --gateway " +"auto, --gateway none (default is 'auto')" +msgstr "" + +#: openstackclient/network/v2/subnet.py:238 +msgid "" +"IP version (default is 4). Note that when subnet pool is specified, IP " +"version is determined from the subnet pool and this option is ignored" +msgstr "" + +#: openstackclient/network/v2/subnet.py:245 +msgid "" +"IPv6 RA (Router Advertisement) mode, valid modes: [dhcpv6-stateful, " +"dhcpv6-stateless, slaac]" +msgstr "" + +#: openstackclient/network/v2/subnet.py:251 +msgid "IPv6 address mode, valid modes: [dhcpv6-stateful, dhcpv6-stateless, slaac]" +msgstr "" + +#: openstackclient/network/v2/subnet.py:258 +msgid "Network this subnet belongs to (name or ID)" +msgstr "" + +#: openstackclient/network/v2/subnet.py:280 +msgid "Subnet to delete (name or ID)" +msgstr "" + +#: openstackclient/network/v2/subnet.py:331 +msgid "Subnet to modify (name or ID)" +msgstr "" + +#: openstackclient/network/v2/subnet.py:336 +msgid "Updated name of the subnet" +msgstr "" + +#: openstackclient/network/v2/subnet.py:343 +msgid "Enable DHCP" +msgstr "" + +#: openstackclient/network/v2/subnet.py:353 +msgid "" +"Specify a gateway for the subnet. The options are: <ip-address>: Specific" +" IP address to use as the gateway, 'none': This subnet will not use a " +"gateway, e.g.: --gateway 192.168.9.1, --gateway none" +msgstr "" + +#: openstackclient/network/v2/subnet.py:387 +msgid "Subnet to display (name or ID)" +msgstr "" + +#: openstackclient/network/v2/subnet_pool.py:77 +msgid "" +"Set subnet pool prefixes (in CIDR notation) (repeat option to set " +"multiple prefixes)" +msgstr "" + +#: openstackclient/network/v2/subnet_pool.py:84 +msgid "Set subnet pool default prefix length" +msgstr "" + +#: openstackclient/network/v2/subnet_pool.py:90 +msgid "Set subnet pool minimum prefix length" +msgstr "" + +#: openstackclient/network/v2/subnet_pool.py:96 +msgid "Set subnet pool maximum prefix length" +msgstr "" + +#: openstackclient/network/v2/subnet_pool.py:108 +msgid "Name of the new subnet pool" +msgstr "" + +#: openstackclient/network/v2/subnet_pool.py:120 +#: openstackclient/network/v2/subnet_pool.py:226 +msgid "" +"Set address scope associated with the subnet pool (name or ID), prefixes " +"must be unique across address scopes" +msgstr "" + +#: openstackclient/network/v2/subnet_pool.py:146 +msgid "Subnet pool to delete (name or ID)" +msgstr "" + +#: openstackclient/network/v2/subnet_pool.py:214 +msgid "Subnet pool to modify (name or ID)" +msgstr "" + +#: openstackclient/network/v2/subnet_pool.py:219 +msgid "Set subnet pool name" +msgstr "" + +#: openstackclient/network/v2/subnet_pool.py:233 +msgid "Remove address scope associated with the subnet pool" +msgstr "" + +#: openstackclient/network/v2/subnet_pool.py:262 +msgid "Subnet pool to display (name or ID)" +msgstr "" + diff --git a/openstackclient/locale/zh_TW/LC_MESSAGES/openstackclient.po b/openstackclient/locale/zh_TW/LC_MESSAGES/openstackclient.po index 2603266..a121aca 100644 --- a/openstackclient/locale/zh_TW/LC_MESSAGES/openstackclient.po +++ b/openstackclient/locale/zh_TW/LC_MESSAGES/openstackclient.po @@ -4,12 +4,12 @@ # python-openstackclient project. # # Translators: -# OpenStack Infra <zanata@openstack.org>, 2015. #zanata +# Andreas Jaeger <jaegerandi@gmail.com>, 2016. #zanata msgid "" msgstr "" -"Project-Id-Version: python-openstackclient 2.0.1.dev168\n" -"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2016-01-19 02:20+0000\n" +"Project-Id-Version: python-openstackclient 2.2.1.dev235\n" +"Report-Msgid-Bugs-To: https://bugs.launchpad.net/openstack-i18n/\n" +"POT-Creation-Date: 2016-04-19 05:02+0000\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" @@ -126,19 +126,12 @@ msgid "Endpoint ID to display" msgstr "要顯示的端點識別號" #, python-format -msgid "Error creating server snapshot: %s" -msgstr "新增雲實例即時存檔時出錯:%s" - -#, python-format msgid "Error creating server: %s" msgstr "新增雲實例時出錯:%s" msgid "Error retrieving diagnostics data" msgstr "獲得診斷資料時出錯" -msgid "File to inject into image before boot (repeat for multiple files)" -msgstr "在開機前要注入映像檔的檔案(為多個檔案重復指定)" - msgid "Filter by parent region ID" msgstr "以父地區識別號來篩選" @@ -287,9 +280,6 @@ msgstr "要刪除的專案(名稱或識別號)" msgid "Prompt interactively for password" msgstr "為密碼互動提示" -msgid "Property key to remove from server (repeat to unset multiple values)" -msgstr "要從雲實例上移除的屬性鍵(重復來取消選擇多個值)" - msgid "" "Property to add/change for this server (repeat option to set multiple " "properties)" @@ -385,9 +375,6 @@ msgstr "要顯示的伺服器(類型、名稱或識別號)" msgid "Set a project property (repeat option to set multiple properties)" msgstr "設定專案屬性(重復這選項來設定多個屬性)" -msgid "Set a property on this server (repeat for multiple values)" -msgstr "為此伺服器設定屬性(為多個值重複設定)" - msgid "Set default project (name or ID)" msgstr "設定預設專案(名稱或識別號)" diff --git a/openstackclient/network/client.py b/openstackclient/network/client.py index 7714c52..be06d2b 100644 --- a/openstackclient/network/client.py +++ b/openstackclient/network/client.py @@ -14,6 +14,7 @@ import logging from openstack import connection +from openstack import profile from openstackclient.common import utils @@ -31,7 +32,13 @@ API_VERSIONS = { def make_client(instance): """Returns a network proxy""" - conn = connection.Connection(authenticator=instance.session.auth) + prof = profile.Profile() + prof.set_region(API_NAME, instance._region_name) + prof.set_version(API_NAME, instance._api_version[API_NAME]) + conn = connection.Connection(authenticator=instance.session.auth, + verify=instance.session.verify, + cert=instance.session.cert, + profile=prof) LOG.debug('Connection: %s', conn) LOG.debug('Network client initialized using OpenStack SDK: %s', conn.network) diff --git a/openstackclient/network/common.py b/openstackclient/network/common.py index 1e2c4cc..a3047d8 100644 --- a/openstackclient/network/common.py +++ b/openstackclient/network/common.py @@ -15,6 +15,7 @@ import abc import six from openstackclient.common import command +from openstackclient.common import exceptions @six.add_metaclass(abc.ABCMeta) @@ -69,6 +70,42 @@ class NetworkAndComputeCommand(command.Command): @six.add_metaclass(abc.ABCMeta) +class NetworkAndComputeDelete(NetworkAndComputeCommand): + """Network and Compute Delete + + Delete class for commands that support implementation via + the network or compute endpoint. Such commands have different + implementations for take_action() and may even have different + arguments. This class supports bulk deletion, and error handling + following the rules in doc/source/command-errors.rst. + """ + + def take_action(self, parsed_args): + ret = 0 + resources = getattr(parsed_args, self.resource, []) + + for r in resources: + self.r = r + try: + if self.app.client_manager.is_network_endpoint_enabled(): + self.take_action_network(self.app.client_manager.network, + parsed_args) + else: + self.take_action_compute(self.app.client_manager.compute, + parsed_args) + except Exception as e: + self.app.log.error("Failed to delete %s with name or ID " + "'%s': %s" % (self.resource, r, e)) + ret += 1 + + if ret: + total = len(resources) + msg = "%s of %s %ss failed to delete." % (ret, total, + self.resource) + raise exceptions.CommandError(msg) + + +@six.add_metaclass(abc.ABCMeta) class NetworkAndComputeLister(command.Lister): """Network and Compute Lister diff --git a/openstackclient/network/utils.py b/openstackclient/network/utils.py new file mode 100644 index 0000000..287f027 --- /dev/null +++ b/openstackclient/network/utils.py @@ -0,0 +1,41 @@ +# 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. +# + + +# Transform compute security group rule for display. +def transform_compute_security_group_rule(sg_rule): + info = {} + info.update(sg_rule) + from_port = info.pop('from_port') + to_port = info.pop('to_port') + if isinstance(from_port, int) and isinstance(to_port, int): + port_range = {'port_range': "%u:%u" % (from_port, to_port)} + elif from_port is None and to_port is None: + port_range = {'port_range': ""} + else: + port_range = {'port_range': "%s:%s" % (from_port, to_port)} + info.update(port_range) + if 'cidr' in info['ip_range']: + info['ip_range'] = info['ip_range']['cidr'] + else: + info['ip_range'] = '' + if info['ip_protocol'] is None: + info['ip_protocol'] = '' + elif info['ip_protocol'].lower() == 'icmp': + info['port_range'] = '' + group = info.pop('group') + if 'name' in group: + info['remote_security_group'] = group['name'] + else: + info['remote_security_group'] = '' + return info diff --git a/openstackclient/network/v2/address_scope.py b/openstackclient/network/v2/address_scope.py new file mode 100644 index 0000000..614900c --- /dev/null +++ b/openstackclient/network/v2/address_scope.py @@ -0,0 +1,214 @@ +# 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. +# + +"""Address scope action implementations""" + +from openstackclient.common import command +from openstackclient.common import exceptions +from openstackclient.common import utils +from openstackclient.i18n import _ +from openstackclient.identity import common as identity_common + + +def _get_columns(item): + columns = list(item.keys()) + if 'tenant_id' in columns: + columns.remove('tenant_id') + columns.append('project_id') + + return tuple(sorted(columns)) + + +def _get_attrs(client_manager, parsed_args): + attrs = {} + attrs['name'] = parsed_args.name + attrs['ip_version'] = parsed_args.ip_version + if parsed_args.share: + attrs['shared'] = True + if parsed_args.no_share: + attrs['shared'] = False + if 'project' in parsed_args and parsed_args.project is not None: + identity_client = client_manager.identity + project_id = identity_common.find_project( + identity_client, + parsed_args.project, + parsed_args.project_domain, + ).id + attrs['tenant_id'] = project_id + + return attrs + + +class CreateAddressScope(command.ShowOne): + """Create a new Address Scope""" + + def get_parser(self, prog_name): + parser = super(CreateAddressScope, self).get_parser(prog_name) + parser.add_argument( + 'name', + metavar="<name>", + help=_("New address scope name") + ) + parser.add_argument( + '--ip-version', + type=int, + default=4, + choices=[4, 6], + help=_("IP version (default is 4)") + ) + parser.add_argument( + '--project', + metavar="<project>", + help=_("Owner's project (name or ID)") + ) + identity_common.add_project_domain_option_to_parser(parser) + + share_group = parser.add_mutually_exclusive_group() + share_group.add_argument( + '--share', + action='store_true', + help=_('Share the address scope between projects') + ) + share_group.add_argument( + '--no-share', + action='store_true', + help=_('Do not share the address scope between projects (default)') + ) + + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + attrs = _get_attrs(self.app.client_manager, parsed_args) + obj = client.create_address_scope(**attrs) + columns = _get_columns(obj) + data = utils.get_item_properties(obj, columns, formatters={}) + + return columns, data + + +class DeleteAddressScope(command.Command): + """Delete an address scope""" + + def get_parser(self, prog_name): + parser = super(DeleteAddressScope, self).get_parser(prog_name) + parser.add_argument( + 'address_scope', + metavar="<address-scope>", + help=_("Address scope to delete (name or ID)") + ) + + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + obj = client.find_address_scope(parsed_args.address_scope) + client.delete_address_scope(obj) + + +class ListAddressScope(command.Lister): + """List address scopes""" + + def take_action(self, parsed_args): + client = self.app.client_manager.network + columns = ( + 'id', + 'name', + 'ip_version', + 'shared', + 'tenant_id', + ) + column_headers = ( + 'ID', + 'Name', + 'IP Version', + 'Shared', + 'Project', + ) + data = client.address_scopes() + + return (column_headers, + (utils.get_item_properties( + s, columns, formatters={}, + ) for s in data)) + + +class SetAddressScope(command.Command): + """Set address scope properties""" + + def get_parser(self, prog_name): + parser = super(SetAddressScope, self).get_parser(prog_name) + parser.add_argument( + 'address_scope', + metavar="<address-scope>", + help=_("Address scope to modify (name or ID)") + ) + parser.add_argument( + '--name', + metavar="<name>", + help=_('Set address scope name') + ) + share_group = parser.add_mutually_exclusive_group() + share_group.add_argument( + '--share', + action='store_true', + help=_('Share the address scope between projects') + ) + share_group.add_argument( + '--no-share', + action='store_true', + help=_('Do not share the address scope between projects') + ) + + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + obj = client.find_address_scope( + parsed_args.address_scope, + ignore_missing=False) + attrs = {} + if parsed_args.name is not None: + attrs['name'] = parsed_args.name + if parsed_args.share: + attrs['shared'] = True + if parsed_args.no_share: + attrs['shared'] = False + if attrs == {}: + msg = _("Nothing specified to be set.") + raise exceptions.CommandError(msg) + client.update_address_scope(obj, **attrs) + + +class ShowAddressScope(command.ShowOne): + """Display address scope details""" + + def get_parser(self, prog_name): + parser = super(ShowAddressScope, self).get_parser(prog_name) + parser.add_argument( + 'address_scope', + metavar="<address-scope>", + help=_("Address scope to display (name or ID)") + ) + + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + obj = client.find_address_scope( + parsed_args.address_scope, + ignore_missing=False) + columns = _get_columns(obj) + data = utils.get_item_properties(obj, columns, formatters={}) + + return columns, data diff --git a/openstackclient/network/v2/floating_ip.py b/openstackclient/network/v2/floating_ip.py index e0a65a4..21f8659 100644 --- a/openstackclient/network/v2/floating_ip.py +++ b/openstackclient/network/v2/floating_ip.py @@ -14,17 +14,101 @@ """IP Floating action implementations""" from openstackclient.common import utils +from openstackclient.i18n import _ from openstackclient.network import common def _get_columns(item): - columns = item.keys() + columns = list(item.keys()) if 'tenant_id' in columns: columns.remove('tenant_id') columns.append('project_id') return tuple(sorted(columns)) +def _get_attrs(client_manager, parsed_args): + attrs = {} + network_client = client_manager.network + + if parsed_args.network is not None: + network = network_client.find_network(parsed_args.network, + ignore_missing=False) + attrs['floating_network_id'] = network.id + + if parsed_args.subnet is not None: + subnet = network_client.find_subnet(parsed_args.subnet, + ignore_missing=False) + attrs['subnet_id'] = subnet.id + + if parsed_args.port is not None: + port = network_client.find_port(parsed_args.port, + ignore_missing=False) + attrs['port_id'] = port.id + + if parsed_args.floating_ip_address is not None: + attrs['floating_ip_address'] = parsed_args.floating_ip_address + + if parsed_args.fixed_ip_address is not None: + attrs['fixed_ip_address'] = parsed_args.fixed_ip_address + + return attrs + + +class CreateFloatingIP(common.NetworkAndComputeShowOne): + """Create floating IP""" + + def update_parser_common(self, parser): + # In Compute v2 network, floating IPs could be allocated from floating + # IP pools, which are actually external networks. So deprecate the + # parameter "pool", and use "network" instead. + parser.add_argument( + 'network', + metavar='<network>', + help=_("Network to allocate floating IP from (name or ID)") + ) + return parser + + def update_parser_network(self, parser): + parser.add_argument( + '--subnet', + metavar='<subnet>', + help=_("Subnet on which you want to create the floating IP " + "(name or ID)") + ) + parser.add_argument( + '--port', + metavar='<port>', + help=_("Port to be associated with the floating IP " + "(name or ID)") + ) + parser.add_argument( + '--floating-ip-address', + metavar='<floating-ip-address>', + dest='floating_ip_address', + help=_("Floating IP address") + ) + parser.add_argument( + '--fixed-ip-address', + metavar='<fixed-ip-address>', + dest='fixed_ip_address', + help=_("Fixed IP address mapped to the floating IP") + ) + return parser + + def take_action_network(self, client, parsed_args): + attrs = _get_attrs(self.app.client_manager, parsed_args) + obj = client.create_ip(**attrs) + columns = _get_columns(obj) + data = utils.get_item_properties(obj, columns) + return (columns, data) + + def take_action_compute(self, client, parsed_args): + obj = client.floating_ips.create(parsed_args.network) + columns = _get_columns(obj._info) + data = utils.get_dict_properties(obj._info, columns) + return (columns, data) + + class DeleteFloatingIP(common.NetworkAndComputeCommand): """Delete floating IP""" @@ -32,7 +116,7 @@ class DeleteFloatingIP(common.NetworkAndComputeCommand): parser.add_argument( 'floating_ip', metavar="<floating-ip>", - help=("Floating IP to delete (IP address or ID)") + help=_("Floating IP to delete (IP address or ID)") ) return parser @@ -106,7 +190,7 @@ class ShowFloatingIP(common.NetworkAndComputeShowOne): parser.add_argument( 'floating_ip', metavar="<floating-ip>", - help=("Floating IP to display (IP address or ID)") + help=_("Floating IP to display (IP address or ID)") ) return parser diff --git a/openstackclient/network/v2/network.py b/openstackclient/network/v2/network.py index 308e0e5..bf01e2e 100644 --- a/openstackclient/network/v2/network.py +++ b/openstackclient/network/v2/network.py @@ -16,6 +16,7 @@ from openstackclient.common import command from openstackclient.common import exceptions from openstackclient.common import utils +from openstackclient.i18n import _ from openstackclient.identity import common as identity_common from openstackclient.network import common @@ -31,20 +32,17 @@ def _format_router_external(item): _formatters = { 'subnets': utils.format_list, 'admin_state_up': _format_admin_state, - 'router_external': _format_router_external, + 'router:external': _format_router_external, 'availability_zones': utils.format_list, 'availability_zone_hints': utils.format_list, } def _get_columns(item): - columns = item.keys() + columns = list(item.keys()) if 'tenant_id' in columns: columns.remove('tenant_id') columns.append('project_id') - if 'router:external' in columns: - columns.remove('router:external') - columns.append('router_external') return tuple(sorted(columns)) @@ -52,10 +50,14 @@ def _get_attrs(client_manager, parsed_args): attrs = {} if parsed_args.name is not None: attrs['name'] = str(parsed_args.name) - if parsed_args.admin_state is not None: - attrs['admin_state_up'] = parsed_args.admin_state - if parsed_args.shared is not None: - attrs['shared'] = parsed_args.shared + if parsed_args.enable: + attrs['admin_state_up'] = True + if parsed_args.disable: + attrs['admin_state_up'] = False + if parsed_args.share: + attrs['shared'] = True + if parsed_args.no_share: + attrs['shared'] = False # "network set" command doesn't support setting project. if 'project' in parsed_args and parsed_args.project is not None: @@ -72,18 +74,75 @@ def _get_attrs(client_manager, parsed_args): parsed_args.availability_zone_hints is not None: attrs['availability_zone_hints'] = parsed_args.availability_zone_hints + # update_external_network_options + if parsed_args.internal: + attrs['router:external'] = False + if parsed_args.external: + attrs['router:external'] = True + if parsed_args.no_default: + attrs['is_default'] = False + if parsed_args.default: + attrs['is_default'] = True + # Update Provider network options + if parsed_args.provider_network_type: + attrs['provider:network_type'] = parsed_args.provider_network_type + if parsed_args.physical_network: + attrs['provider:physical_network'] = parsed_args.physical_network + if parsed_args.segmentation_id: + attrs['provider:segmentation_id'] = parsed_args.segmentation_id + # Update VLAN Transparency for networks + if parsed_args.transparent_vlan: + attrs['vlan_transparent'] = True + if parsed_args.no_transparent_vlan: + attrs['vlan_transparent'] = False return attrs +def _add_additional_network_options(parser): + # Add additional network options + + parser.add_argument( + '--provider-network-type', + metavar='<provider-network-type>', + choices=['flat', 'gre', 'local', + 'vlan', 'vxlan'], + help=_("The physical mechanism by which the virtual network " + "is implemented. The supported options are: " + "flat, gre, local, vlan, vxlan")) + parser.add_argument( + '--provider-physical-network', + metavar='<provider-physical-network>', + dest='physical_network', + help=_("Name of the physical network over which the virtual " + "network is implemented")) + parser.add_argument( + '--provider-segment', + metavar='<provider-segment>', + dest='segmentation_id', + help=_("VLAN ID for VLAN networks or Tunnel ID for GRE/VXLAN " + "networks")) + + vlan_transparent_grp = parser.add_mutually_exclusive_group() + vlan_transparent_grp.add_argument( + '--transparent-vlan', + action='store_true', + help=_("Make the network VLAN transparent")) + vlan_transparent_grp.add_argument( + '--no-transparent-vlan', + action='store_true', + help=_("Do not make the network VLAN transparent")) + + def _get_attrs_compute(client_manager, parsed_args): attrs = {} if parsed_args.name is not None: attrs['label'] = str(parsed_args.name) - if parsed_args.shared is not None: - attrs['share_address'] = parsed_args.shared + if parsed_args.share: + attrs['share_address'] = True + if parsed_args.no_share: + attrs['share_address'] = False if parsed_args.subnet is not None: attrs['cidr'] = parsed_args.subnet - return attrs @@ -94,21 +153,19 @@ class CreateNetwork(common.NetworkAndComputeShowOne): parser.add_argument( 'name', metavar='<name>', - help='New network name', + help=_("New network name") ) share_group = parser.add_mutually_exclusive_group() share_group.add_argument( '--share', - dest='shared', action='store_true', default=None, - help='Share the network between projects', + help=_("Share the network between projects") ) share_group.add_argument( '--no-share', - dest='shared', - action='store_false', - help='Do not share the network between projects', + action='store_true', + help=_("Do not share the network between projects") ) return parser @@ -116,21 +173,19 @@ class CreateNetwork(common.NetworkAndComputeShowOne): admin_group = parser.add_mutually_exclusive_group() admin_group.add_argument( '--enable', - dest='admin_state', action='store_true', default=True, - help='Enable network (default)', + help=_("Enable network (default)") ) admin_group.add_argument( '--disable', - dest='admin_state', - action='store_false', - help='Disable network', + action='store_true', + help=_("Disable network") ) parser.add_argument( '--project', metavar='<project>', - help="Owner's project (name or ID)" + help=_("Owner's project (name or ID)") ) identity_common.add_project_domain_option_to_parser(parser) parser.add_argument( @@ -138,17 +193,43 @@ class CreateNetwork(common.NetworkAndComputeShowOne): action='append', dest='availability_zone_hints', metavar='<availability-zone>', - help='Availability Zone in which to create this network ' - '(requires the Network Availability Zone extension, ' - 'this option can be repeated).', + help=_("Availability Zone in which to create this network " + "(Network Availability Zone extension required, " + "repeat option to set multiple availability zones)") + ) + external_router_grp = parser.add_mutually_exclusive_group() + external_router_grp.add_argument( + '--external', + action='store_true', + help=_("Set this network as an external network " + "(external-net extension required)") ) + external_router_grp.add_argument( + '--internal', + action='store_true', + help=_("Set this network as an internal network (default)") + ) + default_router_grp = parser.add_mutually_exclusive_group() + default_router_grp.add_argument( + '--default', + action='store_true', + help=_("Specify if this network should be used as " + "the default external network") + ) + default_router_grp.add_argument( + '--no-default', + action='store_true', + help=_("Do not use the network as the default external network " + "(default)") + ) + _add_additional_network_options(parser) return parser def update_parser_compute(self, parser): parser.add_argument( '--subnet', metavar='<subnet>', - help="IPv4 subnet for fixed IPs (in CIDR notation)" + help=_("IPv4 subnet for fixed IPs (in CIDR notation)") ) return parser @@ -162,35 +243,35 @@ class CreateNetwork(common.NetworkAndComputeShowOne): def take_action_compute(self, client, parsed_args): attrs = _get_attrs_compute(self.app.client_manager, parsed_args) obj = client.networks.create(**attrs) - columns = tuple(sorted(obj._info.keys())) + columns = _get_columns(obj._info) data = utils.get_dict_properties(obj._info, columns) return (columns, data) -class DeleteNetwork(common.NetworkAndComputeCommand): +class DeleteNetwork(common.NetworkAndComputeDelete): """Delete network(s)""" + # Used by base class to find resources in parsed_args. + resource = 'network' + r = None + def update_parser_common(self, parser): parser.add_argument( 'network', metavar="<network>", nargs="+", - help=("Network(s) to delete (name or ID)") + help=_("Network(s) to delete (name or ID)") ) + return parser def take_action_network(self, client, parsed_args): - for network in parsed_args.network: - obj = client.find_network(network) - client.delete_network(obj) + obj = client.find_network(self.r, ignore_missing=False) + client.delete_network(obj) def take_action_compute(self, client, parsed_args): - for network in parsed_args.network: - network = utils.find_resource( - client.networks, - network, - ) - client.networks.delete(network.id) + network = utils.find_resource(client.networks, self.r) + client.networks.delete(network.id) class ListNetwork(common.NetworkAndComputeLister): @@ -201,13 +282,13 @@ class ListNetwork(common.NetworkAndComputeLister): '--external', action='store_true', default=False, - help='List external networks', + help=_("List external networks") ) parser.add_argument( '--long', action='store_true', default=False, - help='List additional fields in output', + help=_("List additional fields in output") ) return parser @@ -222,7 +303,7 @@ class ListNetwork(common.NetworkAndComputeLister): 'shared', 'subnets', 'provider_network_type', - 'router_external', + 'router:external', 'availability_zones', ) column_headers = ( @@ -291,41 +372,61 @@ class SetNetwork(command.Command): parser.add_argument( 'network', metavar="<network>", - help=("Network to modify (name or ID)") + help=_("Network to modify (name or ID)") ) parser.add_argument( '--name', metavar='<name>', - help='Set network name', + help=_("Set network name") ) admin_group = parser.add_mutually_exclusive_group() admin_group.add_argument( '--enable', - dest='admin_state', action='store_true', default=None, - help='Enable network', + help=_("Enable network") ) admin_group.add_argument( '--disable', - dest='admin_state', - action='store_false', - help='Disable network', + action='store_true', + help=_("Disable network") ) share_group = parser.add_mutually_exclusive_group() share_group.add_argument( '--share', - dest='shared', action='store_true', default=None, - help='Share the network between projects', + help=_("Share the network between projects") ) share_group.add_argument( '--no-share', - dest='shared', - action='store_false', - help='Do not share the network between projects', + action='store_true', + help=_("Do not share the network between projects") + ) + external_router_grp = parser.add_mutually_exclusive_group() + external_router_grp.add_argument( + '--external', + action='store_true', + help=_("Set this network as an external network " + "(external-net extension required)") ) + external_router_grp.add_argument( + '--internal', + action='store_true', + help=_("Set this network as an internal network") + ) + default_router_grp = parser.add_mutually_exclusive_group() + default_router_grp.add_argument( + '--default', + action='store_true', + help=_("Set the network as the default external network") + ) + default_router_grp.add_argument( + '--no-default', + action='store_true', + help=_("Do not use the network as the default external network") + ) + _add_additional_network_options(parser) return parser def take_action(self, parsed_args): @@ -334,11 +435,10 @@ class SetNetwork(command.Command): attrs = _get_attrs(self.app.client_manager, parsed_args) if attrs == {}: - msg = "Nothing specified to be set" + msg = _("Nothing specified to be set") raise exceptions.CommandError(msg) client.update_network(obj, **attrs) - return class ShowNetwork(common.NetworkAndComputeShowOne): @@ -348,7 +448,7 @@ class ShowNetwork(common.NetworkAndComputeShowOne): parser.add_argument( 'network', metavar="<network>", - help=("Network to display (name or ID)") + help=_("Network to display (name or ID)") ) return parser @@ -359,10 +459,10 @@ class ShowNetwork(common.NetworkAndComputeShowOne): return (columns, data) def take_action_compute(self, client, parsed_args): - network = utils.find_resource( + obj = utils.find_resource( client.networks, parsed_args.network, ) - columns = sorted(network._info.keys()) - data = utils.get_dict_properties(network._info, columns) + columns = _get_columns(obj._info) + data = utils.get_dict_properties(obj._info, columns) return (columns, data) diff --git a/openstackclient/network/v2/port.py b/openstackclient/network/v2/port.py index 19b3701..ca02281 100644 --- a/openstackclient/network/v2/port.py +++ b/openstackclient/network/v2/port.py @@ -13,8 +13,18 @@ """Port action implementations""" +import argparse +import logging + from openstackclient.common import command +from openstackclient.common import exceptions +from openstackclient.common import parseractions from openstackclient.common import utils +from openstackclient.i18n import _ +from openstackclient.identity import common as identity_common + + +LOG = logging.getLogger(__name__) def _format_admin_state(state): @@ -34,7 +44,7 @@ _formatters = { def _get_columns(item): - columns = item.keys() + columns = list(item.keys()) if 'tenant_id' in columns: columns.remove('tenant_id') columns.append('project_id') @@ -49,7 +59,214 @@ def _get_columns(item): if binding_column in columns: columns.remove(binding_column) columns.append(binding_column.replace('binding:', 'binding_', 1)) - return sorted(columns) + return tuple(sorted(columns)) + + +def _get_attrs(client_manager, parsed_args): + attrs = {} + + # Handle deprecated options + # NOTE(dtroyer): --device-id and --host-id were deprecated in Mar 2016. + # Do not remove before 3.x release or Mar 2017. + if parsed_args.device_id: + attrs['device_id'] = parsed_args.device_id + LOG.warning(_( + 'The --device-id option is deprecated, ' + 'please use --device instead.' + )) + if parsed_args.host_id: + attrs['binding:host_id'] = parsed_args.host_id + LOG.warning(_( + 'The --host-id option is deprecated, ' + 'please use --host instead.' + )) + + if parsed_args.fixed_ip is not None: + attrs['fixed_ips'] = parsed_args.fixed_ip + if parsed_args.device: + attrs['device_id'] = parsed_args.device + if parsed_args.device_owner is not None: + attrs['device_owner'] = parsed_args.device_owner + if parsed_args.enable: + attrs['admin_state_up'] = True + if parsed_args.disable: + attrs['admin_state_up'] = False + if parsed_args.binding_profile is not None: + attrs['binding:profile'] = parsed_args.binding_profile + if parsed_args.vnic_type is not None: + attrs['binding:vnic_type'] = parsed_args.vnic_type + if parsed_args.host: + attrs['binding:host_id'] = parsed_args.host + + # It is possible that name is not updated during 'port set' + if parsed_args.name is not None: + attrs['name'] = str(parsed_args.name) + # The remaining options do not support 'port set' command, so they require + # additional check + if 'mac_address' in parsed_args and parsed_args.mac_address is not None: + attrs['mac_address'] = parsed_args.mac_address + if 'network' in parsed_args and parsed_args.network is not None: + attrs['network_id'] = parsed_args.network + if 'project' in parsed_args and parsed_args.project is not None: + # TODO(singhj): since 'project' logic is common among + # router, network, port etc., maybe move it to a common file. + identity_client = client_manager.identity + project_id = identity_common.find_project( + identity_client, + parsed_args.project, + parsed_args.project_domain, + ).id + attrs['tenant_id'] = project_id + + return attrs + + +def _prepare_fixed_ips(client_manager, parsed_args): + """Fix and properly format fixed_ip option. + + Appropriately convert any subnet names to their respective ids. + Convert fixed_ips in parsed args to be in valid dictionary format: + {'subnet': 'foo'}. + """ + client = client_manager.network + ips = [] + + if parsed_args.fixed_ip: + for ip_spec in parsed_args.fixed_ip: + if 'subnet' in ip_spec: + subnet_name_id = ip_spec['subnet'] + if subnet_name_id: + _subnet = client.find_subnet(subnet_name_id, + ignore_missing=False) + ip_spec['subnet_id'] = _subnet.id + del ip_spec['subnet'] + + if 'ip-address' in ip_spec: + ip_spec['ip_address'] = ip_spec['ip-address'] + del ip_spec['ip-address'] + + ips.append(ip_spec) + + if ips: + parsed_args.fixed_ip = ips + + +def _add_updatable_args(parser): + # NOTE(dtroyer): --device-id is deprecated in Mar 2016. Do not + # remove before 3.x release or Mar 2017. + device_group = parser.add_mutually_exclusive_group() + device_group.add_argument( + '--device', + metavar='<device-id>', + help=_("Port device ID") + ) + device_group.add_argument( + '--device-id', + metavar='<device-id>', + help=argparse.SUPPRESS, + ) + parser.add_argument( + '--device-owner', + metavar='<device-owner>', + help=_("Device owner of this port") + ) + parser.add_argument( + '--vnic-type', + metavar='<vnic-type>', + choices=['direct', 'direct-physical', 'macvtap', + 'normal', 'baremetal'], + help=_("VNIC type for this port (direct | direct-physical | " + "macvtap | normal | baremetal, default: normal)") + ) + # NOTE(dtroyer): --host-id is deprecated in Mar 2016. Do not + # remove before 3.x release or Mar 2017. + host_group = parser.add_mutually_exclusive_group() + host_group.add_argument( + '--host', + metavar='<host-id>', + help=_("Allocate port on host <host-id> (ID only)") + ) + host_group.add_argument( + '--host-id', + metavar='<host-id>', + help=argparse.SUPPRESS, + ) + + +class CreatePort(command.ShowOne): + """Create a new port""" + + def get_parser(self, prog_name): + parser = super(CreatePort, self).get_parser(prog_name) + + parser.add_argument( + '--network', + metavar='<network>', + required=True, + help=_("Network this port belongs to (name or ID)") + ) + _add_updatable_args(parser) + parser.add_argument( + '--fixed-ip', + metavar='subnet=<subnet>,ip-address=<ip-address>', + action=parseractions.MultiKeyValueAction, + optional_keys=['subnet', 'ip-address'], + help=_("Desired IP and/or subnet (name or ID) for this port: " + "subnet=<subnet>,ip-address=<ip-address> " + "(repeat option to set multiple fixed IP addresses)") + ) + parser.add_argument( + '--binding-profile', + metavar='<binding-profile>', + action=parseractions.KeyValueAction, + help=_("Custom data to be passed as binding:profile: " + "<key>=<value> " + "(repeat option to set multiple binding:profile data)") + ) + admin_group = parser.add_mutually_exclusive_group() + admin_group.add_argument( + '--enable', + action='store_true', + default=True, + help=_("Enable port (default)") + ) + admin_group.add_argument( + '--disable', + action='store_true', + help=_("Disable port") + ) + parser.add_argument( + '--mac-address', + metavar='<mac-address>', + help=_("MAC address of this port") + ) + parser.add_argument( + '--project', + metavar='<project>', + help=_("Owner's project (name or ID)") + ) + identity_common.add_project_domain_option_to_parser(parser) + parser.add_argument( + 'name', + metavar='<name>', + help=_("Name of this port") + ) + # TODO(singhj): Add support for extended options: + # qos,security groups,dhcp, address pairs + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + _network = client.find_network(parsed_args.network, + ignore_missing=False) + parsed_args.network = _network.id + _prepare_fixed_ips(self.app.client_manager, parsed_args) + attrs = _get_attrs(self.app.client_manager, parsed_args) + obj = client.create_port(**attrs) + columns = _get_columns(obj) + data = utils.get_item_properties(obj, columns, formatters=_formatters) + + return columns, data class DeletePort(command.Command): @@ -61,7 +278,7 @@ class DeletePort(command.Command): 'port', metavar="<port>", nargs="+", - help=("Port(s) to delete (name or ID)") + help=_("Port(s) to delete (name or ID)") ) return parser @@ -73,6 +290,135 @@ class DeletePort(command.Command): client.delete_port(res) +class ListPort(command.Lister): + """List ports""" + + def get_parser(self, prog_name): + parser = super(ListPort, self).get_parser(prog_name) + parser.add_argument( + '--router', + metavar='<router>', + dest='router', + help=_("List only ports attached to this router (name or ID)") + ) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + + columns = ( + 'id', + 'name', + 'mac_address', + 'fixed_ips', + ) + column_headers = ( + 'ID', + 'Name', + 'MAC Address', + 'Fixed IP Addresses', + ) + + filters = {} + if parsed_args.router: + _router = client.find_router(parsed_args.router, + ignore_missing=False) + filters = {'device_id': _router.id} + + data = client.ports(**filters) + + return (column_headers, + (utils.get_item_properties( + s, columns, + formatters=_formatters, + ) for s in data)) + + +class SetPort(command.Command): + """Set port properties""" + + def get_parser(self, prog_name): + parser = super(SetPort, self).get_parser(prog_name) + _add_updatable_args(parser) + admin_group = parser.add_mutually_exclusive_group() + admin_group.add_argument( + '--enable', + action='store_true', + default=None, + help=_("Enable port") + ) + admin_group.add_argument( + '--disable', + action='store_true', + help=_("Disable port") + ) + parser.add_argument( + '--name', + metavar="<name>", + help=_("Set port name") + ) + fixed_ip = parser.add_mutually_exclusive_group() + fixed_ip.add_argument( + '--fixed-ip', + metavar='subnet=<subnet>,ip-address=<ip-address>', + action=parseractions.MultiKeyValueAction, + optional_keys=['subnet', 'ip-address'], + help=_("Desired IP and/or subnet (name or ID) for this port: " + "subnet=<subnet>,ip-address=<ip-address> " + "(repeat option to set multiple fixed IP addresses)") + ) + fixed_ip.add_argument( + '--no-fixed-ip', + action='store_true', + help=_("Clear existing information of fixed IP addresses") + ) + binding_profile = parser.add_mutually_exclusive_group() + binding_profile.add_argument( + '--binding-profile', + metavar='<binding-profile>', + action=parseractions.KeyValueAction, + help=_("Custom data to be passed as binding:profile: " + "<key>=<value> " + "(repeat option to set multiple binding:profile data)") + ) + binding_profile.add_argument( + '--no-binding-profile', + action='store_true', + help=_("Clear existing information of binding:profile") + ) + parser.add_argument( + 'port', + metavar="<port>", + help=_("Port to modify (name or ID)") + ) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + + _prepare_fixed_ips(self.app.client_manager, parsed_args) + attrs = _get_attrs(self.app.client_manager, parsed_args) + obj = client.find_port(parsed_args.port, ignore_missing=False) + if 'binding:profile' in attrs: + attrs['binding:profile'].update(obj.binding_profile) + elif parsed_args.no_binding_profile: + attrs['binding:profile'] = {} + if 'fixed_ips' in attrs: + # When user unsets the fixed_ips, obj.fixed_ips = [{}]. + # Adding the obj.fixed_ips list to attrs['fixed_ips'] + # would therefore add an empty dictionary, while we need + # to append the attrs['fixed_ips'] iff there is some info + # in the obj.fixed_ips. Therefore I have opted for this `for` loop + attrs['fixed_ips'] += [ip for ip in obj.fixed_ips if ip] + elif parsed_args.no_fixed_ip: + attrs['fixed_ips'] = [] + + if attrs == {}: + msg = _("Nothing specified to be set") + raise exceptions.CommandError(msg) + client.update_port(obj, **attrs) + + class ShowPort(command.ShowOne): """Display port details""" @@ -81,7 +427,7 @@ class ShowPort(command.ShowOne): parser.add_argument( 'port', metavar="<port>", - help="Port to display (name or ID)" + help=_("Port to display (name or ID)") ) return parser @@ -90,4 +436,4 @@ class ShowPort(command.ShowOne): obj = client.find_port(parsed_args.port, ignore_missing=False) columns = _get_columns(obj) data = utils.get_item_properties(obj, columns, formatters=_formatters) - return (tuple(columns), data) + return columns, data diff --git a/openstackclient/network/v2/router.py b/openstackclient/network/v2/router.py index e4eea3f..a2f0df1 100644 --- a/openstackclient/network/v2/router.py +++ b/openstackclient/network/v2/router.py @@ -13,15 +13,21 @@ """Router action implementations""" +import argparse import json +import logging from openstackclient.common import command from openstackclient.common import exceptions from openstackclient.common import parseractions from openstackclient.common import utils +from openstackclient.i18n import _ from openstackclient.identity import common as identity_common +LOG = logging.getLogger(__name__) + + def _format_admin_state(state): return 'UP' if state else 'DOWN' @@ -33,31 +39,48 @@ def _format_external_gateway_info(info): return '' +def _format_routes(routes): + # Map the route keys to match --route option. + for route in routes: + if 'nexthop' in route: + route['gateway'] = route.pop('nexthop') + return utils.format_list_of_dicts(routes) + + _formatters = { 'admin_state_up': _format_admin_state, 'external_gateway_info': _format_external_gateway_info, 'availability_zones': utils.format_list, 'availability_zone_hints': utils.format_list, + 'routes': _format_routes, } +def _get_columns(item): + columns = list(item.keys()) + if 'tenant_id' in columns: + columns.remove('tenant_id') + columns.append('project_id') + return tuple(sorted(columns)) + + def _get_attrs(client_manager, parsed_args): attrs = {} if parsed_args.name is not None: attrs['name'] = str(parsed_args.name) - if parsed_args.admin_state_up is not None: - attrs['admin_state_up'] = parsed_args.admin_state_up - if parsed_args.distributed is not None: - attrs['distributed'] = parsed_args.distributed + if parsed_args.enable: + attrs['admin_state_up'] = True + if parsed_args.disable: + attrs['admin_state_up'] = False + # centralized is available only for SetRouter and not for CreateRouter + if 'centralized' in parsed_args and parsed_args.centralized: + attrs['distributed'] = False + if parsed_args.distributed: + attrs['distributed'] = True if ('availability_zone_hints' in parsed_args and parsed_args.availability_zone_hints is not None): attrs['availability_zone_hints'] = parsed_args.availability_zone_hints - if 'clear_routes' in parsed_args and parsed_args.clear_routes: - attrs['routes'] = [] - elif 'routes' in parsed_args and parsed_args.routes is not None: - attrs['routes'] = parsed_args.routes - # "router set" command doesn't support setting project. if 'project' in parsed_args and parsed_args.project is not None: identity_client = client_manager.identity @@ -74,6 +97,57 @@ def _get_attrs(client_manager, parsed_args): return attrs +class AddPortToRouter(command.Command): + """Add a port to a router""" + + def get_parser(self, prog_name): + parser = super(AddPortToRouter, self).get_parser(prog_name) + parser.add_argument( + 'router', + metavar='<router>', + help=_("Router to which port will be added (name or ID)") + ) + parser.add_argument( + 'port', + metavar='<port>', + help=_("Port to be added (name or ID)") + ) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + port = client.find_port(parsed_args.port, ignore_missing=False) + client.router_add_interface(client.find_router( + parsed_args.router, ignore_missing=False), port_id=port.id) + + +class AddSubnetToRouter(command.Command): + """Add a subnet to a router""" + + def get_parser(self, prog_name): + parser = super(AddSubnetToRouter, self).get_parser(prog_name) + parser.add_argument( + 'router', + metavar='<router>', + help=_("Router to which subnet will be added (name or ID)") + ) + parser.add_argument( + 'subnet', + metavar='<subnet>', + help=_("Subnet to be added (name or ID)") + ) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + subnet = client.find_subnet(parsed_args.subnet, + ignore_missing=False) + client.router_add_interface( + client.find_router(parsed_args.router, + ignore_missing=False), + subnet_id=subnet.id) + + class CreateRouter(command.ShowOne): """Create a new router""" @@ -82,45 +156,43 @@ class CreateRouter(command.ShowOne): parser.add_argument( 'name', metavar='<name>', - help="New router name", + help=_("New router name") ) admin_group = parser.add_mutually_exclusive_group() admin_group.add_argument( '--enable', - dest='admin_state_up', action='store_true', default=True, - help="Enable router (default)", + help=_("Enable router (default)") ) admin_group.add_argument( '--disable', - dest='admin_state_up', - action='store_false', - help="Disable router", + action='store_true', + help=_("Disable router") ) parser.add_argument( '--distributed', dest='distributed', action='store_true', default=False, - help="Create a distributed router", + help=_("Create a distributed router") ) parser.add_argument( '--project', metavar='<project>', - help="Owner's project (name or ID)", + help=_("Owner's project (name or ID)") ) + identity_common.add_project_domain_option_to_parser(parser) parser.add_argument( '--availability-zone-hint', metavar='<availability-zone>', action='append', dest='availability_zone_hints', - help='Availability Zone in which to create this router ' - '(requires the Router Availability Zone extension, ' - 'this option can be repeated).', + help=_("Availability Zone in which to create this router " + "(Router Availability Zone extension required, " + "repeat option to set multiple availability zones)") ) - identity_common.add_project_domain_option_to_parser(parser) return parser def take_action(self, parsed_args): @@ -129,14 +201,10 @@ class CreateRouter(command.ShowOne): attrs = _get_attrs(self.app.client_manager, parsed_args) obj = client.create_router(**attrs) - columns = sorted(obj.keys()) + columns = _get_columns(obj) data = utils.get_item_properties(obj, columns, formatters=_formatters) - if 'tenant_id' in columns: - # Rename "tenant_id" to "project_id". - index = columns.index('tenant_id') - columns[index] = 'project_id' - return (tuple(columns), data) + return columns, data class DeleteRouter(command.Command): @@ -148,7 +216,7 @@ class DeleteRouter(command.Command): 'router', metavar="<router>", nargs="+", - help=("Router(s) to delete (name or ID)") + help=_("Router(s) to delete (name or ID)") ) return parser @@ -168,7 +236,7 @@ class ListRouter(command.Lister): '--long', action='store_true', default=False, - help='List additional fields in output', + help=_("List additional fields in output") ) return parser @@ -213,6 +281,57 @@ class ListRouter(command.Lister): ) for s in data)) +class RemovePortFromRouter(command.Command): + """Remove a port from a router""" + + def get_parser(self, prog_name): + parser = super(RemovePortFromRouter, self).get_parser(prog_name) + parser.add_argument( + 'router', + metavar='<router>', + help=_("Router from which port will be removed (name or ID)") + ) + parser.add_argument( + 'port', + metavar='<port>', + help=_("Port to be removed (name or ID)") + ) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + port = client.find_port(parsed_args.port, ignore_missing=False) + client.router_remove_interface(client.find_router( + parsed_args.router, ignore_missing=False), port_id=port.id) + + +class RemoveSubnetFromRouter(command.Command): + """Remove a subnet from a router""" + + def get_parser(self, prog_name): + parser = super(RemoveSubnetFromRouter, self).get_parser(prog_name) + parser.add_argument( + 'router', + metavar='<router>', + help=_("Router from which the subnet will be removed (name or ID)") + ) + parser.add_argument( + 'subnet', + metavar='<subnet>', + help=_("Subnet to be removed (name or ID)") + ) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + subnet = client.find_subnet(parsed_args.subnet, + ignore_missing=False) + client.router_remove_interface( + client.find_router(parsed_args.router, + ignore_missing=False), + subnet_id=subnet.id) + + class SetRouter(command.Command): """Set router properties""" @@ -221,40 +340,35 @@ class SetRouter(command.Command): parser.add_argument( 'router', metavar="<router>", - help=("Router to modify (name or ID)") + help=_("Router to modify (name or ID)") ) parser.add_argument( '--name', metavar='<name>', - help='Set router name', + help=_("Set router name") ) admin_group = parser.add_mutually_exclusive_group() admin_group.add_argument( '--enable', - dest='admin_state_up', action='store_true', default=None, - help='Enable router', + help=_("Enable router") ) admin_group.add_argument( '--disable', - dest='admin_state_up', - action='store_false', - help='Disable router', + action='store_true', + help=_("Disable router") ) distribute_group = parser.add_mutually_exclusive_group() distribute_group.add_argument( '--distributed', - dest='distributed', action='store_true', - default=None, - help="Set router to distributed mode (disabled router only)", + help=_("Set router to distributed mode (disabled router only)") ) distribute_group.add_argument( '--centralized', - dest='distributed', - action='store_false', - help="Set router to centralized mode (disabled router only)", + action='store_true', + help=_("Set router to centralized mode (disabled router only)") ) routes_group = parser.add_mutually_exclusive_group() routes_group.add_argument( @@ -264,16 +378,20 @@ class SetRouter(command.Command): dest='routes', default=None, required_keys=['destination', 'gateway'], - help="Routes associated with the router. " - "Repeat this option to set multiple routes. " - "destination: destination subnet (in CIDR notation). " - "gateway: nexthop IP address.", + help=_("Routes associated with the router " + "destination: destination subnet (in CIDR notation) " + "gateway: nexthop IP address " + "(repeat option to set multiple routes)") + ) + routes_group.add_argument( + '--no-route', + action='store_true', + help=_("Clear routes associated with the router") ) routes_group.add_argument( '--clear-routes', - dest='clear_routes', action='store_true', - help="Clear routes associated with the router", + help=argparse.SUPPRESS, ) # TODO(tangchen): Support setting 'ha' property in 'router set' @@ -289,9 +407,27 @@ class SetRouter(command.Command): client = self.app.client_manager.network obj = client.find_router(parsed_args.router, ignore_missing=False) + # Get the common attributes. attrs = _get_attrs(self.app.client_manager, parsed_args) + + # Get the route attributes. + if parsed_args.no_route: + attrs['routes'] = [] + elif parsed_args.clear_routes: + attrs['routes'] = [] + LOG.warning(_( + 'The --clear-routes option is deprecated, ' + 'please use --no-route instead.' + )) + elif parsed_args.routes is not None: + # Map the route keys and append to the current routes. + # The REST API will handle route validation and duplicates. + for route in parsed_args.routes: + route['nexthop'] = route.pop('gateway') + attrs['routes'] = obj.routes + parsed_args.routes + if attrs == {}: - msg = "Nothing specified to be set" + msg = _("Nothing specified to be set") raise exceptions.CommandError(msg) client.update_router(obj, **attrs) @@ -305,13 +441,13 @@ class ShowRouter(command.ShowOne): parser.add_argument( 'router', metavar="<router>", - help="Router to display (name or ID)" + help=_("Router to display (name or ID)") ) return parser def take_action(self, parsed_args): client = self.app.client_manager.network obj = client.find_router(parsed_args.router, ignore_missing=False) - columns = sorted(obj.keys()) + columns = _get_columns(obj) data = utils.get_item_properties(obj, columns, formatters=_formatters) - return (tuple(columns), data) + return columns, data diff --git a/openstackclient/network/v2/security_group.py b/openstackclient/network/v2/security_group.py index 29d82b0..1ef2754 100644 --- a/openstackclient/network/v2/security_group.py +++ b/openstackclient/network/v2/security_group.py @@ -14,9 +14,153 @@ """Security Group action implementations""" import argparse +import six from openstackclient.common import utils +from openstackclient.i18n import _ +from openstackclient.identity import common as identity_common from openstackclient.network import common +from openstackclient.network import utils as network_utils + + +def _format_network_security_group_rules(sg_rules): + # For readability and to align with formatting compute security group + # rules, trim keys with caller known (e.g. security group and tenant ID) + # or empty values. + for sg_rule in sg_rules: + empty_keys = [k for k, v in six.iteritems(sg_rule) if not v] + for key in empty_keys: + sg_rule.pop(key) + sg_rule.pop('security_group_id', None) + sg_rule.pop('tenant_id', None) + return utils.format_list_of_dicts(sg_rules) + + +def _format_compute_security_group_rule(sg_rule): + info = network_utils.transform_compute_security_group_rule(sg_rule) + # Trim parent security group ID since caller has this information. + info.pop('parent_group_id', None) + # Trim keys with empty string values. + keys_to_trim = [ + 'ip_protocol', + 'ip_range', + 'port_range', + 'remote_security_group', + ] + for key in keys_to_trim: + if key in info and not info[key]: + info.pop(key) + return utils.format_dict(info) + + +def _format_compute_security_group_rules(sg_rules): + rules = [] + for sg_rule in sg_rules: + rules.append(_format_compute_security_group_rule(sg_rule)) + return utils.format_list(rules, separator='\n') + + +_formatters_network = { + 'security_group_rules': _format_network_security_group_rules, +} + + +_formatters_compute = { + 'rules': _format_compute_security_group_rules, +} + + +def _get_columns(item): + # Build the display columns and a list of the property columns + # that need to be mapped (display column name, property name). + columns = list(item.keys()) + property_column_mappings = [] + if 'security_group_rules' in columns: + columns.append('rules') + columns.remove('security_group_rules') + property_column_mappings.append(('rules', 'security_group_rules')) + if 'tenant_id' in columns: + columns.append('project_id') + columns.remove('tenant_id') + property_column_mappings.append(('project_id', 'tenant_id')) + display_columns = sorted(columns) + + # Build the property columns and apply any column mappings. + property_columns = sorted(columns) + for property_column_mapping in property_column_mappings: + property_index = property_columns.index(property_column_mapping[0]) + property_columns[property_index] = property_column_mapping[1] + return tuple(display_columns), property_columns + + +class CreateSecurityGroup(common.NetworkAndComputeShowOne): + """Create a new security group""" + + def update_parser_common(self, parser): + parser.add_argument( + "name", + metavar="<name>", + help=_("New security group name") + ) + parser.add_argument( + "--description", + metavar="<description>", + help=_("Security group description") + ) + return parser + + def update_parser_network(self, parser): + parser.add_argument( + '--project', + metavar='<project>', + help=_("Owner's project (name or ID)") + ) + identity_common.add_project_domain_option_to_parser(parser) + return parser + + def _get_description(self, parsed_args): + if parsed_args.description is not None: + return parsed_args.description + else: + return parsed_args.name + + def take_action_network(self, client, parsed_args): + # Build the create attributes. + attrs = {} + attrs['name'] = parsed_args.name + attrs['description'] = self._get_description(parsed_args) + if parsed_args.project is not None: + identity_client = self.app.client_manager.identity + project_id = identity_common.find_project( + identity_client, + parsed_args.project, + parsed_args.project_domain, + ).id + attrs['tenant_id'] = project_id + + # Create the security group and display the results. + obj = client.create_security_group(**attrs) + display_columns, property_columns = _get_columns(obj) + data = utils.get_item_properties( + obj, + property_columns, + formatters=_formatters_network + ) + return (display_columns, data) + + def take_action_compute(self, client, parsed_args): + description = self._get_description(parsed_args) + obj = client.security_groups.create( + parsed_args.name, + description, + ) + display_columns, property_columns = _get_columns(obj._info) + data = utils.get_dict_properties( + obj._info, + property_columns, + formatters=_formatters_compute + ) + return (display_columns, data) class DeleteSecurityGroup(common.NetworkAndComputeCommand): @@ -26,7 +170,7 @@ class DeleteSecurityGroup(common.NetworkAndComputeCommand): parser.add_argument( 'group', metavar='<group>', - help='Security group to delete (name or ID)', + help=_("Security group to delete (name or ID)") ) return parser @@ -61,7 +205,7 @@ class ListSecurityGroup(common.NetworkAndComputeLister): '--all-projects', action='store_true', default=False, - help='Display information from all projects (admin only)', + help=_("Display information from all projects (admin only)") ) return parser @@ -88,3 +232,94 @@ class ListSecurityGroup(common.NetworkAndComputeLister): data = client.security_groups.list(search_opts=search) return self._get_return_data(data, include_project=parsed_args.all_projects) + + +class SetSecurityGroup(common.NetworkAndComputeCommand): + """Set security group properties""" + + def update_parser_common(self, parser): + parser.add_argument( + 'group', + metavar='<group>', + help=_("Security group to modify (name or ID)") + ) + parser.add_argument( + '--name', + metavar='<new-name>', + help=_("New security group name") + ) + parser.add_argument( + "--description", + metavar="<description>", + help=_("New security group description") + ) + return parser + + def take_action_network(self, client, parsed_args): + obj = client.find_security_group(parsed_args.group, + ignore_missing=False) + attrs = {} + if parsed_args.name is not None: + attrs['name'] = parsed_args.name + if parsed_args.description is not None: + attrs['description'] = parsed_args.description + # NOTE(rtheis): Previous behavior did not raise a CommandError + # if there were no updates. Maintain this behavior and issue + # the update. + client.update_security_group(obj, **attrs) + + def take_action_compute(self, client, parsed_args): + data = utils.find_resource( + client.security_groups, + parsed_args.group, + ) + + if parsed_args.name is not None: + data.name = parsed_args.name + if parsed_args.description is not None: + data.description = parsed_args.description + + # NOTE(rtheis): Previous behavior did not raise a CommandError + # if there were no updates. Maintain this behavior and issue + # the update. + client.security_groups.update( + data, + data.name, + data.description, + ) + + +class ShowSecurityGroup(common.NetworkAndComputeShowOne): + """Display security group details""" + + def update_parser_common(self, parser): + parser.add_argument( + 'group', + metavar='<group>', + help=_("Security group to display (name or ID)") + ) + return parser + + def take_action_network(self, client, parsed_args): + obj = client.find_security_group(parsed_args.group, + ignore_missing=False) + display_columns, property_columns = _get_columns(obj) + data = utils.get_item_properties( + obj, + property_columns, + formatters=_formatters_network + ) + return (display_columns, data) + + def take_action_compute(self, client, parsed_args): + obj = utils.find_resource( + client.security_groups, + parsed_args.group, + ) + display_columns, property_columns = _get_columns(obj._info) + data = utils.get_dict_properties( + obj._info, + property_columns, + formatters=_formatters_compute + ) + return (display_columns, data) diff --git a/openstackclient/network/v2/security_group_rule.py b/openstackclient/network/v2/security_group_rule.py index a61e323..5abe9b9 100644 --- a/openstackclient/network/v2/security_group_rule.py +++ b/openstackclient/network/v2/security_group_rule.py @@ -13,54 +13,325 @@ """Security Group Rule action implementations""" +import argparse import six +try: + from novaclient.v2 import security_group_rules as compute_secgroup_rules +except ImportError: + from novaclient.v1_1 import security_group_rules as compute_secgroup_rules + from openstackclient.common import exceptions +from openstackclient.common import parseractions from openstackclient.common import utils +from openstackclient.i18n import _ +from openstackclient.identity import common as identity_common from openstackclient.network import common - - -def _xform_security_group_rule(sgroup): - info = {} - info.update(sgroup) - from_port = info.pop('from_port') - to_port = info.pop('to_port') - if isinstance(from_port, int) and isinstance(to_port, int): - port_range = {'port_range': "%u:%u" % (from_port, to_port)} - elif from_port is None and to_port is None: - port_range = {'port_range': ""} - else: - port_range = {'port_range': "%s:%s" % (from_port, to_port)} - info.update(port_range) - if 'cidr' in info['ip_range']: - info['ip_range'] = info['ip_range']['cidr'] - else: - info['ip_range'] = '' - if info['ip_protocol'] is None: - info['ip_protocol'] = '' - elif info['ip_protocol'].lower() == 'icmp': - info['port_range'] = '' - group = info.pop('group') - if 'name' in group: - info['remote_security_group'] = group['name'] - else: - info['remote_security_group'] = '' - return info +from openstackclient.network import utils as network_utils def _format_security_group_rule_show(obj): - data = _xform_security_group_rule(obj) + data = network_utils.transform_compute_security_group_rule(obj) return zip(*sorted(six.iteritems(data))) +def _format_network_port_range(rule): + # Display port range or ICMP type and code. For example: + # - ICMP type: 'type=3' + # - ICMP type and code: 'type=3:code=0' + # - ICMP code: Not supported + # - Matching port range: '443:443' + # - Different port range: '22:24' + # - 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: + port_range_min = port_range_max + if rule.port_range_max is None: + port_range_max = port_range_min + port_range = port_range_min + ':' + port_range_max + return port_range + + def _get_columns(item): - columns = item.keys() + columns = list(item.keys()) if 'tenant_id' in columns: columns.remove('tenant_id') columns.append('project_id') return tuple(sorted(columns)) +def _convert_to_lowercase(string): + return string.lower() + + +def _is_icmp_protocol(protocol): + # NOTE(rtheis): Neutron has deprecated protocol icmpv6. + # However, while the OSC CLI doesn't document the protocol, + # the code must still handle it. In addition, handle both + # protocol names and numbers. + if protocol in ['icmp', 'icmpv6', 'ipv6-icmp', '1', '58']: + return True + else: + return False + + +class CreateSecurityGroupRule(common.NetworkAndComputeShowOne): + """Create a new security group rule""" + + def update_parser_common(self, parser): + parser.add_argument( + 'group', + metavar='<group>', + help=_("Create rule in this security group (name or ID)") + ) + source_group = parser.add_mutually_exclusive_group() + source_group.add_argument( + "--src-ip", + metavar="<ip-address>", + help=_("Source IP address block (may use CIDR notation; " + "default for IPv4 rule: 0.0.0.0/0)") + ) + source_group.add_argument( + "--src-group", + metavar="<group>", + help=_("Source security group (name or ID)") + ) + return parser + + def update_parser_network(self, parser): + parser.add_argument( + '--dst-port', + metavar='<port-range>', + action=parseractions.RangeAction, + help=_("Destination port, may be a single port or a starting and " + "ending port range: 137:139. Required for IP protocols TCP " + "and UDP. Ignored for ICMP IP protocols.") + ) + parser.add_argument( + '--icmp-type', + metavar='<icmp-type>', + type=int, + help=_("ICMP type for ICMP IP protocols") + ) + parser.add_argument( + '--icmp-code', + metavar='<icmp-code>', + type=int, + help=_("ICMP code for ICMP IP protocols") + ) + # NOTE(rtheis): Support either protocol option name for now. + # However, consider deprecating and then removing --proto in + # a future release. + protocol_group = parser.add_mutually_exclusive_group() + protocol_group.add_argument( + '--protocol', + metavar='<protocol>', + type=_convert_to_lowercase, + help=_("IP protocol (ah, dccp, egp, esp, gre, icmp, igmp, " + "ipv6-encap, ipv6-frag, ipv6-icmp, ipv6-nonxt, " + "ipv6-opts, ipv6-route, ospf, pgm, rsvp, sctp, tcp, " + "udp, udplite, vrrp and integer representations [0-255]; " + "default: tcp)") + ) + protocol_group.add_argument( + '--proto', + metavar='<proto>', + type=_convert_to_lowercase, + help=argparse.SUPPRESS + ) + direction_group = parser.add_mutually_exclusive_group() + direction_group.add_argument( + '--ingress', + action='store_true', + help=_("Rule applies to incoming network traffic (default)") + ) + direction_group.add_argument( + '--egress', + action='store_true', + help=_("Rule applies to outgoing network traffic") + ) + parser.add_argument( + '--ethertype', + metavar='<ethertype>', + choices=['IPv4', 'IPv6'], + help=_("Ethertype of network traffic " + "(IPv4, IPv6; default: based on IP protocol)") + ) + parser.add_argument( + '--project', + metavar='<project>', + help=_("Owner's project (name or ID)") + ) + identity_common.add_project_domain_option_to_parser(parser) + return parser + + def update_parser_compute(self, parser): + parser.add_argument( + '--dst-port', + metavar='<port-range>', + default=(0, 0), + action=parseractions.RangeAction, + help=_("Destination port, may be a single port or a starting and " + "ending port range: 137:139. Required for IP protocols TCP " + "and UDP. Ignored for ICMP IP protocols.") + ) + # NOTE(rtheis): Support either protocol option name for now. + # However, consider deprecating and then removing --proto in + # a future release. + protocol_group = parser.add_mutually_exclusive_group() + protocol_group.add_argument( + '--protocol', + metavar='<protocol>', + choices=['icmp', 'tcp', 'udp'], + type=_convert_to_lowercase, + help=_("IP protocol (icmp, tcp, udp; default: tcp)") + ) + protocol_group.add_argument( + '--proto', + metavar='<proto>', + choices=['icmp', 'tcp', 'udp'], + type=_convert_to_lowercase, + help=argparse.SUPPRESS + ) + return parser + + def _get_protocol(self, parsed_args): + protocol = 'tcp' + if parsed_args.protocol is not None: + protocol = parsed_args.protocol + if parsed_args.proto is not None: + protocol = parsed_args.proto + return protocol + + def _is_ipv6_protocol(self, protocol): + # NOTE(rtheis): Neutron has deprecated protocol icmpv6. + # However, while the OSC CLI doesn't document the protocol, + # the code must still handle it. In addition, handle both + # protocol names and numbers. + if (protocol.startswith('ipv6-') or + protocol in ['icmpv6', '41', '43', '44', '58', '59', '60']): + return True + else: + return False + + def take_action_network(self, client, parsed_args): + # Get the security group ID to hold the rule. + security_group_id = client.find_security_group( + parsed_args.group, + ignore_missing=False + ).id + + # Build the create attributes. + attrs = {} + attrs['protocol'] = self._get_protocol(parsed_args) + + # NOTE(rtheis): A direction must be specified and ingress + # is the default. + if parsed_args.ingress or not parsed_args.egress: + attrs['direction'] = 'ingress' + if parsed_args.egress: + attrs['direction'] = 'egress' + + # NOTE(rtheis): Use ethertype specified else default based + # on IP protocol. + if parsed_args.ethertype: + attrs['ethertype'] = parsed_args.ethertype + elif self._is_ipv6_protocol(attrs['protocol']): + attrs['ethertype'] = 'IPv6' + else: + attrs['ethertype'] = 'IPv4' + + # NOTE(rtheis): Validate the port range and ICMP type and code. + # It would be ideal if argparse could do this. + if parsed_args.dst_port and (parsed_args.icmp_type or + parsed_args.icmp_code): + msg = _('Argument --dst-port not allowed with arguments ' + '--icmp-type and --icmp-code') + raise exceptions.CommandError(msg) + if parsed_args.icmp_type is None and parsed_args.icmp_code is not None: + msg = _('Argument --icmp-type required with argument --icmp-code') + raise exceptions.CommandError(msg) + is_icmp_protocol = _is_icmp_protocol(attrs['protocol']) + if not is_icmp_protocol and (parsed_args.icmp_type or + parsed_args.icmp_code): + msg = _('ICMP IP protocol required with arguments ' + '--icmp-type and --icmp-code') + raise exceptions.CommandError(msg) + # NOTE(rtheis): For backwards compatibility, continue ignoring + # the destination port range when an ICMP IP protocol is specified. + if parsed_args.dst_port and not is_icmp_protocol: + attrs['port_range_min'] = parsed_args.dst_port[0] + attrs['port_range_max'] = parsed_args.dst_port[1] + if parsed_args.icmp_type: + attrs['port_range_min'] = parsed_args.icmp_type + if parsed_args.icmp_code: + attrs['port_range_max'] = parsed_args.icmp_code + + if parsed_args.src_group is not None: + attrs['remote_group_id'] = client.find_security_group( + parsed_args.src_group, + ignore_missing=False + ).id + elif parsed_args.src_ip is not None: + attrs['remote_ip_prefix'] = parsed_args.src_ip + elif attrs['ethertype'] == 'IPv4': + attrs['remote_ip_prefix'] = '0.0.0.0/0' + attrs['security_group_id'] = security_group_id + if parsed_args.project is not None: + identity_client = self.app.client_manager.identity + project_id = identity_common.find_project( + identity_client, + parsed_args.project, + parsed_args.project_domain, + ).id + attrs['tenant_id'] = project_id + + # Create and show the security group rule. + obj = client.create_security_group_rule(**attrs) + columns = _get_columns(obj) + data = utils.get_item_properties(obj, columns) + return (columns, data) + + def take_action_compute(self, client, parsed_args): + group = utils.find_resource( + client.security_groups, + parsed_args.group, + ) + protocol = self._get_protocol(parsed_args) + if protocol == 'icmp': + from_port, to_port = -1, -1 + else: + from_port, to_port = parsed_args.dst_port + src_ip = None + if parsed_args.src_group is not None: + parsed_args.src_group = utils.find_resource( + client.security_groups, + parsed_args.src_group, + ).id + if parsed_args.src_ip is not None: + src_ip = parsed_args.src_ip + else: + src_ip = '0.0.0.0/0' + obj = client.security_group_rules.create( + group.id, + protocol, + from_port, + to_port, + src_ip, + parsed_args.src_group, + ) + return _format_security_group_rule_show(obj._info) + + class DeleteSecurityGroupRule(common.NetworkAndComputeCommand): """Delete a security group rule""" @@ -68,7 +339,7 @@ class DeleteSecurityGroupRule(common.NetworkAndComputeCommand): parser.add_argument( 'rule', metavar='<rule>', - help='Security group rule to delete (ID only)', + help=_("Security group rule to delete (ID only)") ) return parser @@ -80,6 +351,141 @@ class DeleteSecurityGroupRule(common.NetworkAndComputeCommand): client.security_group_rules.delete(parsed_args.rule) +class ListSecurityGroupRule(common.NetworkAndComputeLister): + """List security group rules""" + + def update_parser_common(self, parser): + parser.add_argument( + 'group', + metavar='<group>', + nargs='?', + help=_("List all rules in this security group (name or ID)") + ) + return parser + + def update_parser_network(self, parser): + # Accept but hide the argument for consistency with compute. + # Network will always return all projects for an admin. + parser.add_argument( + '--all-projects', + action='store_true', + default=False, + help=argparse.SUPPRESS + ) + parser.add_argument( + '--long', + action='store_true', + default=False, + help=_("List additional fields in output") + ) + return parser + + def update_parser_compute(self, parser): + parser.add_argument( + '--all-projects', + action='store_true', + default=False, + help=_("Display information from all projects (admin only)") + ) + # Accept but hide the argument for consistency with network. + # There are no additional fields to display at this time. + parser.add_argument( + '--long', + action='store_false', + default=False, + help=argparse.SUPPRESS + ) + return parser + + def _get_column_headers(self, parsed_args): + column_headers = ( + 'ID', + 'IP Protocol', + 'IP Range', + 'Port Range', + ) + if parsed_args.long: + column_headers = column_headers + ('Direction', 'Ethertype',) + column_headers = column_headers + ('Remote Security Group',) + if parsed_args.group is None: + column_headers = column_headers + ('Security Group',) + return column_headers + + def take_action_network(self, client, parsed_args): + column_headers = self._get_column_headers(parsed_args) + columns = ( + 'id', + 'protocol', + 'remote_ip_prefix', + 'port_range_min', + ) + if parsed_args.long: + columns = columns + ('direction', 'ethertype',) + columns = columns + ('remote_group_id',) + + # Get the security group rules using the requested query. + query = {} + if parsed_args.group is not None: + # NOTE(rtheis): Unfortunately, the security group resource + # does not contain security group rules resources. So use + # the security group ID in a query to get the resources. + security_group_id = client.find_security_group( + parsed_args.group, + ignore_missing=False + ).id + query = {'security_group_id': security_group_id} + else: + columns = columns + ('security_group_id',) + 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) + + return (column_headers, + (utils.get_item_properties( + s, columns, + ) for s in rules)) + + def take_action_compute(self, client, parsed_args): + column_headers = self._get_column_headers(parsed_args) + columns = ( + "ID", + "IP Protocol", + "IP Range", + "Port Range", + "Remote Security Group", + ) + + rules_to_list = [] + if parsed_args.group is not None: + group = utils.find_resource( + client.security_groups, + parsed_args.group, + ) + rules_to_list = group.rules + else: + columns = columns + ('parent_group_id',) + search = {'all_tenants': parsed_args.all_projects} + for group in client.security_groups.list(search_opts=search): + rules_to_list.extend(group.rules) + + # NOTE(rtheis): Turn the raw rules into resources. + rules = [] + for rule in rules_to_list: + rules.append(compute_secgroup_rules.SecurityGroupRule( + client.security_group_rules, + network_utils.transform_compute_security_group_rule(rule), + )) + + return (column_headers, + (utils.get_item_properties( + s, columns, + ) for s in rules)) + + class ShowSecurityGroupRule(common.NetworkAndComputeShowOne): """Display security group rule details""" @@ -87,7 +493,7 @@ class ShowSecurityGroupRule(common.NetworkAndComputeShowOne): parser.add_argument( 'rule', metavar="<rule>", - help="Security group rule to display (ID only)" + help=_("Security group rule to display (ID only)") ) return parser @@ -113,8 +519,8 @@ class ShowSecurityGroupRule(common.NetworkAndComputeShowOne): break if obj is None: - msg = "Could not find security group rule " \ - "with ID %s" % parsed_args.rule + msg = _("Could not find security group rule with ID ") + \ + parsed_args.rule raise exceptions.CommandError(msg) # NOTE(rtheis): Format security group rule diff --git a/openstackclient/network/v2/subnet.py b/openstackclient/network/v2/subnet.py index b514a88..e7e1be9 100644 --- a/openstackclient/network/v2/subnet.py +++ b/openstackclient/network/v2/subnet.py @@ -12,9 +12,14 @@ # """Subnet action implementations""" +import copy from openstackclient.common import command +from openstackclient.common import exceptions +from openstackclient.common import parseractions from openstackclient.common import utils +from openstackclient.i18n import _ +from openstackclient.identity import common as identity_common def _format_allocation_pools(data): @@ -23,21 +28,245 @@ def _format_allocation_pools(data): return ','.join(pool_formatted) +def _format_host_routes(data): + # Map the host route keys to match --host-route option. + return utils.format_list_of_dicts(convert_entries_to_gateway(data)) + + _formatters = { 'allocation_pools': _format_allocation_pools, 'dns_nameservers': utils.format_list, - 'host_routes': utils.format_list, + 'host_routes': _format_host_routes, } +def _get_common_parse_arguments(parser): + parser.add_argument( + '--allocation-pool', + metavar='start=<ip-address>,end=<ip-address>', + dest='allocation_pools', + action=parseractions.MultiKeyValueAction, + required_keys=['start', 'end'], + help=_("Allocation pool IP addresses for this subnet " + "e.g.: start=192.168.199.2,end=192.168.199.254 " + "(repeat option to add multiple IP addresses)") + ) + parser.add_argument( + '--dns-nameserver', + metavar='<dns-nameserver>', + action='append', + dest='dns_nameservers', + help=_("DNS server for this subnet " + "(repeat option to set multiple DNS servers)") + ) + parser.add_argument( + '--host-route', + metavar='destination=<subnet>,gateway=<ip-address>', + dest='host_routes', + action=parseractions.MultiKeyValueAction, + required_keys=['destination', 'gateway'], + help=_("Additional route for this subnet " + "e.g.: destination=10.10.0.0/16,gateway=192.168.71.254 " + "destination: destination subnet (in CIDR notation) " + "gateway: nexthop IP address " + "(repeat option to add multiple routes)") + ) + + def _get_columns(item): - columns = item.keys() + columns = list(item.keys()) if 'tenant_id' in columns: columns.remove('tenant_id') columns.append('project_id') return tuple(sorted(columns)) +def convert_entries_to_nexthop(entries): + # Change 'gateway' entry to 'nexthop' + changed_entries = copy.deepcopy(entries) + for entry in changed_entries: + if 'gateway' in entry: + entry['nexthop'] = entry['gateway'] + del entry['gateway'] + + return changed_entries + + +def convert_entries_to_gateway(entries): + # Change 'nexthop' entry to 'gateway' + changed_entries = copy.deepcopy(entries) + for entry in changed_entries: + if 'nexthop' in entry: + entry['gateway'] = entry['nexthop'] + del entry['nexthop'] + + return changed_entries + + +def _get_attrs(client_manager, parsed_args, is_create=True): + attrs = {} + if 'name' in parsed_args and parsed_args.name is not None: + attrs['name'] = str(parsed_args.name) + + if is_create: + if 'project' in parsed_args and parsed_args.project is not None: + identity_client = client_manager.identity + project_id = identity_common.find_project( + identity_client, + parsed_args.project, + parsed_args.project_domain, + ).id + attrs['tenant_id'] = project_id + client = client_manager.network + attrs['network_id'] = client.find_network(parsed_args.network, + ignore_missing=False).id + if parsed_args.subnet_pool is not None: + subnet_pool = client.find_subnet_pool(parsed_args.subnet_pool, + ignore_missing=False) + attrs['subnetpool_id'] = subnet_pool.id + if parsed_args.use_default_subnet_pool: + attrs['use_default_subnetpool'] = True + if parsed_args.prefix_length is not None: + attrs['prefixlen'] = parsed_args.prefix_length + if parsed_args.subnet_range is not None: + attrs['cidr'] = parsed_args.subnet_range + if parsed_args.ip_version is not None: + attrs['ip_version'] = parsed_args.ip_version + if parsed_args.ipv6_ra_mode is not None: + attrs['ipv6_ra_mode'] = parsed_args.ipv6_ra_mode + if parsed_args.ipv6_address_mode is not None: + attrs['ipv6_address_mode'] = parsed_args.ipv6_address_mode + + if 'gateway' in parsed_args and parsed_args.gateway is not None: + gateway = parsed_args.gateway.lower() + + if not is_create and gateway == 'auto': + msg = _("Auto option is not available for Subnet Set. " + "Valid options are <ip-address> or none") + raise exceptions.CommandError(msg) + elif gateway != 'auto': + if gateway == 'none': + attrs['gateway_ip'] = None + else: + attrs['gateway_ip'] = gateway + if ('allocation_pools' in parsed_args and + parsed_args.allocation_pools is not None): + attrs['allocation_pools'] = parsed_args.allocation_pools + if parsed_args.dhcp: + attrs['enable_dhcp'] = True + elif parsed_args.no_dhcp: + attrs['enable_dhcp'] = False + if ('dns_nameservers' in parsed_args and + parsed_args.dns_nameservers is not None): + attrs['dns_nameservers'] = parsed_args.dns_nameservers + if 'host_routes' in parsed_args and parsed_args.host_routes is not None: + # Change 'gateway' entry to 'nexthop' to match the API + attrs['host_routes'] = convert_entries_to_nexthop( + parsed_args.host_routes) + return attrs + + +class CreateSubnet(command.ShowOne): + """Create a subnet""" + + def get_parser(self, prog_name): + parser = super(CreateSubnet, self).get_parser(prog_name) + parser.add_argument( + 'name', + help=_("New subnet name") + ) + parser.add_argument( + '--project', + metavar='<project>', + help=_("Owner's project (name or ID)") + ) + identity_common.add_project_domain_option_to_parser(parser) + subnet_pool_group = parser.add_mutually_exclusive_group() + subnet_pool_group.add_argument( + '--subnet-pool', + metavar='<subnet-pool>', + help=_("Subnet pool from which this subnet will obtain a CIDR " + "(Name or ID)") + ) + subnet_pool_group.add_argument( + '--use-default-subnet-pool', + action='store_true', + help=_("Use default subnet pool for --ip-version") + ) + parser.add_argument( + '--prefix-length', + metavar='<prefix-length>', + help=_("Prefix length for subnet allocation from subnet pool") + ) + parser.add_argument( + '--subnet-range', + metavar='<subnet-range>', + help=_("Subnet range in CIDR notation " + "(required if --subnet-pool is not specified, " + "optional otherwise)") + ) + dhcp_enable_group = parser.add_mutually_exclusive_group() + dhcp_enable_group.add_argument( + '--dhcp', + action='store_true', + default=True, + help=_("Enable DHCP (default)") + ) + dhcp_enable_group.add_argument( + '--no-dhcp', + action='store_true', + help=_("Disable DHCP") + ) + parser.add_argument( + '--gateway', + metavar='<gateway>', + default='auto', + help=_("Specify a gateway for the subnet. The three options are: " + "<ip-address>: Specific IP address to use as the gateway, " + "'auto': Gateway address should automatically be chosen " + "from within the subnet itself, 'none': This subnet will " + "not use a gateway, e.g.: --gateway 192.168.9.1, " + "--gateway auto, --gateway none (default is 'auto')") + ) + parser.add_argument( + '--ip-version', + type=int, + default=4, + choices=[4, 6], + help=_("IP version (default is 4). Note that when subnet pool is " + "specified, IP version is determined from the subnet pool " + "and this option is ignored") + ) + parser.add_argument( + '--ipv6-ra-mode', + choices=['dhcpv6-stateful', 'dhcpv6-stateless', 'slaac'], + help=_("IPv6 RA (Router Advertisement) mode, " + "valid modes: [dhcpv6-stateful, dhcpv6-stateless, slaac]") + ) + parser.add_argument( + '--ipv6-address-mode', + choices=['dhcpv6-stateful', 'dhcpv6-stateless', 'slaac'], + help=_("IPv6 address mode, " + "valid modes: [dhcpv6-stateful, dhcpv6-stateless, slaac]") + ) + parser.add_argument( + '--network', + required=True, + metavar='<network>', + help=_("Network this subnet belongs to (name or ID)") + ) + _get_common_parse_arguments(parser) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + attrs = _get_attrs(self.app.client_manager, parsed_args) + obj = client.create_subnet(**attrs) + columns = _get_columns(obj) + data = utils.get_item_properties(obj, columns, formatters=_formatters) + return (columns, data) + + class DeleteSubnet(command.Command): """Delete subnet""" @@ -46,7 +275,7 @@ class DeleteSubnet(command.Command): parser.add_argument( 'subnet', metavar="<subnet>", - help="Subnet to delete (name or ID)" + help=_("Subnet to delete (name or ID)") ) return parser @@ -65,12 +294,24 @@ class ListSubnet(command.Lister): '--long', action='store_true', default=False, - help='List additional fields in output', + help=_("List additional fields in output") + ) + parser.add_argument( + '--ip-version', + type=int, + choices=[4, 6], + metavar='<ip-version>', + dest='ip_version', + help=_("List only subnets of given IP version in output" + "Allowed values for IP version are 4 and 6."), ) return parser def take_action(self, parsed_args): - data = self.app.client_manager.network.subnets() + filters = {} + if parsed_args.ip_version: + filters['ip_version'] = parsed_args.ip_version + data = self.app.client_manager.network.subnets(**filters) headers = ('ID', 'Name', 'Network', 'Subnet') columns = ('id', 'name', 'network_id', 'cidr') @@ -89,15 +330,71 @@ class ListSubnet(command.Lister): ) for s in data)) +class SetSubnet(command.Command): + """Set subnet properties""" + + def get_parser(self, prog_name): + parser = super(SetSubnet, self).get_parser(prog_name) + parser.add_argument( + 'subnet', + metavar="<subnet>", + help=_("Subnet to modify (name or ID)") + ) + parser.add_argument( + '--name', + metavar='<name>', + help=_("Updated name of the subnet") + ) + dhcp_enable_group = parser.add_mutually_exclusive_group() + dhcp_enable_group.add_argument( + '--dhcp', + action='store_true', + default=None, + help=_("Enable DHCP") + ) + dhcp_enable_group.add_argument( + '--no-dhcp', + action='store_true', + help=_("Disable DHCP") + ) + parser.add_argument( + '--gateway', + metavar='<gateway>', + help=_("Specify a gateway for the subnet. The options are: " + "<ip-address>: Specific IP address to use as the gateway, " + "'none': This subnet will not use a gateway, " + "e.g.: --gateway 192.168.9.1, --gateway none") + ) + _get_common_parse_arguments(parser) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + obj = client.find_subnet(parsed_args.subnet, ignore_missing=False) + attrs = _get_attrs(self.app.client_manager, parsed_args, + is_create=False) + if not attrs: + msg = "Nothing specified to be set" + raise exceptions.CommandError(msg) + if 'dns_nameservers' in attrs: + attrs['dns_nameservers'] += obj.dns_nameservers + if 'host_routes' in attrs: + attrs['host_routes'] += obj.host_routes + if 'allocation_pools' in attrs: + attrs['allocation_pools'] += obj.allocation_pools + client.update_subnet(obj, **attrs) + return + + class ShowSubnet(command.ShowOne): - """Show subnet details""" + """Display subnet details""" def get_parser(self, prog_name): parser = super(ShowSubnet, self).get_parser(prog_name) parser.add_argument( 'subnet', metavar="<subnet>", - help="Subnet to show (name or ID)" + help=_("Subnet to display (name or ID)") ) return parser diff --git a/openstackclient/network/v2/subnet_pool.py b/openstackclient/network/v2/subnet_pool.py index 5bb45c1..a1a9442 100644 --- a/openstackclient/network/v2/subnet_pool.py +++ b/openstackclient/network/v2/subnet_pool.py @@ -14,11 +14,15 @@ """Subnet pool action implementations""" from openstackclient.common import command +from openstackclient.common import exceptions +from openstackclient.common import parseractions from openstackclient.common import utils +from openstackclient.i18n import _ +from openstackclient.identity import common as identity_common def _get_columns(item): - columns = item.keys() + columns = list(item.keys()) if 'tenant_id' in columns: columns.remove('tenant_id') columns.append('project_id') @@ -30,6 +34,147 @@ _formatters = { } +def _get_attrs(client_manager, parsed_args): + attrs = {} + network_client = client_manager.network + + if parsed_args.name is not None: + attrs['name'] = str(parsed_args.name) + if parsed_args.prefixes is not None: + attrs['prefixes'] = parsed_args.prefixes + if parsed_args.default_prefix_length is not None: + attrs['default_prefixlen'] = parsed_args.default_prefix_length + if parsed_args.min_prefix_length is not None: + attrs['min_prefixlen'] = parsed_args.min_prefix_length + if parsed_args.max_prefix_length is not None: + attrs['max_prefixlen'] = parsed_args.max_prefix_length + + if parsed_args.address_scope is not None: + attrs['address_scope_id'] = network_client.find_address_scope( + parsed_args.address_scope, ignore_missing=False).id + if 'no_address_scope' in parsed_args and parsed_args.no_address_scope: + attrs['address_scope_id'] = None + + if parsed_args.default: + attrs['is_default'] = True + if parsed_args.no_default: + attrs['is_default'] = False + + if 'share' in parsed_args and parsed_args.share: + attrs['shared'] = True + if 'no_share' in parsed_args and parsed_args.no_share: + attrs['shared'] = False + + # "subnet pool set" command doesn't support setting project. + if 'project' in parsed_args and parsed_args.project is not None: + identity_client = client_manager.identity + project_id = identity_common.find_project( + identity_client, + parsed_args.project, + parsed_args.project_domain, + ).id + attrs['tenant_id'] = project_id + + return attrs + + +def _add_prefix_options(parser, for_create=False): + parser.add_argument( + '--pool-prefix', + metavar='<pool-prefix>', + dest='prefixes', + action='append', + required=for_create, + help=_("Set subnet pool prefixes (in CIDR notation) " + "(repeat option to set multiple prefixes)") + ) + parser.add_argument( + '--default-prefix-length', + metavar='<default-prefix-length>', + type=int, + action=parseractions.NonNegativeAction, + help=_("Set subnet pool default prefix length") + ) + parser.add_argument( + '--min-prefix-length', + metavar='<min-prefix-length>', + action=parseractions.NonNegativeAction, + type=int, + help=_("Set subnet pool minimum prefix length") + ) + parser.add_argument( + '--max-prefix-length', + metavar='<max-prefix-length>', + type=int, + action=parseractions.NonNegativeAction, + help=_("Set subnet pool maximum prefix length") + ) + + +def _add_default_options(parser): + default_group = parser.add_mutually_exclusive_group() + default_group.add_argument( + '--default', + action='store_true', + help=_("Set this as a default subnet pool"), + ) + default_group.add_argument( + '--no-default', + action='store_true', + help=_("Set this as a non-default subnet pool"), + ) + + +class CreateSubnetPool(command.ShowOne): + """Create subnet pool""" + + def get_parser(self, prog_name): + parser = super(CreateSubnetPool, self).get_parser(prog_name) + parser.add_argument( + 'name', + metavar='<name>', + help=_("Name of the new subnet pool") + ) + _add_prefix_options(parser, for_create=True) + parser.add_argument( + '--project', + metavar='<project>', + help=_("Owner's project (name or ID)") + ) + identity_common.add_project_domain_option_to_parser(parser) + parser.add_argument( + '--address-scope', + metavar='<address-scope>', + help=_("Set address scope associated with the subnet pool " + "(name or ID), prefixes must be unique across address " + "scopes") + ) + _add_default_options(parser) + shared_group = parser.add_mutually_exclusive_group() + shared_group.add_argument( + '--share', + action='store_true', + help=_("Set this subnet pool as shared"), + ) + shared_group.add_argument( + '--no-share', + action='store_true', + help=_("Set this subnet pool as not shared"), + ) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + attrs = _get_attrs(self.app.client_manager, parsed_args) + # NeutronServer expects prefixes to be a List + if "prefixes" not in attrs: + attrs['prefixes'] = [] + obj = client.create_subnet_pool(**attrs) + columns = _get_columns(obj) + data = utils.get_item_properties(obj, columns, formatters=_formatters) + return (columns, data) + + class DeleteSubnetPool(command.Command): """Delete subnet pool""" @@ -37,8 +182,8 @@ class DeleteSubnetPool(command.Command): parser = super(DeleteSubnetPool, self).get_parser(prog_name) parser.add_argument( 'subnet_pool', - metavar="<subnet-pool>", - help=("Subnet pool to delete (name or ID)") + metavar='<subnet-pool>', + help=_("Subnet pool to delete (name or ID)") ) return parser @@ -57,7 +202,7 @@ class ListSubnetPool(command.Lister): '--long', action='store_true', default=False, - help='List additional fields in output', + help=_("List additional fields in output") ) return parser @@ -71,6 +216,8 @@ class ListSubnetPool(command.Lister): 'Prefixes', 'Default Prefix Length', 'Address Scope', + 'Default Subnet Pool', + 'Shared', ) columns = ( 'id', @@ -78,6 +225,8 @@ class ListSubnetPool(command.Lister): 'prefixes', 'default_prefixlen', 'address_scope_id', + 'is_default', + 'shared', ) else: headers = ( @@ -94,10 +243,60 @@ class ListSubnetPool(command.Lister): return (headers, (utils.get_item_properties( s, columns, - formatters={}, + formatters=_formatters, ) for s in data)) +class SetSubnetPool(command.Command): + """Set subnet pool properties""" + + def get_parser(self, prog_name): + parser = super(SetSubnetPool, self).get_parser(prog_name) + parser.add_argument( + 'subnet_pool', + metavar='<subnet-pool>', + help=_("Subnet pool to modify (name or ID)") + ) + parser.add_argument( + '--name', + metavar='<name>', + help=_("Set subnet pool name") + ) + _add_prefix_options(parser) + address_scope_group = parser.add_mutually_exclusive_group() + address_scope_group.add_argument( + '--address-scope', + metavar='<address-scope>', + help=_("Set address scope associated with the subnet pool " + "(name or ID), prefixes must be unique across address " + "scopes") + ) + address_scope_group.add_argument( + '--no-address-scope', + action='store_true', + help=_("Remove address scope associated with the subnet pool") + ) + _add_default_options(parser) + + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + obj = client.find_subnet_pool(parsed_args.subnet_pool, + ignore_missing=False) + + attrs = _get_attrs(self.app.client_manager, parsed_args) + if attrs == {}: + msg = _("Nothing specified to be set") + raise exceptions.CommandError(msg) + + # Existing prefixes must be a subset of the new prefixes. + if 'prefixes' in attrs: + attrs['prefixes'].extend(obj.prefixes) + + client.update_subnet_pool(obj, **attrs) + + class ShowSubnetPool(command.ShowOne): """Display subnet pool details""" @@ -105,8 +304,8 @@ class ShowSubnetPool(command.ShowOne): parser = super(ShowSubnetPool, self).get_parser(prog_name) parser.add_argument( 'subnet_pool', - metavar="<subnet-pool>", - help=("Subnet pool to display (name or ID)") + metavar='<subnet-pool>', + help=_("Subnet pool to display (name or ID)") ) return parser diff --git a/openstackclient/shell.py b/openstackclient/shell.py index 53e9be0..9968d73 100644 --- a/openstackclient/shell.py +++ b/openstackclient/shell.py @@ -16,6 +16,7 @@ """Command-line interface to the OpenStack APIs""" +import argparse import getpass import logging import sys @@ -131,6 +132,16 @@ class OpenStackShell(app.App): self.log.info("END return value: %s", ret_val) def init_profile(self): + # NOTE(dtroyer): Remove this 'if' block when the --profile global + # option is removed + if osprofiler_profiler and self.options.old_profile: + self.log.warning( + 'The --profile option is deprecated, ' + 'please use --os-profile instead' + ) + if not self.options.profile: + self.options.profile = self.options.old_profile + self.do_profile = osprofiler_profiler and self.options.profile if self.do_profile: osprofiler_profiler.init(self.options.profile) @@ -144,7 +155,7 @@ class OpenStackShell(app.App): # bigger than most big default one (CRITICAL) or something like # that (PROFILE = 60 for instance), but not sure we need it here. self.log.warning("Trace ID: %s" % trace_id) - self.log.warning("To display trace use next command:\n" + self.log.warning("Display trace with command:\n" "osprofiler trace show --html %s " % trace_id) def run_subcommand(self, argv): @@ -189,6 +200,18 @@ class OpenStackShell(app.App): dest='cacert', default=utils.env('OS_CACERT'), help='CA certificate bundle file (Env: OS_CACERT)') + parser.add_argument( + '--os-cert', + metavar='<certificate-file>', + dest='cert', + default=utils.env('OS_CERT'), + help='Client certificate bundle file (Env: OS_CERT)') + parser.add_argument( + '--os-key', + metavar='<key-file>', + dest='key', + default=utils.env('OS_KEY'), + help='Client certificate key file (Env: OS_KEY)') verify_group = parser.add_mutually_exclusive_group() verify_group.add_argument( '--verify', @@ -227,19 +250,30 @@ class OpenStackShell(app.App): action='store_true', help="Print API call timing info", ) + parser.add_argument( + '--enable-beta-commands', + action='store_true', + help="Enable beta commands which are subject to change", + ) # osprofiler HMAC key argument if osprofiler_profiler: - parser.add_argument('--profile', - metavar='hmac-key', - help='HMAC key to use for encrypting context ' - 'data for performance profiling of operation. ' - 'This key should be the value of one of the ' - 'HMAC keys configured in osprofiler ' - 'middleware in the projects user would like ' - 'to profile. It needs to be specified in ' - 'configuration files of the required ' - 'projects.') + parser.add_argument( + '--os-profile', + metavar='hmac-key', + dest='profile', + help='HMAC key for encrypting profiling context data', + ) + # NOTE(dtroyer): This global option should have been named + # --os-profile as --profile interferes with at + # least one existing command option. Deprecate + # --profile and remove after Apr 2017. + parser.add_argument( + '--profile', + metavar='hmac-key', + dest='old_profile', + help=argparse.SUPPRESS, + ) return clientmanager.build_plugin_option_parser(parser) @@ -355,7 +389,7 @@ class OpenStackShell(app.App): self.log.warning( "%s version %s is not in supported versions %s" % (api, version_opt, - ', '.join(mod.API_VERSIONS.keys()))) + ', '.join(list(mod.API_VERSIONS.keys())))) # Command groups deal only with major versions version = '.v' + version_opt.replace('.', '_').split('_')[0] diff --git a/openstackclient/tests/common/test_clientmanager.py b/openstackclient/tests/common/test_clientmanager.py index 2bd9e78..fa6c3fc 100644 --- a/openstackclient/tests/common/test_clientmanager.py +++ b/openstackclient/tests/common/test_clientmanager.py @@ -41,6 +41,7 @@ auth.get_options_list() class Container(object): attr = clientmanager.ClientCache(lambda x: object()) + buggy_attr = clientmanager.ClientCache(lambda x: x.foo) def __init__(self): pass @@ -58,6 +59,8 @@ class FakeOptions(object): self.interface = None self.url = None self.auth = {} + self.cert = None + self.key = None self.default_domain = 'default' self.__dict__.update(kwargs) @@ -70,6 +73,13 @@ class TestClientCache(utils.TestCase): c = Container() self.assertEqual(c.attr, c.attr) + def test_attribute_error_propagates(self): + c = Container() + err = self.assertRaises(exc.PluginAttributeError, + getattr, c, 'buggy_attr') + self.assertNotIsInstance(err, AttributeError) + self.assertEqual("'Container' object has no attribute 'foo'", str(err)) + class TestClientManager(utils.TestCase): @@ -268,6 +278,21 @@ class TestClientManager(utils.TestCase): self.assertEqual('cafile', client_manager._cacert) self.assertTrue(client_manager.is_network_endpoint_enabled()) + def test_client_manager_password_no_cert(self): + client_manager = clientmanager.ClientManager( + cli_options=FakeOptions()) + self.assertIsNone(client_manager._cert) + + def test_client_manager_password_client_cert(self): + client_manager = clientmanager.ClientManager( + cli_options=FakeOptions(cert='cert')) + self.assertEqual('cert', client_manager._cert) + + def test_client_manager_password_client_cert_and_key(self): + client_manager = clientmanager.ClientManager( + cli_options=FakeOptions(cert='cert', key='key')) + self.assertEqual(('cert', 'key'), client_manager._cert) + def _select_auth_plugin(self, auth_params, api_version, auth_plugin_name): auth_params['auth_type'] = auth_plugin_name auth_params['identity_api_version'] = api_version diff --git a/openstackclient/tests/common/test_extension.py b/openstackclient/tests/common/test_extension.py index 6653282..0736a3e 100644 --- a/openstackclient/tests/common/test_extension.py +++ b/openstackclient/tests/common/test_extension.py @@ -12,13 +12,16 @@ # import copy +import mock from openstackclient.common import extension from openstackclient.tests import fakes from openstackclient.tests import utils +from openstackclient.tests.compute.v2 import fakes as compute_fakes from openstackclient.tests.identity.v2_0 import fakes as identity_fakes from openstackclient.tests.network.v2 import fakes as network_fakes +from openstackclient.tests.volume.v2 import fakes as volume_fakes class TestExtension(utils.TestCommand): @@ -34,6 +37,16 @@ class TestExtension(utils.TestCommand): self.app.client_manager.identity.extensions) self.identity_extensions_mock.reset_mock() + self.app.client_manager.compute = compute_fakes.FakeComputev2Client( + endpoint=fakes.AUTH_URL, + token=fakes.AUTH_TOKEN, + ) + + self.app.client_manager.volume = volume_fakes.FakeVolumeClient( + endpoint=fakes.AUTH_URL, + token=fakes.AUTH_TOKEN, + ) + network_client = network_fakes.FakeNetworkV2Client() self.app.client_manager.network = network_client self.network_extensions_mock = network_client.extensions @@ -43,6 +56,8 @@ class TestExtension(utils.TestCommand): class TestExtensionList(TestExtension): columns = ('Name', 'Alias', 'Description') + long_columns = ('Name', 'Namespace', 'Description', 'Alias', 'Updated', + 'Links') def setUp(self): super(TestExtensionList, self).setUp() @@ -55,12 +70,33 @@ class TestExtensionList(TestExtension): ), ] + self.app.client_manager.compute.list_extensions = mock.Mock() + self.compute_extensions_mock = ( + self.app.client_manager.compute.list_extensions) + self.compute_extensions_mock.show_all.return_value = [ + fakes.FakeResource( + None, + copy.deepcopy(compute_fakes.EXTENSION), + loaded=True, + ), + ] + + self.app.client_manager.volume.list_extensions = mock.Mock() + self.volume_extensions_mock = ( + self.app.client_manager.volume.list_extensions) + self.volume_extensions_mock.show_all.return_value = [ + fakes.FakeResource( + None, + copy.deepcopy(volume_fakes.EXTENSION), + loaded=True, + ), + ] + # Get the command object to test self.cmd = extension.ListExtension(self.app, None) - def test_extension_list_no_options(self): - arglist = [] - verifylist = [] + def _test_extension_list_helper(self, arglist, verifylist, + expected_data, long=False): parsed_args = self.check_parser(self.cmd, arglist, verifylist) # In base command class Lister in cliff, abstract method take_action() @@ -68,10 +104,15 @@ class TestExtensionList(TestExtension): # containing the data to be listed. columns, data = self.cmd.take_action(parsed_args) - # no args should output from all services - self.identity_extensions_mock.list.assert_called_with() + if long: + self.assertEqual(self.long_columns, columns) + else: + self.assertEqual(self.columns, columns) + self.assertEqual(expected_data, tuple(data)) - self.assertEqual(self.columns, columns) + def test_extension_list_no_options(self): + arglist = [] + verifylist = [] datalist = ( ( identity_fakes.extension_name, @@ -79,12 +120,26 @@ class TestExtensionList(TestExtension): identity_fakes.extension_description, ), ( + compute_fakes.extension_name, + compute_fakes.extension_alias, + compute_fakes.extension_description, + ), + ( + volume_fakes.extension_name, + volume_fakes.extension_alias, + volume_fakes.extension_description, + ), + ( network_fakes.extension_name, network_fakes.extension_alias, network_fakes.extension_description, ), ) - self.assertEqual(datalist, tuple(data)) + self._test_extension_list_helper(arglist, verifylist, datalist) + self.identity_extensions_mock.list.assert_called_with() + self.compute_extensions_mock.show_all.assert_called_with() + self.volume_extensions_mock.show_all.assert_called_with() + self.network_extensions_mock.assert_called_with() def test_extension_list_long(self): arglist = [ @@ -93,19 +148,6 @@ class TestExtensionList(TestExtension): verifylist = [ ('long', True), ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - # In base command class Lister in cliff, abstract method take_action() - # returns a tuple containing the column names and an iterable - # containing the data to be listed. - columns, data = self.cmd.take_action(parsed_args) - - # no args should output from all services - self.identity_extensions_mock.list.assert_called_with() - - collist = ('Name', 'Namespace', 'Description', 'Alias', 'Updated', - 'Links') - self.assertEqual(collist, columns) datalist = ( ( identity_fakes.extension_name, @@ -116,6 +158,22 @@ class TestExtensionList(TestExtension): identity_fakes.extension_links, ), ( + compute_fakes.extension_name, + compute_fakes.extension_namespace, + compute_fakes.extension_description, + compute_fakes.extension_alias, + compute_fakes.extension_updated, + compute_fakes.extension_links, + ), + ( + volume_fakes.extension_name, + volume_fakes.extension_namespace, + volume_fakes.extension_description, + volume_fakes.extension_alias, + volume_fakes.extension_updated, + volume_fakes.extension_links, + ), + ( network_fakes.extension_name, network_fakes.extension_namespace, network_fakes.extension_description, @@ -124,7 +182,11 @@ class TestExtensionList(TestExtension): network_fakes.extension_links, ), ) - self.assertEqual(datalist, tuple(data)) + self._test_extension_list_helper(arglist, verifylist, datalist, True) + self.identity_extensions_mock.list.assert_called_with() + self.compute_extensions_mock.show_all.assert_called_with() + self.volume_extensions_mock.show_all.assert_called_with() + self.network_extensions_mock.assert_called_with() def test_extension_list_identity(self): arglist = [ @@ -133,22 +195,13 @@ class TestExtensionList(TestExtension): verifylist = [ ('identity', True), ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - # In base command class Lister in cliff, abstract method take_action() - # returns a tuple containing the column names and an iterable - # containing the data to be listed. - columns, data = self.cmd.take_action(parsed_args) - - self.identity_extensions_mock.list.assert_called_with() - - self.assertEqual(self.columns, columns) datalist = (( identity_fakes.extension_name, identity_fakes.extension_alias, identity_fakes.extension_description, ), ) - self.assertEqual(datalist, tuple(data)) + self._test_extension_list_helper(arglist, verifylist, datalist) + self.identity_extensions_mock.list.assert_called_with() def test_extension_list_network(self): arglist = [ @@ -157,13 +210,6 @@ class TestExtensionList(TestExtension): verifylist = [ ('network', True), ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - columns, data = self.cmd.take_action(parsed_args) - - self.network_extensions_mock.assert_called_with() - - self.assertEqual(self.columns, columns) datalist = ( ( network_fakes.extension_name, @@ -171,4 +217,35 @@ class TestExtensionList(TestExtension): network_fakes.extension_description, ), ) - self.assertEqual(datalist, tuple(data)) + self._test_extension_list_helper(arglist, verifylist, datalist) + self.network_extensions_mock.assert_called_with() + + def test_extension_list_compute(self): + arglist = [ + '--compute', + ] + verifylist = [ + ('compute', True), + ] + datalist = (( + compute_fakes.extension_name, + compute_fakes.extension_alias, + compute_fakes.extension_description, + ), ) + self._test_extension_list_helper(arglist, verifylist, datalist) + self.compute_extensions_mock.show_all.assert_called_with() + + def test_extension_list_volume(self): + arglist = [ + '--volume', + ] + verifylist = [ + ('volume', True), + ] + datalist = (( + volume_fakes.extension_name, + volume_fakes.extension_alias, + volume_fakes.extension_description, + ), ) + self._test_extension_list_helper(arglist, verifylist, datalist) + self.volume_extensions_mock.show_all.assert_called_with() diff --git a/openstackclient/tests/common/test_module.py b/openstackclient/tests/common/test_module.py index 2821da9..7d08dae 100644 --- a/openstackclient/tests/common/test_module.py +++ b/openstackclient/tests/common/test_module.py @@ -48,10 +48,11 @@ class TestCommandList(utils.TestCommand): super(TestCommandList, self).setUp() self.app.command_manager = mock.Mock() - self.app.command_manager.get_command_groups.return_value = ['test'] + self.app.command_manager.get_command_groups.return_value = [ + 'openstack.common' + ] self.app.command_manager.get_command_names.return_value = [ - 'one', - 'cmd two', + 'limits show\nextension list' ] # Get the command object to test @@ -67,12 +68,15 @@ class TestCommandList(utils.TestCommand): # containing the data to be listed. columns, data = self.cmd.take_action(parsed_args) + # TODO(bapalm): Adjust this when cliff properly supports + # handling the detection rather than using the hard-code below. collist = ('Command Group', 'Commands') self.assertEqual(collist, columns) datalist = (( - 'test', - ['one', 'cmd two'], - ), ) + 'openstack.common', + 'limits show\nextension list' + ),) + self.assertEqual(datalist, tuple(data)) diff --git a/openstackclient/tests/common/test_parseractions.py b/openstackclient/tests/common/test_parseractions.py index a4ee07b..5c5ca3d 100644 --- a/openstackclient/tests/common/test_parseractions.py +++ b/openstackclient/tests/common/test_parseractions.py @@ -91,11 +91,7 @@ class TestMultiKeyValueAction(utils.TestCase): {'req1': 'aaa', 'req2': 'bbb'}, {'req1': '', 'req2': ''}, ] - # Need to sort the lists before comparing them - key = lambda x: x['req1'] - expect.sort(key=key) - actual.sort(key=key) - self.assertListEqual(expect, actual) + self.assertItemsEqual(expect, actual) def test_empty_required_optional(self): self.parser.add_argument( @@ -119,11 +115,7 @@ class TestMultiKeyValueAction(utils.TestCase): {'req1': 'aaa', 'req2': 'bbb'}, {'req1': '', 'req2': ''}, ] - # Need to sort the lists before comparing them - key = lambda x: x['req1'] - expect.sort(key=key) - actual.sort(key=key) - self.assertListEqual(expect, actual) + self.assertItemsEqual(expect, actual) def test_error_values_with_comma(self): self.assertRaises( diff --git a/openstackclient/tests/common/test_quota.py b/openstackclient/tests/common/test_quota.py index edf29c9..c9ec559 100644 --- a/openstackclient/tests/common/test_quota.py +++ b/openstackclient/tests/common/test_quota.py @@ -59,6 +59,7 @@ class TestQuota(compute_fakes.TestComputev2): self.service_catalog_mock = \ self.app.client_manager.auth_ref.service_catalog self.service_catalog_mock.reset_mock() + self.app.client_manager.auth_ref.project_id = identity_fakes.project_id class TestQuotaSet(TestQuota): @@ -96,6 +97,9 @@ class TestQuotaSet(TestQuota): loaded=True, ) + self.network_mock = self.app.client_manager.network + self.network_mock.update_quota = mock.Mock() + self.cmd = quota.SetQuota(self.app, None) def test_quota_set(self): @@ -131,6 +135,7 @@ class TestQuotaSet(TestQuota): ('project', identity_fakes.project_name), ] + self.app.client_manager.network_endpoint_enabled = False parsed_args = self.check_parser(self.cmd, arglist, verifylist) self.cmd.take_action(parsed_args) @@ -184,6 +189,61 @@ class TestQuotaSet(TestQuota): **kwargs ) + def test_quota_set_network(self): + arglist = [ + '--subnets', str(network_fakes.QUOTA['subnet']), + '--networks', str(network_fakes.QUOTA['network']), + '--floating-ips', str(network_fakes.QUOTA['floatingip']), + '--subnetpools', str(network_fakes.QUOTA['subnetpool']), + '--secgroup-rules', + str(network_fakes.QUOTA['security_group_rule']), + '--secgroups', str(network_fakes.QUOTA['security_group']), + '--routers', str(network_fakes.QUOTA['router']), + '--rbac-policies', str(network_fakes.QUOTA['rbac_policy']), + '--ports', str(network_fakes.QUOTA['port']), + '--vips', str(network_fakes.QUOTA['vip']), + '--members', str(network_fakes.QUOTA['member']), + '--health-monitors', str(network_fakes.QUOTA['health_monitor']), + identity_fakes.project_name, + ] + verifylist = [ + ('subnet', network_fakes.QUOTA['subnet']), + ('network', network_fakes.QUOTA['network']), + ('floatingip', network_fakes.QUOTA['floatingip']), + ('subnetpool', network_fakes.QUOTA['subnetpool']), + ('security_group_rule', + network_fakes.QUOTA['security_group_rule']), + ('security_group', network_fakes.QUOTA['security_group']), + ('router', network_fakes.QUOTA['router']), + ('rbac_policy', network_fakes.QUOTA['rbac_policy']), + ('port', network_fakes.QUOTA['port']), + ('vip', network_fakes.QUOTA['vip']), + ('member', network_fakes.QUOTA['member']), + ('health_monitor', network_fakes.QUOTA['health_monitor']), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + kwargs = { + 'subnet': network_fakes.QUOTA['subnet'], + 'network': network_fakes.QUOTA['network'], + 'floatingip': network_fakes.QUOTA['floatingip'], + 'subnetpool': network_fakes.QUOTA['subnetpool'], + 'security_group_rule': + network_fakes.QUOTA['security_group_rule'], + 'security_group': network_fakes.QUOTA['security_group'], + 'router': network_fakes.QUOTA['router'], + 'rbac_policy': network_fakes.QUOTA['rbac_policy'], + 'port': network_fakes.QUOTA['port'], + 'vip': network_fakes.QUOTA['vip'], + 'member': network_fakes.QUOTA['member'], + 'health_monitor': network_fakes.QUOTA['health_monitor'], + } + self.network_mock.update_quota.assert_called_with( + identity_fakes.project_id, + **kwargs + ) + class TestQuotaShow(TestQuota): @@ -304,3 +364,13 @@ class TestQuotaShow(TestQuota): identity_fakes.project_id) self.volume_quotas_class_mock.get.assert_called_with( identity_fakes.project_id) + + def test_quota_show_no_project(self): + parsed_args = self.check_parser(self.cmd, [], []) + + self.cmd.take_action(parsed_args) + + self.quotas_mock.get.assert_called_with(identity_fakes.project_id) + self.volume_quotas_mock.get.assert_called_with( + identity_fakes.project_id) + self.network.get_quota.assert_called_with(identity_fakes.project_id) diff --git a/openstackclient/tests/common/test_utils.py b/openstackclient/tests/common/test_utils.py index 95bce45..2248d04 100644 --- a/openstackclient/tests/common/test_utils.py +++ b/openstackclient/tests/common/test_utils.py @@ -306,6 +306,18 @@ class TestFindResource(test_utils.TestCase): self.manager.get.assert_called_with(self.name) self.manager.find.assert_called_with(name=self.name) + def test_find_resource_list_forbidden(self): + self.manager.get = mock.Mock(side_effect=Exception('Boom!')) + self.manager.find = mock.Mock(side_effect=Exception('Boom!')) + self.manager.list = mock.Mock( + side_effect=exceptions.Forbidden(403) + ) + self.assertRaises(exceptions.Forbidden, + utils.find_resource, + self.manager, + self.name) + self.manager.list.assert_called_with() + def test_find_resource_find_no_unique(self): self.manager.get = mock.Mock(side_effect=Exception('Boom!')) self.manager.find = mock.Mock(side_effect=NoUniqueMatch()) diff --git a/openstackclient/tests/compute/v2/fakes.py b/openstackclient/tests/compute/v2/fakes.py index 29baa8e..62a46b1 100644 --- a/openstackclient/tests/compute/v2/fakes.py +++ b/openstackclient/tests/compute/v2/fakes.py @@ -76,14 +76,36 @@ QUOTA = { QUOTA_columns = tuple(sorted(QUOTA)) QUOTA_data = tuple(QUOTA[x] for x in sorted(QUOTA)) -service_host = 'host_test' -service_binary = 'compute_test' -service_status = 'enabled' -SERVICE = { - 'host': service_host, - 'binary': service_binary, - 'status': service_status, -} + +class FakeAggregate(object): + """Fake one aggregate.""" + + @staticmethod + def create_one_aggregate(attrs=None): + """Create a fake aggregate. + + :param Dictionary attrs: + A dictionary with all attributes + :return: + A FakeResource object, with id and other attributes + """ + attrs = attrs or {} + + # Set default attribute + aggregate_info = { + "name": "aggregate-name-" + uuid.uuid4().hex, + "availability_zone": "ag_zone", + "hosts": [], + "id": "aggregate-id-" + uuid.uuid4().hex, + "metadata": { + "availability_zone": "ag_zone", + } + } + aggregate_info.update(attrs) + aggregate = fakes.FakeResource( + info=copy.deepcopy(aggregate_info), + loaded=True) + return aggregate class FakeComputev2Client(object): @@ -137,6 +159,15 @@ class FakeComputev2Client(object): self.networks = mock.Mock() self.networks.resource_class = fakes.FakeResource(None, {}) + self.keypairs = mock.Mock() + self.keypairs.resource_class = fakes.FakeResource(None, {}) + + self.hosts = mock.Mock() + self.hosts.resource_class = fakes.FakeResource(None, {}) + + self.server_groups = mock.Mock() + self.server_groups.resource_class = fakes.FakeResource(None, {}) + self.auth_token = kwargs['token'] self.management_url = kwargs['endpoint'] @@ -177,7 +208,7 @@ class FakeHypervisor(object): """Fake one or more hypervisor.""" @staticmethod - def create_one_hypervisor(attrs={}): + def create_one_hypervisor(attrs=None): """Create a fake hypervisor. :param Dictionary attrs: @@ -185,6 +216,8 @@ class FakeHypervisor(object): :return: A FakeResource object, with id, hypervisor_hostname, and so on """ + attrs = attrs or {} + # Set default attributes. hypervisor_info = { 'id': 'hypervisor-id-' + uuid.uuid4().hex, @@ -223,7 +256,7 @@ class FakeHypervisor(object): return hypervisor @staticmethod - def create_hypervisors(attrs={}, count=2): + def create_hypervisors(attrs=None, count=2): """Create multiple fake hypervisors. :param Dictionary attrs: @@ -240,11 +273,11 @@ class FakeHypervisor(object): return hypervisors -class FakehypervisorStats(object): +class FakeHypervisorStats(object): """Fake one or more hypervisor stats.""" @staticmethod - def create_one_hypervisor_stats(attrs={}, methods={}): + def create_one_hypervisor_stats(attrs=None): """Create a fake hypervisor stats. :param Dictionary attrs: @@ -252,6 +285,8 @@ class FakehypervisorStats(object): :return: A FakeResource object, with id, hypervisor_hostname, and so on """ + attrs = attrs or {} + # Set default attributes. stats_info = { 'count': 2, @@ -271,7 +306,6 @@ class FakehypervisorStats(object): # Set default method. hypervisor_stats_method = {'to_dict': stats_info} - hypervisor_stats_method.update(methods) hypervisor_stats = fakes.FakeResource( info=copy.deepcopy(stats_info), @@ -280,7 +314,7 @@ class FakehypervisorStats(object): return hypervisor_stats @staticmethod - def create_hypervisors_stats(attrs={}, count=2): + def create_hypervisors_stats(attrs=None, count=2): """Create multiple fake hypervisors stats. :param Dictionary attrs: @@ -293,7 +327,7 @@ class FakehypervisorStats(object): hypervisors = [] for i in range(0, count): hypervisors.append( - FakehypervisorStats.create_one_hypervisor_stats(attrs)) + FakeHypervisorStats.create_one_hypervisor_stats(attrs)) return hypervisors @@ -302,20 +336,15 @@ class FakeSecurityGroup(object): """Fake one or more security groups.""" @staticmethod - def create_one_security_group(attrs=None, methods=None): + def create_one_security_group(attrs=None): """Create a fake security group. :param Dictionary attrs: A dictionary with all attributes - :param Dictionary methods: - A dictionary with all methods :return: A FakeResource object, with id, name, etc. """ - if attrs is None: - attrs = {} - if methods is None: - methods = {} + attrs = attrs or {} # Set default attributes. security_group_attrs = { @@ -329,26 +358,17 @@ class FakeSecurityGroup(object): # Overwrite default attributes. security_group_attrs.update(attrs) - # Set default methods. - security_group_methods = {} - - # Overwrite default methods. - security_group_methods.update(methods) - security_group = fakes.FakeResource( info=copy.deepcopy(security_group_attrs), - methods=copy.deepcopy(security_group_methods), loaded=True) return security_group @staticmethod - def create_security_groups(attrs=None, methods=None, count=2): + def create_security_groups(attrs=None, count=2): """Create multiple fake security groups. :param Dictionary attrs: A dictionary with all attributes - :param Dictionary methods: - A dictionary with all methods :param int count: The number of security groups to fake :return: @@ -357,7 +377,7 @@ class FakeSecurityGroup(object): security_groups = [] for i in range(0, count): security_groups.append( - FakeSecurityGroup.create_one_security_group(attrs, methods)) + FakeSecurityGroup.create_one_security_group(attrs)) return security_groups @@ -366,50 +386,41 @@ class FakeSecurityGroupRule(object): """Fake one or more security group rules.""" @staticmethod - def create_one_security_group_rule(attrs={}, methods={}): + def create_one_security_group_rule(attrs=None): """Create a fake security group rule. :param Dictionary attrs: A dictionary with all attributes - :param Dictionary methods: - A dictionary with all methods :return: A FakeResource object, with id, etc. """ + attrs = attrs or {} + # Set default attributes. security_group_rule_attrs = { - 'from_port': -1, + 'from_port': 0, 'group': {}, 'id': 'security-group-rule-id-' + uuid.uuid4().hex, - 'ip_protocol': 'icmp', + 'ip_protocol': 'tcp', 'ip_range': {'cidr': '0.0.0.0/0'}, 'parent_group_id': 'security-group-id-' + uuid.uuid4().hex, - 'to_port': -1, + 'to_port': 0, } # Overwrite default attributes. security_group_rule_attrs.update(attrs) - # Set default methods. - security_group_rule_methods = {} - - # Overwrite default methods. - security_group_rule_methods.update(methods) - security_group_rule = fakes.FakeResource( info=copy.deepcopy(security_group_rule_attrs), - methods=copy.deepcopy(security_group_rule_methods), loaded=True) return security_group_rule @staticmethod - def create_security_group_rules(attrs={}, methods={}, count=2): + def create_security_group_rules(attrs=None, count=2): """Create multiple fake security group rules. :param Dictionary attrs: A dictionary with all attributes - :param Dictionary methods: - A dictionary with all methods :param int count: The number of security group rules to fake :return: @@ -418,8 +429,7 @@ class FakeSecurityGroupRule(object): security_group_rules = [] for i in range(0, count): security_group_rules.append( - FakeSecurityGroupRule.create_one_security_group_rule( - attrs, methods)) + FakeSecurityGroupRule.create_one_security_group_rule(attrs)) return security_group_rules @@ -428,7 +438,7 @@ class FakeServer(object): """Fake one or more compute servers.""" @staticmethod - def create_one_server(attrs={}, methods={}): + def create_one_server(attrs=None, methods=None): """Create a fake server. :param Dictionary attrs: @@ -438,6 +448,9 @@ class FakeServer(object): :return: A FakeResource object, with id, name, metadata """ + attrs = attrs or {} + methods = methods or {} + # Set default attributes. server_info = { 'id': 'server-id-' + uuid.uuid4().hex, @@ -448,7 +461,8 @@ class FakeServer(object): }, 'flavor': { 'id': 'flavor-id-' + uuid.uuid4().hex, - } + }, + 'OS-EXT-STS:power_state': 1, } # Overwrite default attributes. @@ -460,7 +474,7 @@ class FakeServer(object): return server @staticmethod - def create_servers(attrs={}, methods={}, count=2): + def create_servers(attrs=None, methods=None, count=2): """Create multiple fake servers. :param Dictionary attrs: @@ -498,42 +512,68 @@ class FakeServer(object): return mock.MagicMock(side_effect=servers) -class FakeFlavorResource(fakes.FakeResource): - """Fake flavor object's methods to help test. +class FakeService(object): + """Fake one or more services.""" - The flavor object has three methods to get, set, unset its properties. - Need to fake them, otherwise the functions to be tested won't run properly. - """ + @staticmethod + def create_one_service(attrs=None): + """Create a fake service. - def __init__(self, manager=None, info={}, loaded=False, methods={}): - super(FakeFlavorResource, self).__init__(manager, info, - loaded, methods) - # Fake properties. - self._keys = {'property': 'value'} + :param Dictionary attrs: + A dictionary with all attributes + :return: + A FakeResource object, with id, name, ram, vcpus, properties + """ + attrs = attrs or {} + + # Set default attributes. + service_info = { + 'host': 'host-' + uuid.uuid4().hex, + 'binary': 'binary-' + uuid.uuid4().hex, + 'status': 'enabled', + 'disabled_reason': 'earthquake', + } - def set_keys(self, args): - self._keys.update(args) + # Overwrite default attributes. + service_info.update(attrs) - def unset_keys(self, keys): - for key in keys: - self._keys.pop(key, None) + service = fakes.FakeResource(info=copy.deepcopy(service_info), + loaded=True) - def get_keys(self): - return self._keys + return service + + @staticmethod + def create_services(attrs=None, count=2): + """Create multiple fake services. + + :param Dictionary attrs: + A dictionary with all attributes + :param int count: + The number of services to fake + :return: + A list of FakeResource objects faking the services + """ + services = [] + for i in range(0, count): + services.append(FakeService.create_one_service(attrs)) + + return services class FakeFlavor(object): """Fake one or more flavors.""" @staticmethod - def create_one_flavor(attrs={}): + def create_one_flavor(attrs=None): """Create a fake flavor. :param Dictionary attrs: A dictionary with all attributes :return: - A FakeFlavorResource object, with id, name, ram, vcpus, properties + A FakeResource object, with id, name, ram, vcpus, properties """ + attrs = attrs or {} + # Set default attributes. flavor_info = { 'id': 'flavor-id-' + uuid.uuid4().hex, @@ -541,8 +581,8 @@ class FakeFlavor(object): 'ram': 8192, 'vcpus': 4, 'disk': 128, - 'swap': '', - 'rxtx_factor': '1.0', + 'swap': 0, + 'rxtx_factor': 1.0, 'OS-FLV-DISABLED:disabled': False, 'os-flavor-access:is_public': True, 'OS-FLV-EXT-DATA:ephemeral': 0, @@ -551,7 +591,15 @@ class FakeFlavor(object): # Overwrite default attributes. flavor_info.update(attrs) - flavor = FakeFlavorResource(info=copy.deepcopy(flavor_info), + # Set default methods. + flavor_methods = { + 'set_keys': None, + 'unset_keys': None, + 'get_keys': {'property': 'value'}, + } + + flavor = fakes.FakeResource(info=copy.deepcopy(flavor_info), + methods=flavor_methods, loaded=True) # Set attributes with special mappings in nova client. @@ -562,7 +610,7 @@ class FakeFlavor(object): return flavor @staticmethod - def create_flavors(attrs={}, count=2): + def create_flavors(attrs=None, count=2): """Create multiple fake flavors. :param Dictionary attrs: @@ -570,7 +618,7 @@ class FakeFlavor(object): :param int count: The number of flavors to fake :return: - A list of FakeFlavorResource objects faking the flavors + A list of FakeResource objects faking the flavors """ flavors = [] for i in range(0, count): @@ -586,7 +634,7 @@ class FakeFlavor(object): list. Otherwise create one. :param List flavors: - A list of FakeFlavorResource objects faking flavors + A list of FakeResource objects faking flavors :param int count: The number of flavors to fake :return: @@ -598,20 +646,71 @@ class FakeFlavor(object): return mock.MagicMock(side_effect=flavors) +class FakeKeypair(object): + """Fake one or more keypairs.""" + + @staticmethod + def create_one_keypair(attrs=None, no_pri=False): + """Create a fake keypair + + :param Dictionary attrs: + A dictionary with all attributes + :return: + A FakeResource + """ + attrs = attrs or {} + + # Set default attributes. + keypair_info = { + 'name': 'keypair-name-' + uuid.uuid4().hex, + 'fingerprint': 'dummy', + 'public_key': 'dummy', + 'user_id': 'user' + } + if not no_pri: + keypair_info['private_key'] = 'private_key' + + # Overwrite default attributes. + keypair_info.update(attrs) + + keypair = fakes.FakeResource(info=copy.deepcopy(keypair_info), + loaded=True) + + return keypair + + @staticmethod + def create_keypairs(attrs=None, count=2): + """Create multiple fake keypairs. + + :param Dictionary attrs: + A dictionary with all attributes + :param int count: + The number of keypairs to fake + :return: + A list of FakeResource objects faking the keypairs + """ + + keypairs = [] + for i in range(0, count): + keypairs.append(FakeKeypair.create_one_keypair(attrs)) + + return keypairs + + class FakeAvailabilityZone(object): """Fake one or more compute availability zones (AZs).""" @staticmethod - def create_one_availability_zone(attrs={}, methods={}): + def create_one_availability_zone(attrs=None): """Create a fake AZ. :param Dictionary attrs: A dictionary with all attributes - :param Dictionary methods: - A dictionary with all methods :return: A FakeResource object with zoneName, zoneState, etc. """ + attrs = attrs or {} + # Set default attributes. host_name = uuid.uuid4().hex service_name = uuid.uuid4().hex @@ -631,18 +730,15 @@ class FakeAvailabilityZone(object): availability_zone = fakes.FakeResource( info=copy.deepcopy(availability_zone), - methods=methods, loaded=True) return availability_zone @staticmethod - def create_availability_zones(attrs={}, methods={}, count=2): + def create_availability_zones(attrs=None, count=2): """Create multiple fake AZs. :param Dictionary attrs: A dictionary with all attributes - :param Dictionary methods: - A dictionary with all methods :param int count: The number of AZs to fake :return: @@ -651,8 +747,7 @@ class FakeAvailabilityZone(object): availability_zones = [] for i in range(0, count): availability_zone = \ - FakeAvailabilityZone.create_one_availability_zone( - attrs, methods) + FakeAvailabilityZone.create_one_availability_zone(attrs) availability_zones.append(availability_zone) return availability_zones @@ -662,16 +757,16 @@ class FakeFloatingIP(object): """Fake one or more floating ip.""" @staticmethod - def create_one_floating_ip(attrs={}, methods={}): + def create_one_floating_ip(attrs=None): """Create a fake floating ip. :param Dictionary attrs: A dictionary with all attributes - :param Dictionary methods: - A dictionary with all methods :return: A FakeResource object, with id, ip, and so on """ + attrs = attrs or {} + # Set default attributes. floating_ip_attrs = { 'id': 'floating-ip-id-' + uuid.uuid4().hex, @@ -684,27 +779,18 @@ class FakeFloatingIP(object): # Overwrite default attributes. floating_ip_attrs.update(attrs) - # Set default methods. - floating_ip_methods = {} - - # Overwrite default methods. - floating_ip_methods.update(methods) - floating_ip = fakes.FakeResource( info=copy.deepcopy(floating_ip_attrs), - methods=copy.deepcopy(floating_ip_methods), loaded=True) return floating_ip @staticmethod - def create_floating_ips(attrs={}, methods={}, count=2): + def create_floating_ips(attrs=None, count=2): """Create multiple fake floating ips. :param Dictionary attrs: A dictionary with all attributes - :param Dictionary methods: - A dictionary with all methods :param int count: The number of floating ips to fake :return: @@ -712,10 +798,7 @@ class FakeFloatingIP(object): """ floating_ips = [] for i in range(0, count): - floating_ips.append(FakeFloatingIP.create_one_floating_ip( - attrs, - methods - )) + floating_ips.append(FakeFloatingIP.create_one_floating_ip(attrs)) return floating_ips @staticmethod @@ -742,16 +825,16 @@ class FakeNetwork(object): """Fake one or more networks.""" @staticmethod - def create_one_network(attrs={}, methods={}): + def create_one_network(attrs=None): """Create a fake network. :param Dictionary attrs: A dictionary with all attributes - :param Dictionary methods: - A dictionary with all methods :return: A FakeResource object, with id, label, cidr and so on """ + attrs = attrs or {} + # Set default attributes. network_attrs = { 'bridge': 'br100', @@ -791,28 +874,17 @@ class FakeNetwork(object): # Overwrite default attributes. network_attrs.update(attrs) - # Set default methods. - network_methods = { - 'keys': ['id', 'label', 'cidr'], - } - - # Overwrite default methods. - network_methods.update(methods) - network = fakes.FakeResource(info=copy.deepcopy(network_attrs), - methods=copy.deepcopy(network_methods), loaded=True) return network @staticmethod - def create_networks(attrs={}, methods={}, count=2): + def create_networks(attrs=None, count=2): """Create multiple fake networks. :param Dictionary attrs: A dictionary with all attributes - :param Dictionary methods: - A dictionary with all methods :param int count: The number of networks to fake :return: @@ -820,6 +892,108 @@ class FakeNetwork(object): """ networks = [] for i in range(0, count): - networks.append(FakeNetwork.create_one_network(attrs, methods)) + networks.append(FakeNetwork.create_one_network(attrs)) return networks + + @staticmethod + def get_networks(networks=None, count=2): + """Get an iterable MagicMock object with a list of faked networks. + + If networks list is provided, then initialize the Mock object with the + list. Otherwise create one. + + :param List networks: + A list of FakeResource objects faking networks + :param int count: + The number of networks to fake + :return: + An iterable Mock object with side_effect set to a list of faked + networks + """ + if networks is None: + networks = FakeNetwork.create_networks(count=count) + return mock.Mock(side_effect=networks) + + +class FakeHost(object): + """Fake one host.""" + + @staticmethod + def create_one_host(attrs=None): + """Create a fake host. + + :param Dictionary attrs: + A dictionary with all attributes + :return: + A FakeResource object, with id and other attributes + """ + attrs = attrs or {} + + # Set default attributes. + host_info = { + "id": 1, + "service_id": 1, + "host": "host1", + "uuid": 'host-id-' + uuid.uuid4().hex, + "vcpus": 10, + "memory_mb": 100, + "local_gb": 100, + "vcpus_used": 5, + "memory_mb_used": 50, + "local_gb_used": 10, + "hypervisor_type": "xen", + "hypervisor_version": 1, + "hypervisor_hostname": "devstack1", + "free_ram_mb": 50, + "free_disk_gb": 50, + "current_workload": 10, + "running_vms": 1, + "cpu_info": "", + "disk_available_least": 1, + "host_ip": "10.10.10.10", + "supported_instances": "", + "metrics": "", + "pci_stats": "", + "extra_resources": "", + "stats": "", + "numa_topology": "", + "ram_allocation_ratio": 1.0, + "cpu_allocation_ratio": 1.0 + } + host_info.update(attrs) + host = fakes.FakeResource( + info=copy.deepcopy(host_info), + loaded=True) + return host + + +class FakeServerGroup(object): + """Fake one server group""" + + @staticmethod + def create_one_server_group(attrs=None): + """Create a fake server group + + :param Dictionary attrs: + A dictionary with all attributes + :return: + A FakeResource object, with id and other attributes + """ + if attrs is None: + attrs = {} + + server_group_info = { + 'id': 'server-group-id-' + uuid.uuid4().hex, + 'members': [], + 'metadata': {}, + 'name': 'server-group-name-' + uuid.uuid4().hex, + 'policies': [], + 'project_id': 'server-group-project-id-' + uuid.uuid4().hex, + 'user_id': 'server-group-user-id-' + uuid.uuid4().hex, + } + server_group_info.update(attrs) + server_group = fakes.FakeResource( + info=copy.deepcopy(server_group_info), + loaded=True) + return server_group diff --git a/openstackclient/tests/compute/v2/test_aggregate.py b/openstackclient/tests/compute/v2/test_aggregate.py new file mode 100644 index 0000000..58dd775 --- /dev/null +++ b/openstackclient/tests/compute/v2/test_aggregate.py @@ -0,0 +1,392 @@ +# Copyright 2016 Huawei, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# + +from openstackclient.compute.v2 import aggregate +from openstackclient.tests.compute.v2 import fakes as compute_fakes +from openstackclient.tests import utils as tests_utils + + +class TestAggregate(compute_fakes.TestComputev2): + + fake_ag = compute_fakes.FakeAggregate.create_one_aggregate() + + columns = ( + 'availability_zone', + 'hosts', + 'id', + 'metadata', + 'name', + ) + + data = ( + fake_ag.availability_zone, + fake_ag.hosts, + fake_ag.id, + fake_ag.metadata, + fake_ag.name, + ) + + def setUp(self): + super(TestAggregate, self).setUp() + + # Get a shortcut to the AggregateManager Mock + self.aggregate_mock = self.app.client_manager.compute.aggregates + self.aggregate_mock.reset_mock() + + +class TestAggregateAddHost(TestAggregate): + + def setUp(self): + super(TestAggregateAddHost, self).setUp() + + self.aggregate_mock.get.return_value = self.fake_ag + self.aggregate_mock.add_host.return_value = self.fake_ag + self.cmd = aggregate.AddAggregateHost(self.app, None) + + def test_aggregate_add_host(self): + arglist = [ + 'ag1', + 'host1', + ] + verifylist = [ + ('aggregate', 'ag1'), + ('host', 'host1'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + self.aggregate_mock.get.assert_called_once_with(parsed_args.aggregate) + self.aggregate_mock.add_host.assert_called_once_with(self.fake_ag, + parsed_args.host) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + +class TestAggregateCreate(TestAggregate): + + def setUp(self): + super(TestAggregateCreate, self).setUp() + + self.aggregate_mock.create.return_value = self.fake_ag + self.aggregate_mock.set_metadata.return_value = self.fake_ag + self.cmd = aggregate.CreateAggregate(self.app, None) + + def test_aggregate_create(self): + arglist = [ + 'ag1', + ] + verifylist = [ + ('name', 'ag1'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + self.aggregate_mock.create.assert_called_once_with(parsed_args.name, + None) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + def test_aggregate_create_with_zone(self): + arglist = [ + '--zone', 'zone1', + 'ag1', + ] + verifylist = [ + ('zone', 'zone1'), + ('name', 'ag1'), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + self.aggregate_mock.create.assert_called_once_with(parsed_args.name, + parsed_args.zone) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + def test_aggregate_create_with_property(self): + arglist = [ + '--property', 'key1=value1', + '--property', 'key2=value2', + 'ag1', + ] + verifylist = [ + ('property', {'key1': 'value1', 'key2': 'value2'}), + ('name', 'ag1'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + self.aggregate_mock.create.assert_called_once_with(parsed_args.name, + None) + self.aggregate_mock.set_metadata.assert_called_once_with( + self.fake_ag, parsed_args.property) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + +class TestAggregateDelete(TestAggregate): + + def setUp(self): + super(TestAggregateDelete, self).setUp() + + self.aggregate_mock.get.return_value = self.fake_ag + self.cmd = aggregate.DeleteAggregate(self.app, None) + + def test_aggregate_delete(self): + arglist = [ + 'ag1', + ] + verifylist = [ + ('aggregate', 'ag1'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + self.aggregate_mock.get.assert_called_once_with(parsed_args.aggregate) + self.aggregate_mock.delete.assert_called_once_with(self.fake_ag.id) + self.assertIsNone(result) + + +class TestAggregateList(TestAggregate): + + list_columns = ( + "ID", + "Name", + "Availability Zone", + ) + + list_columns_long = ( + "ID", + "Name", + "Availability Zone", + "Properties", + ) + + list_data = (( + TestAggregate.fake_ag.id, + TestAggregate.fake_ag.name, + TestAggregate.fake_ag.availability_zone, + ), ) + + list_data_long = (( + TestAggregate.fake_ag.id, + TestAggregate.fake_ag.name, + TestAggregate.fake_ag.availability_zone, + {}, + ), ) + + def setUp(self): + super(TestAggregateList, self).setUp() + + self.aggregate_mock.list.return_value = [self.fake_ag] + self.cmd = aggregate.ListAggregate(self.app, None) + + def test_aggregate_list(self): + + parsed_args = self.check_parser(self.cmd, [], []) + columns, data = self.cmd.take_action(parsed_args) + + self.assertEqual(self.list_columns, columns) + self.assertEqual(self.list_data, tuple(data)) + + def test_aggregate_list_with_long(self): + arglist = [ + '--long', + ] + vertifylist = [ + ('long', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, vertifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.assertEqual(self.list_columns_long, columns) + self.assertEqual(self.list_data_long, tuple(data)) + + +class TestAggregateRemoveHost(TestAggregate): + + def setUp(self): + super(TestAggregateRemoveHost, self).setUp() + + self.aggregate_mock.get.return_value = self.fake_ag + self.aggregate_mock.remove_host.return_value = self.fake_ag + self.cmd = aggregate.RemoveAggregateHost(self.app, None) + + def test_aggregate_add_host(self): + arglist = [ + 'ag1', + 'host1', + ] + verifylist = [ + ('aggregate', 'ag1'), + ('host', 'host1'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + self.aggregate_mock.get.assert_called_once_with(parsed_args.aggregate) + self.aggregate_mock.remove_host.assert_called_once_with( + self.fake_ag, parsed_args.host) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + +class TestAggregateSet(TestAggregate): + + def setUp(self): + super(TestAggregateSet, self).setUp() + + self.aggregate_mock.get.return_value = self.fake_ag + self.cmd = aggregate.SetAggregate(self.app, None) + + def test_aggregate_set_no_option(self): + arglist = [ + 'ag1', + ] + verifylist = [ + ('aggregate', 'ag1'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + self.aggregate_mock.get.assert_called_once_with(parsed_args.aggregate) + self.assertNotCalled(self.aggregate_mock.update) + self.assertNotCalled(self.aggregate_mock.set_metadata) + self.assertIsNone(result) + + def test_aggregate_set_with_name(self): + arglist = [ + '--name', 'new_name', + 'ag1', + ] + verifylist = [ + ('name', 'new_name'), + ('aggregate', 'ag1'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + self.aggregate_mock.get.assert_called_once_with(parsed_args.aggregate) + self.aggregate_mock.update.assert_called_once_with( + self.fake_ag, {'name': parsed_args.name}) + self.assertNotCalled(self.aggregate_mock.set_metadata) + self.assertIsNone(result) + + def test_aggregate_set_with_zone(self): + arglist = [ + '--zone', 'new_zone', + 'ag1', + ] + verifylist = [ + ('zone', 'new_zone'), + ('aggregate', 'ag1'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + self.aggregate_mock.get.assert_called_once_with(parsed_args.aggregate) + self.aggregate_mock.update.assert_called_once_with( + self.fake_ag, {'availability_zone': parsed_args.zone}) + self.assertNotCalled(self.aggregate_mock.set_metadata) + self.assertIsNone(result) + + def test_aggregate_set_with_property(self): + arglist = [ + '--property', 'key1=value1', + '--property', 'key2=value2', + 'ag1', + ] + verifylist = [ + ('property', {'key1': 'value1', 'key2': 'value2'}), + ('aggregate', 'ag1'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + self.aggregate_mock.get.assert_called_once_with(parsed_args.aggregate) + self.assertNotCalled(self.aggregate_mock.update) + self.aggregate_mock.set_metadata.assert_called_once_with( + self.fake_ag, parsed_args.property) + self.assertIsNone(result) + + +class TestAggregateShow(TestAggregate): + + columns = ( + 'availability_zone', + 'hosts', + 'id', + 'name', + 'properties', + ) + + data = ( + TestAggregate.fake_ag.availability_zone, + TestAggregate.fake_ag.hosts, + TestAggregate.fake_ag.id, + TestAggregate.fake_ag.name, + '', + ) + + def setUp(self): + super(TestAggregateShow, self).setUp() + + self.aggregate_mock.get.return_value = self.fake_ag + self.cmd = aggregate.ShowAggregate(self.app, None) + + def test_aggregate_show(self): + arglist = [ + 'ag1', + ] + verifylist = [ + ('aggregate', 'ag1'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + self.aggregate_mock.get.assert_called_once_with(parsed_args.aggregate) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + +class TestAggregateUnset(TestAggregate): + + def setUp(self): + super(TestAggregateUnset, self).setUp() + + self.aggregate_mock.get.return_value = self.fake_ag + self.cmd = aggregate.UnsetAggregate(self.app, None) + + def test_aggregate_unset(self): + arglist = [ + '--property', 'unset_key', + 'ag1', + ] + verifylist = [ + ('property', ['unset_key']), + ('aggregate', 'ag1'), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + self.aggregate_mock.set_metadata.assert_called_once_with( + self.fake_ag, {'unset_key': None}) + self.assertIsNone(result) + + def test_aggregate_unset_no_property(self): + arglist = [ + 'ag1', + ] + verifylist = None + self.assertRaises(tests_utils.ParserException, + self.check_parser, + self.cmd, + arglist, + verifylist) diff --git a/openstackclient/tests/compute/v2/test_flavor.py b/openstackclient/tests/compute/v2/test_flavor.py index 03ca880..6f507b1 100644 --- a/openstackclient/tests/compute/v2/test_flavor.py +++ b/openstackclient/tests/compute/v2/test_flavor.py @@ -30,6 +30,166 @@ class TestFlavor(compute_fakes.TestComputev2): self.flavors_mock.reset_mock() +class TestFlavorCreate(TestFlavor): + + flavor = compute_fakes.FakeFlavor.create_one_flavor( + attrs={'links': 'flavor-links'}) + + columns = ( + 'OS-FLV-DISABLED:disabled', + 'OS-FLV-EXT-DATA:ephemeral', + 'disk', + 'id', + 'name', + 'os-flavor-access:is_public', + 'ram', + 'rxtx_factor', + 'swap', + 'vcpus', + ) + data = ( + flavor.disabled, + flavor.ephemeral, + flavor.disk, + flavor.id, + flavor.name, + flavor.is_public, + flavor.ram, + flavor.rxtx_factor, + flavor.swap, + flavor.vcpus, + ) + + def setUp(self): + super(TestFlavorCreate, self).setUp() + + self.flavors_mock.create.return_value = self.flavor + self.cmd = flavor.CreateFlavor(self.app, None) + + def test_flavor_create_default_options(self): + + arglist = [ + self.flavor.name + ] + verifylist = [ + ('name', self.flavor.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + default_args = ( + self.flavor.name, + 256, + 1, + 0, + 'auto', + 0, + 0, + 1.0, + True + ) + columns, data = self.cmd.take_action(parsed_args) + self.flavors_mock.create.assert_called_once_with(*default_args) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + def test_flavor_create_all_options(self): + + arglist = [ + self.flavor.name, + '--id', self.flavor.id, + '--ram', str(self.flavor.ram), + '--disk', str(self.flavor.disk), + '--ephemeral', str(self.flavor.ephemeral), + '--swap', str(self.flavor.swap), + '--vcpus', str(self.flavor.vcpus), + '--rxtx-factor', str(self.flavor.rxtx_factor), + '--public', + ] + verifylist = [ + ('name', self.flavor.name), + ('id', self.flavor.id), + ('ram', self.flavor.ram), + ('disk', self.flavor.disk), + ('ephemeral', self.flavor.ephemeral), + ('swap', self.flavor.swap), + ('vcpus', self.flavor.vcpus), + ('rxtx_factor', self.flavor.rxtx_factor), + ('public', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + args = ( + self.flavor.name, + self.flavor.ram, + self.flavor.vcpus, + self.flavor.disk, + self.flavor.id, + self.flavor.ephemeral, + self.flavor.swap, + self.flavor.rxtx_factor, + self.flavor.is_public, + ) + columns, data = self.cmd.take_action(parsed_args) + self.flavors_mock.create.assert_called_once_with(*args) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + def test_flavor_create_other_options(self): + + self.flavor.is_public = False + arglist = [ + self.flavor.name, + '--id', self.flavor.id, + '--ram', str(self.flavor.ram), + '--disk', str(self.flavor.disk), + '--ephemeral', str(self.flavor.ephemeral), + '--swap', str(self.flavor.swap), + '--vcpus', str(self.flavor.vcpus), + '--rxtx-factor', str(self.flavor.rxtx_factor), + '--private', + ] + verifylist = [ + ('name', self.flavor.name), + ('id', self.flavor.id), + ('ram', self.flavor.ram), + ('disk', self.flavor.disk), + ('ephemeral', self.flavor.ephemeral), + ('swap', self.flavor.swap), + ('vcpus', self.flavor.vcpus), + ('rxtx_factor', self.flavor.rxtx_factor), + ('public', False), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + args = ( + self.flavor.name, + self.flavor.ram, + self.flavor.vcpus, + self.flavor.disk, + self.flavor.id, + self.flavor.ephemeral, + self.flavor.swap, + self.flavor.rxtx_factor, + self.flavor.is_public, + ) + columns, data = self.cmd.take_action(parsed_args) + self.flavors_mock.create.assert_called_once_with(*args) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + def test_flavor_create_no_options(self): + arglist = [] + verifylist = None + self.assertRaises(tests_utils.ParserException, + self.check_parser, + self.cmd, + arglist, + verifylist) + + class TestFlavorDelete(TestFlavor): flavor = compute_fakes.FakeFlavor.create_one_flavor() @@ -273,7 +433,7 @@ class TestFlavorSet(TestFlavor): super(TestFlavorSet, self).setUp() self.flavors_mock.find.return_value = self.flavor - + self.flavors_mock.get.side_effect = exceptions.NotFound(None) self.cmd = flavor.SetFlavor(self.app, None) def test_flavor_set(self): @@ -288,8 +448,8 @@ class TestFlavorSet(TestFlavor): parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) - - self.flavors_mock.find.assert_called_with(name='baremetal') + self.flavors_mock.find.assert_called_with(name=parsed_args.flavor, + is_public=None) self.assertIsNone(result) @@ -329,9 +489,9 @@ class TestFlavorShow(TestFlavor): def setUp(self): super(TestFlavorShow, self).setUp() - # Return value of utils.find_resource() - self.flavors_mock.get.return_value = self.flavor - + # Return value of _find_resource() + self.flavors_mock.find.return_value = self.flavor + self.flavors_mock.get.side_effect = exceptions.NotFound(None) self.cmd = flavor.ShowFlavor(self.app, None) def test_show_no_options(self): @@ -367,7 +527,7 @@ class TestFlavorUnset(TestFlavor): super(TestFlavorUnset, self).setUp() self.flavors_mock.find.return_value = self.flavor - + self.flavors_mock.get.side_effect = exceptions.NotFound(None) self.cmd = flavor.UnsetFlavor(self.app, None) def test_flavor_unset(self): @@ -382,6 +542,6 @@ class TestFlavorUnset(TestFlavor): parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) - - self.flavors_mock.find.assert_called_with(name='baremetal') + self.flavors_mock.find.assert_called_with(name=parsed_args.flavor, + is_public=None) self.assertIsNone(result) diff --git a/openstackclient/tests/compute/v2/test_host.py b/openstackclient/tests/compute/v2/test_host.py new file mode 100644 index 0000000..de53757 --- /dev/null +++ b/openstackclient/tests/compute/v2/test_host.py @@ -0,0 +1,75 @@ +# Copyright 2016 IBM Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# + +from openstackclient.compute.v2 import host +from openstackclient.tests.compute.v2 import fakes as compute_fakes + + +class TestHost(compute_fakes.TestComputev2): + + def setUp(self): + super(TestHost, self).setUp() + + # Get a shortcut to the FlavorManager Mock + self.host_mock = self.app.client_manager.compute.hosts + self.host_mock.reset_mock() + + +class TestHostSet(TestHost): + + def setUp(self): + super(TestHostSet, self).setUp() + + self.host = compute_fakes.FakeHost.create_one_host() + self.host_mock.get.return_value = self.host + self.host_mock.update.return_value = None + + self.cmd = host.SetHost(self.app, None) + + def test_host_set_no_option(self): + arglist = [ + str(self.host.id) + ] + verifylist = [ + ('host', str(self.host.id)) + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + self.assertIsNone(result) + + body = {} + self.host_mock.update.assert_called_with(self.host.id, body) + + def test_host_set(self): + arglist = [ + '--enable', + '--disable-maintenance', + str(self.host.id) + ] + verifylist = [ + ('enable', True), + ('enable_maintenance', False), + ('host', str(self.host.id)) + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + self.assertIsNone(result) + + body = {'status': True, 'maintenance_mode': False} + self.host_mock.update.assert_called_with(self.host.id, body) diff --git a/openstackclient/tests/compute/v2/test_hypervisor_stats.py b/openstackclient/tests/compute/v2/test_hypervisor_stats.py index 39e303a..ca5ce29 100644 --- a/openstackclient/tests/compute/v2/test_hypervisor_stats.py +++ b/openstackclient/tests/compute/v2/test_hypervisor_stats.py @@ -33,7 +33,7 @@ class TestHypervisorStatsShow(TestHypervisorStats): super(TestHypervisorStatsShow, self).setUp() self.hypervisor_stats = \ - compute_fakes.FakehypervisorStats.create_one_hypervisor_stats() + compute_fakes.FakeHypervisorStats.create_one_hypervisor_stats() self.hypervisors_mock.statistics.return_value =\ self.hypervisor_stats diff --git a/openstackclient/tests/compute/v2/test_keypair.py b/openstackclient/tests/compute/v2/test_keypair.py new file mode 100644 index 0000000..a50a532 --- /dev/null +++ b/openstackclient/tests/compute/v2/test_keypair.py @@ -0,0 +1,260 @@ +# Copyright 2016 IBM +# +# 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 openstackclient.compute.v2 import keypair +from openstackclient.tests.compute.v2 import fakes as compute_fakes +from openstackclient.tests import utils as tests_utils + + +class TestKeypair(compute_fakes.TestComputev2): + + def setUp(self): + super(TestKeypair, self).setUp() + + # Get a shortcut to the KeypairManager Mock + self.keypairs_mock = self.app.client_manager.compute.keypairs + self.keypairs_mock.reset_mock() + + +class TestKeypairCreate(TestKeypair): + + keypair = compute_fakes.FakeKeypair.create_one_keypair() + + def setUp(self): + super(TestKeypairCreate, self).setUp() + + self.columns = ( + 'fingerprint', + 'name', + 'user_id' + ) + self.data = ( + self.keypair.fingerprint, + self.keypair.name, + self.keypair.user_id + ) + + # Get the command object to test + self.cmd = keypair.CreateKeypair(self.app, None) + + self.keypairs_mock.create.return_value = self.keypair + + def test_key_pair_create_no_options(self): + + arglist = [ + self.keypair.name, + ] + verifylist = [ + ('name', self.keypair.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.keypairs_mock.create.assert_called_with( + self.keypair.name, + public_key=None + ) + + self.assertEqual({}, columns) + self.assertEqual({}, data) + + def test_keypair_create_public_key(self): + # overwrite the setup one because we want to omit private_key + self.keypair = compute_fakes.FakeKeypair.create_one_keypair( + no_pri=True) + self.keypairs_mock.create.return_value = self.keypair + + self.data = ( + self.keypair.fingerprint, + self.keypair.name, + self.keypair.user_id + ) + + arglist = [ + '--public-key', self.keypair.public_key, + self.keypair.name, + ] + verifylist = [ + ('public_key', self.keypair.public_key), + ('name', self.keypair.name) + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + with mock.patch('io.open') as mock_open: + mock_open.return_value = mock.MagicMock() + m_file = mock_open.return_value.__enter__.return_value + m_file.read.return_value = 'dummy' + + columns, data = self.cmd.take_action(parsed_args) + + self.keypairs_mock.create.assert_called_with( + self.keypair.name, + public_key=self.keypair.public_key + ) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + +class TestKeypairDelete(TestKeypair): + + keypair = compute_fakes.FakeKeypair.create_one_keypair() + + def setUp(self): + super(TestKeypairDelete, self).setUp() + + self.keypairs_mock.get.return_value = self.keypair + self.keypairs_mock.delete.return_value = None + + self.cmd = keypair.DeleteKeypair(self.app, None) + + def test_keypair_delete(self): + arglist = [ + self.keypair.name + ] + verifylist = [ + ('name', self.keypair.name), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + ret = self.cmd.take_action(parsed_args) + + self.assertIsNone(ret) + self.keypairs_mock.delete.assert_called_with(self.keypair.name) + + +class TestKeypairList(TestKeypair): + + # Return value of self.keypairs_mock.list(). + keypairs = compute_fakes.FakeKeypair.create_keypairs(count=1) + + columns = ( + "Name", + "Fingerprint" + ) + + data = (( + keypairs[0].name, + keypairs[0].fingerprint + ), ) + + def setUp(self): + super(TestKeypairList, self).setUp() + + self.keypairs_mock.list.return_value = self.keypairs + + # Get the command object to test + self.cmd = keypair.ListKeypair(self.app, None) + + def test_keypair_list_no_options(self): + arglist = [] + verifylist = [ + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # In base command class Lister in cliff, abstract method take_action() + # returns a tuple containing the column names and an iterable + # containing the data to be listed. + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + + self.keypairs_mock.list.assert_called_with() + + self.assertEqual(self.columns, columns) + self.assertEqual(tuple(self.data), tuple(data)) + + +class TestKeypairShow(TestKeypair): + + keypair = compute_fakes.FakeKeypair.create_one_keypair() + + def setUp(self): + super(TestKeypairShow, self).setUp() + + self.keypairs_mock.get.return_value = self.keypair + + self.cmd = keypair.ShowKeypair(self.app, None) + + self.columns = ( + "fingerprint", + "name", + "user_id" + ) + + self.data = ( + self.keypair.fingerprint, + self.keypair.name, + self.keypair.user_id + ) + + def test_show_no_options(self): + + arglist = [] + verifylist = [] + + # Missing required args should boil here + self.assertRaises(tests_utils.ParserException, self.check_parser, + self.cmd, arglist, verifylist) + + def test_keypair_show(self): + # overwrite the setup one because we want to omit private_key + self.keypair = compute_fakes.FakeKeypair.create_one_keypair( + no_pri=True) + self.keypairs_mock.get.return_value = self.keypair + + self.data = ( + self.keypair.fingerprint, + self.keypair.name, + self.keypair.user_id + ) + + arglist = [ + self.keypair.name + ] + verifylist = [ + ('name', self.keypair.name) + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + def test_keypair_show_public(self): + + arglist = [ + '--public-key', + self.keypair.name + ] + verifylist = [ + ('public_key', True), + ('name', self.keypair.name) + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.assertEqual({}, columns) + self.assertEqual({}, data) diff --git a/openstackclient/tests/compute/v2/test_security_group.py b/openstackclient/tests/compute/v2/test_security_group.py deleted file mode 100644 index 01e27b8..0000000 --- a/openstackclient/tests/compute/v2/test_security_group.py +++ /dev/null @@ -1,127 +0,0 @@ -# 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 copy - -from openstackclient.compute.v2 import security_group -from openstackclient.tests.compute.v2 import fakes as compute_fakes -from openstackclient.tests import fakes -from openstackclient.tests.identity.v2_0 import fakes as identity_fakes - - -security_group_id = '11' -security_group_name = 'wide-open' -security_group_description = 'nothing but net' - -SECURITY_GROUP = { - 'id': security_group_id, - 'name': security_group_name, - 'description': security_group_description, - 'tenant_id': identity_fakes.project_id, -} - - -class FakeSecurityGroupResource(fakes.FakeResource): - - def get_keys(self): - return {'property': 'value'} - - -class TestSecurityGroup(compute_fakes.TestComputev2): - - def setUp(self): - super(TestSecurityGroup, self).setUp() - - # Get a shortcut compute client security_groups mock - self.secgroups_mock = self.app.client_manager.compute.security_groups - self.secgroups_mock.reset_mock() - - # Get a shortcut identity client projects mock - self.projects_mock = self.app.client_manager.identity.projects - self.projects_mock.reset_mock() - - -class TestSecurityGroupCreate(TestSecurityGroup): - - columns = ( - 'description', - 'id', - 'name', - 'tenant_id', - ) - data = ( - security_group_description, - security_group_id, - security_group_name, - identity_fakes.project_id, - ) - - def setUp(self): - super(TestSecurityGroupCreate, self).setUp() - - self.secgroups_mock.create.return_value = FakeSecurityGroupResource( - None, - copy.deepcopy(SECURITY_GROUP), - loaded=True, - ) - - # Get the command object to test - self.cmd = security_group.CreateSecurityGroup(self.app, None) - - def test_security_group_create_no_options(self): - arglist = [ - security_group_name, - ] - verifylist = [ - ('name', security_group_name), - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - # In base command class ShowOne in cliff, abstract method take_action() - # returns a two-part tuple with a tuple of column names and a tuple of - # data to be shown. - columns, data = self.cmd.take_action(parsed_args) - - # SecurityGroupManager.create(name, description) - self.secgroups_mock.create.assert_called_with( - security_group_name, - security_group_name, - ) - - self.assertEqual(self.columns, columns) - self.assertEqual(self.data, data) - - def test_security_group_create_description(self): - arglist = [ - security_group_name, - '--description', security_group_description, - ] - verifylist = [ - ('name', security_group_name), - ('description', security_group_description), - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - # In base command class ShowOne in cliff, abstract method take_action() - # returns a two-part tuple with a tuple of column names and a tuple of - # data to be shown. - columns, data = self.cmd.take_action(parsed_args) - - # SecurityGroupManager.create(name, description) - self.secgroups_mock.create.assert_called_with( - security_group_name, - security_group_description, - ) - - self.assertEqual(self.columns, columns) - self.assertEqual(self.data, data) diff --git a/openstackclient/tests/compute/v2/test_security_group_rule.py b/openstackclient/tests/compute/v2/test_security_group_rule.py deleted file mode 100644 index 9a8003f..0000000 --- a/openstackclient/tests/compute/v2/test_security_group_rule.py +++ /dev/null @@ -1,495 +0,0 @@ -# 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 copy - -from openstackclient.compute.v2 import security_group -from openstackclient.tests.compute.v2 import fakes as compute_fakes -from openstackclient.tests import fakes -from openstackclient.tests.identity.v2_0 import fakes as identity_fakes -from openstackclient.tests import utils - - -security_group_id = '11' -security_group_name = 'wide-open' -security_group_description = 'nothing but net' - -security_group_rule_id = '1' -security_group_rule_cidr = '0.0.0.0/0' - -SECURITY_GROUP_RULE = { - 'id': security_group_rule_id, - 'group': {}, - 'ip_protocol': 'tcp', - 'ip_range': {'cidr': security_group_rule_cidr}, - 'parent_group_id': security_group_id, - 'from_port': 0, - 'to_port': 0, -} - -SECURITY_GROUP_RULE_ICMP = { - 'id': security_group_rule_id, - 'group': {}, - 'ip_protocol': 'icmp', - 'ip_range': {'cidr': security_group_rule_cidr}, - 'parent_group_id': security_group_id, - 'from_port': -1, - 'to_port': -1, -} - -SECURITY_GROUP_RULE_REMOTE_GROUP = { - 'id': security_group_rule_id, - 'group': {"tenant_id": "14", "name": "default"}, - 'ip_protocol': 'tcp', - 'ip_range': {}, - 'parent_group_id': security_group_id, - 'from_port': 80, - 'to_port': 80, -} - -SECURITY_GROUP = { - 'id': security_group_id, - 'name': security_group_name, - 'description': security_group_description, - 'tenant_id': identity_fakes.project_id, - 'rules': [SECURITY_GROUP_RULE, - SECURITY_GROUP_RULE_ICMP, - SECURITY_GROUP_RULE_REMOTE_GROUP], -} - -security_group_2_id = '12' -security_group_2_name = 'he-shoots' -security_group_2_description = 'he scores' - -SECURITY_GROUP_2_RULE = { - 'id': '2', - 'group': {}, - 'ip_protocol': 'tcp', - 'ip_range': {}, - 'parent_group_id': security_group_2_id, - 'from_port': 80, - 'to_port': 80, -} - -SECURITY_GROUP_2 = { - 'id': security_group_2_id, - 'name': security_group_2_name, - 'description': security_group_2_description, - 'tenant_id': identity_fakes.project_id, - 'rules': [SECURITY_GROUP_2_RULE], -} - - -class FakeSecurityGroupRuleResource(fakes.FakeResource): - - def get_keys(self): - return {'property': 'value'} - - -class TestSecurityGroupRule(compute_fakes.TestComputev2): - - def setUp(self): - super(TestSecurityGroupRule, self).setUp() - - # Get a shortcut compute client security_groups mock - self.secgroups_mock = self.app.client_manager.compute.security_groups - self.secgroups_mock.reset_mock() - - # Get a shortcut compute client security_group_rules mock - self.sg_rules_mock = \ - self.app.client_manager.compute.security_group_rules - self.sg_rules_mock.reset_mock() - - -class TestSecurityGroupRuleCreate(TestSecurityGroupRule): - - columns = ( - 'id', - 'ip_protocol', - 'ip_range', - 'parent_group_id', - 'port_range', - 'remote_security_group', - ) - - def setUp(self): - super(TestSecurityGroupRuleCreate, self).setUp() - - self.secgroups_mock.get.return_value = FakeSecurityGroupRuleResource( - None, - copy.deepcopy(SECURITY_GROUP), - loaded=True, - ) - - # Get the command object to test - self.cmd = security_group.CreateSecurityGroupRule(self.app, None) - - def test_security_group_rule_create_no_options(self): - self.sg_rules_mock.create.return_value = FakeSecurityGroupRuleResource( - None, - copy.deepcopy(SECURITY_GROUP_RULE), - loaded=True, - ) - - arglist = [ - security_group_name, - ] - verifylist = [ - ('group', security_group_name), - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - # In base command class ShowOne in cliff, abstract method take_action() - # returns a two-part tuple with a tuple of column names and a tuple of - # data to be shown. - columns, data = self.cmd.take_action(parsed_args) - - # SecurityGroupManager.create(name, description) - self.sg_rules_mock.create.assert_called_with( - security_group_id, - 'tcp', - 0, - 0, - security_group_rule_cidr, - None, - ) - - self.assertEqual(self.columns, columns) - datalist = ( - security_group_rule_id, - 'tcp', - security_group_rule_cidr, - security_group_id, - '0:0', - '', - ) - self.assertEqual(datalist, data) - - def test_security_group_rule_create_ftp(self): - sg_rule = copy.deepcopy(SECURITY_GROUP_RULE) - sg_rule['from_port'] = 20 - sg_rule['to_port'] = 21 - self.sg_rules_mock.create.return_value = FakeSecurityGroupRuleResource( - None, - sg_rule, - loaded=True, - ) - - arglist = [ - security_group_name, - '--dst-port', '20:21', - ] - verifylist = [ - ('group', security_group_name), - ('dst_port', (20, 21)), - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - # In base command class ShowOne in cliff, abstract method take_action() - # returns a two-part tuple with a tuple of column names and a tuple of - # data to be shown. - columns, data = self.cmd.take_action(parsed_args) - - # SecurityGroupManager.create(name, description) - self.sg_rules_mock.create.assert_called_with( - security_group_id, - 'tcp', - 20, - 21, - security_group_rule_cidr, - None, - ) - - self.assertEqual(self.columns, columns) - datalist = ( - security_group_rule_id, - 'tcp', - security_group_rule_cidr, - security_group_id, - '20:21', - '', - ) - self.assertEqual(datalist, data) - - def test_security_group_rule_create_ssh(self): - sg_rule = copy.deepcopy(SECURITY_GROUP_RULE) - sg_rule['from_port'] = 22 - sg_rule['to_port'] = 22 - sg_rule['ip_range'] = {} - sg_rule['group'] = {'name': security_group_name} - self.sg_rules_mock.create.return_value = FakeSecurityGroupRuleResource( - None, - sg_rule, - loaded=True, - ) - - arglist = [ - security_group_name, - '--dst-port', '22', - '--src-group', security_group_id, - ] - verifylist = [ - ('group', security_group_name), - ('dst_port', (22, 22)), - ('src_group', security_group_id), - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - # In base command class ShowOne in cliff, abstract method take_action() - # returns a two-part tuple with a tuple of column names and a tuple of - # data to be shown. - columns, data = self.cmd.take_action(parsed_args) - - # SecurityGroupManager.create(name, description) - self.sg_rules_mock.create.assert_called_with( - security_group_id, - 'tcp', - 22, - 22, - security_group_rule_cidr, - security_group_id, - ) - - self.assertEqual(self.columns, columns) - datalist = ( - security_group_rule_id, - 'tcp', - '', - security_group_id, - '22:22', - security_group_name, - ) - self.assertEqual(datalist, data) - - def test_security_group_rule_create_udp(self): - sg_rule = copy.deepcopy(SECURITY_GROUP_RULE) - sg_rule['ip_protocol'] = 'udp' - self.sg_rules_mock.create.return_value = FakeSecurityGroupRuleResource( - None, - sg_rule, - loaded=True, - ) - - arglist = [ - security_group_name, - '--proto', 'udp', - ] - verifylist = [ - ('group', security_group_name), - ('proto', 'udp'), - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - # In base command class ShowOne in cliff, abstract method take_action() - # returns a two-part tuple with a tuple of column names and a tuple of - # data to be shown. - columns, data = self.cmd.take_action(parsed_args) - - # SecurityGroupManager.create(name, description) - self.sg_rules_mock.create.assert_called_with( - security_group_id, - 'udp', - 0, - 0, - security_group_rule_cidr, - None, - ) - - self.assertEqual(self.columns, columns) - datalist = ( - security_group_rule_id, - 'udp', - security_group_rule_cidr, - security_group_id, - '0:0', - '', - ) - self.assertEqual(datalist, data) - - def test_security_group_rule_create_icmp(self): - sg_rule_cidr = '10.0.2.0/24' - sg_rule = copy.deepcopy(SECURITY_GROUP_RULE_ICMP) - sg_rule['ip_range'] = {'cidr': sg_rule_cidr} - self.sg_rules_mock.create.return_value = FakeSecurityGroupRuleResource( - None, - sg_rule, - loaded=True, - ) - - arglist = [ - security_group_name, - '--proto', 'ICMP', - '--src-ip', sg_rule_cidr, - ] - verifylist = [ - ('group', security_group_name), - ('proto', 'ICMP'), - ('src_ip', sg_rule_cidr) - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - # In base command class ShowOne in cliff, abstract method take_action() - # returns a two-part tuple with a tuple of column names and a tuple of - # data to be shown. - columns, data = self.cmd.take_action(parsed_args) - - # SecurityGroupManager.create(name, description) - self.sg_rules_mock.create.assert_called_with( - security_group_id, - 'ICMP', - -1, - -1, - sg_rule_cidr, - None, - ) - - self.assertEqual(self.columns, columns) - datalist = ( - security_group_rule_id, - 'icmp', - sg_rule_cidr, - security_group_id, - '', - '', - ) - self.assertEqual(datalist, data) - - def test_security_group_rule_create_src_invalid(self): - arglist = [ - security_group_name, - '--proto', 'ICMP', - '--src-ip', security_group_rule_cidr, - '--src-group', security_group_id, - ] - - self.assertRaises(utils.ParserException, - self.check_parser, self.cmd, arglist, []) - - -class TestSecurityGroupRuleList(TestSecurityGroupRule): - - def setUp(self): - super(TestSecurityGroupRuleList, self).setUp() - - security_group_mock = FakeSecurityGroupRuleResource( - None, - copy.deepcopy(SECURITY_GROUP), - loaded=True, - ) - - security_group_2_mock = FakeSecurityGroupRuleResource( - None, - copy.deepcopy(SECURITY_GROUP_2), - loaded=True, - ) - - self.secgroups_mock.get.return_value = security_group_mock - self.secgroups_mock.list.return_value = [security_group_mock, - security_group_2_mock] - - # Get the command object to test - self.cmd = security_group.ListSecurityGroupRule(self.app, None) - - def test_security_group_rule_list(self): - - arglist = [ - security_group_name, - ] - verifylist = [ - ('group', security_group_name), - ] - - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - # In base command class Lister in cliff, abstract method take_action() - # returns a tuple containing the column names and an iterable - # containing the data to be listed. - columns, data = self.cmd.take_action(parsed_args) - - collist = ( - 'ID', - 'IP Protocol', - 'IP Range', - 'Port Range', - 'Remote Security Group', - ) - self.assertEqual(collist, columns) - datalist = (( - security_group_rule_id, - 'tcp', - security_group_rule_cidr, - '0:0', - '', - ), ( - security_group_rule_id, - 'icmp', - security_group_rule_cidr, - '', - '', - ), ( - security_group_rule_id, - 'tcp', - '', - '80:80', - 'default', - ),) - self.assertEqual(datalist, tuple(data)) - - def test_security_group_rule_list_no_group(self): - - parsed_args = self.check_parser(self.cmd, [], []) - - # In base command class Lister in cliff, abstract method take_action() - # returns a tuple containing the column names and an iterable - # containing the data to be listed. - columns, data = self.cmd.take_action(parsed_args) - - collist = ( - 'ID', - 'IP Protocol', - 'IP Range', - 'Port Range', - 'Remote Security Group', - 'Security Group', - ) - self.assertEqual(collist, columns) - datalist = (( - security_group_rule_id, - 'tcp', - security_group_rule_cidr, - '0:0', - '', - security_group_id, - ), ( - security_group_rule_id, - 'icmp', - security_group_rule_cidr, - '', - '', - security_group_id, - ), ( - security_group_rule_id, - 'tcp', - '', - '80:80', - 'default', - security_group_id, - ), ( - '2', - 'tcp', - '', - '80:80', - '', - security_group_2_id, - ),) - self.assertEqual(datalist, tuple(data)) diff --git a/openstackclient/tests/compute/v2/test_server.py b/openstackclient/tests/compute/v2/test_server.py index aa8d733..7d184b3 100644 --- a/openstackclient/tests/compute/v2/test_server.py +++ b/openstackclient/tests/compute/v2/test_server.py @@ -89,6 +89,7 @@ class TestServer(compute_fakes.TestComputev2): class TestServerCreate(TestServer): columns = ( + 'OS-EXT-STS:power_state', 'addresses', 'flavor', 'id', @@ -100,6 +101,8 @@ class TestServerCreate(TestServer): def datalist(self): datalist = ( + server._format_servers_list_power_state( + getattr(self.new_server, 'OS-EXT-STS:power_state')), '', self.flavor.name + ' (' + self.new_server.flavor.get('id') + ')', self.new_server.id, @@ -146,7 +149,6 @@ class TestServerCreate(TestServer): ('server_name', self.new_server.name), ] - # Missing required args should bail here self.assertRaises(utils.ParserException, self.check_parser, self.cmd, arglist, verifylist) @@ -592,6 +594,64 @@ class TestServerImageCreate(TestServer): self.assertEqual(self.columns, columns) self.assertEqual(self.datalist(), data) + @mock.patch.object(common_utils, 'wait_for_status', return_value=False) + def test_server_create_image_with_wait_fails(self, mock_wait_for_status): + arglist = [ + '--wait', + self.server.id, + ] + verifylist = [ + ('wait', True), + ('server', self.server.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.assertRaises(SystemExit, self.cmd.take_action, parsed_args) + + mock_wait_for_status.assert_called_once_with( + self.images_mock.get, + self.image.id, + callback=server._show_progress + ) + + # ServerManager.create_image(server, image_name, metadata=) + self.servers_mock.create_image.assert_called_with( + self.servers_mock.get.return_value, + self.server.name, + ) + + @mock.patch.object(common_utils, 'wait_for_status', return_value=True) + def test_server_create_image_with_wait_ok(self, mock_wait_for_status): + arglist = [ + '--wait', + self.server.id, + ] + verifylist = [ + ('wait', True), + ('server', self.server.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # In base command class ShowOne in cliff, abstract method take_action() + # returns a two-part tuple with a tuple of column names and a tuple of + # data to be shown. + columns, data = self.cmd.take_action(parsed_args) + + # ServerManager.create_image(server, image_name, metadata=) + self.servers_mock.create_image.assert_called_with( + self.servers_mock.get.return_value, + self.server.name, + ) + + mock_wait_for_status.assert_called_once_with( + self.images_mock.get, + self.image.id, + callback=server._show_progress + ) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.datalist(), data) + class TestServerList(TestServer): @@ -872,6 +932,58 @@ class TestServerRebuild(TestServer): self.cimages_mock.get.assert_called_with(self.image.id) self.server.rebuild.assert_called_with(self.image, password) + @mock.patch.object(common_utils, 'wait_for_status', return_value=True) + def test_rebuild_with_wait_ok(self, mock_wait_for_status): + arglist = [ + '--wait', + self.server.id, + ] + verifylist = [ + ('wait', True), + ('server', self.server.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # Get the command object to test. + self.cmd.take_action(parsed_args) + + # kwargs = dict(success_status=['active', 'verify_resize'],) + + mock_wait_for_status.assert_called_once_with( + self.servers_mock.get, + self.server.id, + callback=server._show_progress, + # **kwargs + ) + + self.servers_mock.get.assert_called_with(self.server.id) + self.cimages_mock.get.assert_called_with(self.image.id) + self.server.rebuild.assert_called_with(self.image, None) + + @mock.patch.object(common_utils, 'wait_for_status', return_value=False) + def test_rebuild_with_wait_fails(self, mock_wait_for_status): + arglist = [ + '--wait', + self.server.id, + ] + verifylist = [ + ('wait', True), + ('server', self.server.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.assertRaises(SystemExit, self.cmd.take_action, parsed_args) + + mock_wait_for_status.assert_called_once_with( + self.servers_mock.get, + self.server.id, + callback=server._show_progress + ) + + self.servers_mock.get.assert_called_with(self.server.id) + self.cimages_mock.get.assert_called_with(self.image.id) + self.server.rebuild.assert_called_with(self.image, None) + class TestServerResize(TestServer): @@ -982,6 +1094,104 @@ class TestServerResize(TestServer): self.servers_mock.revert_resize.assert_called_with(self.server) self.assertIsNone(result) + @mock.patch.object(common_utils, 'wait_for_status', return_value=True) + def test_server_resize_with_wait_ok(self, mock_wait_for_status): + + arglist = [ + '--flavor', self.flavors_get_return_value.id, + '--wait', + self.server.id, + ] + + verifylist = [ + ('flavor', self.flavors_get_return_value.id), + ('confirm', False), + ('revert', False), + ('wait', True), + ('server', self.server.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + self.servers_mock.get.assert_called_with( + self.server.id, + ) + + kwargs = dict(success_status=['active', 'verify_resize'],) + + mock_wait_for_status.assert_called_once_with( + self.servers_mock.get, + self.server.id, + callback=server._show_progress, + **kwargs + ) + + self.servers_mock.resize.assert_called_with( + self.server, + self.flavors_get_return_value + ) + self.assertNotCalled(self.servers_mock.confirm_resize) + self.assertNotCalled(self.servers_mock.revert_resize) + + @mock.patch.object(common_utils, 'wait_for_status', return_value=False) + def test_server_resize_with_wait_fails(self, mock_wait_for_status): + + arglist = [ + '--flavor', self.flavors_get_return_value.id, + '--wait', + self.server.id, + ] + + verifylist = [ + ('flavor', self.flavors_get_return_value.id), + ('confirm', False), + ('revert', False), + ('wait', True), + ('server', self.server.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.assertRaises(SystemExit, self.cmd.take_action, parsed_args) + + self.servers_mock.get.assert_called_with( + self.server.id, + ) + + kwargs = dict(success_status=['active', 'verify_resize'],) + + mock_wait_for_status.assert_called_once_with( + self.servers_mock.get, + self.server.id, + callback=server._show_progress, + **kwargs + ) + + self.servers_mock.resize.assert_called_with( + self.server, + self.flavors_get_return_value + ) + + +class TestServerRestore(TestServer): + + def setUp(self): + super(TestServerRestore, self).setUp() + + # Get the command object to test + self.cmd = server.RestoreServer(self.app, None) + + # Set methods to be tested. + self.methods = { + 'restore': None, + } + + def test_server_restore_one_server(self): + self.run_method_with_servers('restore', 1) + + def test_server_restore_multi_servers(self): + self.run_method_with_servers('restore', 3) + class TestServerResume(TestServer): @@ -1023,6 +1233,101 @@ class TestServerShelve(TestServer): self.run_method_with_servers('shelve', 3) +class TestServerShow(TestServer): + + def setUp(self): + super(TestServerShow, self).setUp() + + self.image = image_fakes.FakeImage.create_one_image() + self.flavor = compute_fakes.FakeFlavor.create_one_flavor() + server_info = { + 'image': {'id': self.image.id}, + 'flavor': {'id': self.flavor.id}, + 'tenant_id': 'tenant-id-xxx', + 'networks': {'public': ['10.20.30.40', '2001:db8::f']}, + } + # Fake the server.diagnostics() method. The return value contains http + # response and data. The data is a dict. Sincce this method itself is + # faked, we don't need to fake everything of the return value exactly. + resp = mock.Mock() + resp.status_code = 200 + server_method = { + 'diagnostics': (resp, {'test': 'test'}), + } + self.server = compute_fakes.FakeServer.create_one_server( + attrs=server_info, methods=server_method) + + # This is the return value for utils.find_resource() + self.servers_mock.get.return_value = self.server + self.cimages_mock.get.return_value = self.image + self.flavors_mock.get.return_value = self.flavor + + # Get the command object to test + self.cmd = server.ShowServer(self.app, None) + + self.columns = ( + 'OS-EXT-STS:power_state', + 'addresses', + 'flavor', + 'id', + 'image', + 'name', + 'networks', + 'project_id', + 'properties', + ) + + self.data = ( + 'Running', + 'public=10.20.30.40, 2001:db8::f', + self.flavor.name + " (" + self.flavor.id + ")", + self.server.id, + self.image.name + " (" + self.image.id + ")", + self.server.name, + {'public': ['10.20.30.40', '2001:db8::f']}, + 'tenant-id-xxx', + '', + ) + + def test_show_no_options(self): + arglist = [] + verifylist = [] + + self.assertRaises(utils.ParserException, self.check_parser, + self.cmd, arglist, verifylist) + + def test_show(self): + arglist = [ + self.server.name, + ] + verifylist = [ + ('diagnostics', False), + ('server', self.server.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + def test_show_diagnostics(self): + arglist = [ + '--diagnostics', + self.server.name, + ] + verifylist = [ + ('diagnostics', True), + ('server', self.server.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.assertEqual(('test',), columns) + self.assertEqual(('test',), data) + + class TestServerStart(TestServer): def setUp(self): @@ -1241,14 +1546,12 @@ class TestServerGeneral(TestServer): @mock.patch('openstackclient.common.utils.find_resource') def test_prep_server_detail(self, find_resource): # Setup mock method return value. utils.find_resource() will be called - # twice in _prep_server_detail(): - # - The first time, return image info. - # - The second time, return flavor info. + # three times in _prep_server_detail(): + # - The first time, return server info. + # - The second time, return image info. + # - The third time, return flavor info. _image = image_fakes.FakeImage.create_one_image() _flavor = compute_fakes.FakeFlavor.create_one_flavor() - find_resource.side_effect = [_image, _flavor] - - # compute_client.servers.get() will be called once, return server info. server_info = { 'image': {u'id': _image.id}, 'flavor': {u'id': _flavor.id}, @@ -1257,7 +1560,7 @@ class TestServerGeneral(TestServer): 'links': u'http://xxx.yyy.com', } _server = compute_fakes.FakeServer.create_one_server(attrs=server_info) - self.servers_mock.get.return_value = _server + find_resource.side_effect = [_server, _image, _flavor] # Prepare result data. info = { @@ -1268,6 +1571,8 @@ class TestServerGeneral(TestServer): 'image': u'%s (%s)' % (_image.name, _image.id), 'project_id': u'tenant-id-xxx', 'properties': '', + 'OS-EXT-STS:power_state': server._format_servers_list_power_state( + getattr(_server, 'OS-EXT-STS:power_state')), } # Call _prep_server_detail(). diff --git a/openstackclient/tests/compute/v2/test_server_group.py b/openstackclient/tests/compute/v2/test_server_group.py new file mode 100644 index 0000000..70ff23f --- /dev/null +++ b/openstackclient/tests/compute/v2/test_server_group.py @@ -0,0 +1,283 @@ +# Copyright 2016 Huawei, Inc. 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 openstackclient.common import exceptions +from openstackclient.common import utils +from openstackclient.compute.v2 import server_group +from openstackclient.tests.compute.v2 import fakes as compute_fakes +from openstackclient.tests import utils as tests_utils + + +class TestServerGroup(compute_fakes.TestComputev2): + + fake_server_group = compute_fakes.FakeServerGroup.create_one_server_group() + + columns = ( + 'id', + 'members', + 'name', + 'policies', + 'project_id', + 'user_id', + ) + + data = ( + fake_server_group.id, + utils.format_list(fake_server_group.members), + fake_server_group.name, + utils.format_list(fake_server_group.policies), + fake_server_group.project_id, + fake_server_group.user_id, + ) + + def setUp(self): + super(TestServerGroup, self).setUp() + + # Get a shortcut to the ServerGroupsManager Mock + self.server_groups_mock = self.app.client_manager.compute.server_groups + self.server_groups_mock.reset_mock() + + +class TestServerGroupCreate(TestServerGroup): + + def setUp(self): + super(TestServerGroupCreate, self).setUp() + + self.server_groups_mock.create.return_value = self.fake_server_group + self.cmd = server_group.CreateServerGroup(self.app, None) + + def test_server_group_create(self): + arglist = [ + '--policy', 'affinity', + 'affinity_group', + ] + verifylist = [ + ('policy', ['affinity']), + ('name', 'affinity_group'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + self.server_groups_mock.create.assert_called_once_with( + name=parsed_args.name, + policies=parsed_args.policy, + ) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + def test_server_group_create_with_multiple_policies(self): + arglist = [ + '--policy', 'affinity', + '--policy', 'soft-affinity', + 'affinity_group', + ] + verifylist = [ + ('policy', ['affinity', 'soft-affinity']), + ('name', 'affinity_group'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + self.server_groups_mock.create.assert_called_once_with( + name=parsed_args.name, + policies=parsed_args.policy, + ) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + def test_server_group_create_no_policy(self): + arglist = [ + 'affinity_group', + ] + verifylist = None + self.assertRaises(tests_utils.ParserException, + self.check_parser, + self.cmd, + arglist, + verifylist) + + +class TestServerGroupDelete(TestServerGroup): + + def setUp(self): + super(TestServerGroupDelete, self).setUp() + + self.server_groups_mock.get.return_value = self.fake_server_group + self.cmd = server_group.DeleteServerGroup(self.app, None) + + def test_server_group_delete(self): + arglist = [ + 'affinity_group', + ] + verifylist = [ + ('server_group', ['affinity_group']), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + self.server_groups_mock.get.assert_called_once_with('affinity_group') + self.server_groups_mock.delete.assert_called_once_with( + self.fake_server_group.id + ) + self.assertIsNone(result) + + def test_server_group_multiple_delete(self): + arglist = [ + 'affinity_group', + 'anti_affinity_group' + ] + verifylist = [ + ('server_group', ['affinity_group', 'anti_affinity_group']), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + self.server_groups_mock.get.assert_any_call('affinity_group') + self.server_groups_mock.get.assert_any_call('anti_affinity_group') + self.server_groups_mock.delete.assert_called_with( + self.fake_server_group.id + ) + self.assertEqual(2, self.server_groups_mock.get.call_count) + self.assertEqual(2, self.server_groups_mock.delete.call_count) + self.assertIsNone(result) + + def test_server_group_delete_no_input(self): + arglist = [] + verifylist = None + self.assertRaises(tests_utils.ParserException, + self.check_parser, + self.cmd, + arglist, + verifylist) + + def test_server_group_multiple_delete_with_exception(self): + arglist = [ + 'affinity_group', + 'anti_affinity_group' + ] + verifylist = [ + ('server_group', ['affinity_group', 'anti_affinity_group']), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + find_mock_result = [self.fake_server_group, 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 server groups failed to delete.', + str(e)) + + find_mock.assert_any_call(self.server_groups_mock, + 'affinity_group') + find_mock.assert_any_call(self.server_groups_mock, + 'anti_affinity_group') + + self.assertEqual(2, find_mock.call_count) + self.server_groups_mock.delete.assert_called_once_with( + self.fake_server_group.id + ) + + +class TestServerGroupList(TestServerGroup): + + list_columns = ( + 'ID', + 'Name', + 'Policies', + ) + + list_columns_long = ( + 'ID', + 'Name', + 'Policies', + 'Members', + 'Project Id', + 'User Id', + ) + + list_data = (( + TestServerGroup.fake_server_group.id, + TestServerGroup.fake_server_group.name, + utils.format_list(TestServerGroup.fake_server_group.policies), + ),) + + list_data_long = (( + TestServerGroup.fake_server_group.id, + TestServerGroup.fake_server_group.name, + utils.format_list(TestServerGroup.fake_server_group.policies), + utils.format_list(TestServerGroup.fake_server_group.members), + TestServerGroup.fake_server_group.project_id, + TestServerGroup.fake_server_group.user_id, + ),) + + def setUp(self): + super(TestServerGroupList, self).setUp() + + self.server_groups_mock.list.return_value = [self.fake_server_group] + self.cmd = server_group.ListServerGroup(self.app, None) + + def test_server_group_list(self): + arglist = [] + verifylist = [ + ('all_projects', False), + ('long', False), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + self.server_groups_mock.list.assert_called_once_with(False) + + self.assertEqual(self.list_columns, columns) + self.assertEqual(self.list_data, tuple(data)) + + def test_server_group_list_with_all_projects_and_long(self): + arglist = [ + '--all-projects', + '--long', + ] + verifylist = [ + ('all_projects', True), + ('long', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + self.server_groups_mock.list.assert_called_once_with(True) + + self.assertEqual(self.list_columns_long, columns) + self.assertEqual(self.list_data_long, tuple(data)) + + +class TestServerGroupShow(TestServerGroup): + + def setUp(self): + super(TestServerGroupShow, self).setUp() + + self.server_groups_mock.get.return_value = self.fake_server_group + self.cmd = server_group.ShowServerGroup(self.app, None) + + def test_server_group_show(self): + arglist = [ + 'affinity_group', + ] + verifylist = [ + ('server_group', 'affinity_group'), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) diff --git a/openstackclient/tests/compute/v2/test_service.py b/openstackclient/tests/compute/v2/test_service.py index 2f8b2e7..7a5a840 100644 --- a/openstackclient/tests/compute/v2/test_service.py +++ b/openstackclient/tests/compute/v2/test_service.py @@ -13,11 +13,10 @@ # under the License. # -import copy +import mock from openstackclient.compute.v2 import service from openstackclient.tests.compute.v2 import fakes as compute_fakes -from openstackclient.tests import fakes class TestService(compute_fakes.TestComputev2): @@ -35,6 +34,8 @@ class TestServiceDelete(TestService): def setUp(self): super(TestServiceDelete, self).setUp() + self.service = compute_fakes.FakeService.create_one_service() + self.service_mock.delete.return_value = None # Get the command object to test @@ -42,17 +43,17 @@ class TestServiceDelete(TestService): def test_service_delete_no_options(self): arglist = [ - compute_fakes.service_binary, + self.service.binary, ] verifylist = [ - ('service', compute_fakes.service_binary), + ('service', self.service.binary), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) self.service_mock.delete.assert_called_with( - compute_fakes.service_binary, + self.service.binary, ) self.assertIsNone(result) @@ -62,94 +63,189 @@ class TestServiceList(TestService): def setUp(self): super(TestServiceList, self).setUp() - self.service_mock.list.return_value = [fakes.FakeResource( - None, - copy.deepcopy(compute_fakes.SERVICE), - loaded=True, - )] + self.service = compute_fakes.FakeService.create_one_service() + + self.service_mock.list.return_value = [self.service] # Get the command object to test self.cmd = service.ListService(self.app, None) def test_service_list(self): arglist = [ - '--host', compute_fakes.service_host, - '--service', compute_fakes.service_binary, + '--host', self.service.host, + '--service', self.service.binary, ] verifylist = [ - ('host', compute_fakes.service_host), - ('service', compute_fakes.service_binary), + ('host', self.service.host), + ('service', self.service.binary), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) # In base command class Lister in cliff, abstract method take_action() # returns a tuple containing the column names and an iterable # containing the data to be listed. - self.cmd.take_action(parsed_args) + columns, data = self.cmd.take_action(parsed_args) self.service_mock.list.assert_called_with( - compute_fakes.service_host, - compute_fakes.service_binary, + self.service.host, + self.service.binary, ) + self.assertNotIn("Disabled Reason", columns) + self.assertNotIn(self.service.disabled_reason, list(data)[0]) + + def test_service_list_with_long_option(self): + arglist = [ + '--host', self.service.host, + '--service', self.service.binary, + '--long' + ] + verifylist = [ + ('host', self.service.host), + ('service', self.service.binary), + ('long', True) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # In base command class Lister in cliff, abstract method take_action() + # returns a tuple containing the column names and an iterable + # containing the data to be listed. + columns, data = self.cmd.take_action(parsed_args) + + self.assertIn("Disabled Reason", columns) + self.assertIn(self.service.disabled_reason, list(data)[0]) + class TestServiceSet(TestService): def setUp(self): super(TestServiceSet, self).setUp() - self.service_mock.enable.return_value = [fakes.FakeResource( - None, - copy.deepcopy(compute_fakes.SERVICE), - loaded=True, - )] + self.service = compute_fakes.FakeService.create_one_service() - self.service_mock.disable.return_value = [fakes.FakeResource( - None, - copy.deepcopy(compute_fakes.SERVICE), - loaded=True, - )] + self.service_mock.enable.return_value = self.service + self.service_mock.disable.return_value = self.service self.cmd = service.SetService(self.app, None) def test_service_set_enable(self): arglist = [ - compute_fakes.service_host, - compute_fakes.service_binary, '--enable', + self.service.host, + self.service.binary, ] verifylist = [ - ('host', compute_fakes.service_host), - ('service', compute_fakes.service_binary), ('enabled', True), + ('host', self.service.host), + ('service', self.service.binary), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) self.service_mock.enable.assert_called_with( - compute_fakes.service_host, - compute_fakes.service_binary, + self.service.host, + self.service.binary ) self.assertIsNone(result) def test_service_set_disable(self): arglist = [ - compute_fakes.service_host, - compute_fakes.service_binary, '--disable', + self.service.host, + self.service.binary, ] verifylist = [ - ('host', compute_fakes.service_host), - ('service', compute_fakes.service_binary), ('enabled', False), + ('host', self.service.host), + ('service', self.service.binary), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) self.service_mock.disable.assert_called_with( - compute_fakes.service_host, - compute_fakes.service_binary, + self.service.host, + self.service.binary + ) + self.assertIsNone(result) + + def test_service_set_disable_with_reason(self): + reason = 'earthquake' + arglist = [ + '--disable', + '--disable-reason', reason, + self.service.host, + self.service.binary, + ] + verifylist = [ + ('enabled', False), + ('disable_reason', reason), + ('host', self.service.host), + ('service', self.service.binary), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.service_mock.disable_log_reason.assert_called_with( + self.service.host, + self.service.binary, + reason + ) + self.assertIsNone(result) + + def test_service_set_only_with_disable_reason(self): + reason = 'earthquake' + arglist = [ + '--disable-reason', reason, + self.service.host, + self.service.binary, + ] + verifylist = [ + ('enabled', True), + ('disable_reason', reason), + ('host', self.service.host), + ('service', self.service.binary), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + with mock.patch.object(self.cmd.log, 'info') as mock_log: + result = self.cmd.take_action(parsed_args) + + msg = "argument --disable-reason has been ignored" + mock_log.assert_called_once_with(msg) + + self.service_mock.enable.assert_called_with( + self.service.host, + self.service.binary + ) + self.assertIsNone(result) + + def test_service_set_enable_with_disable_reason(self): + reason = 'earthquake' + arglist = [ + '--enable', + '--disable-reason', reason, + self.service.host, + self.service.binary, + ] + verifylist = [ + ('enabled', True), + ('disable_reason', reason), + ('host', self.service.host), + ('service', self.service.binary), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + with mock.patch.object(self.cmd.log, 'info') as mock_log: + result = self.cmd.take_action(parsed_args) + + msg = "argument --disable-reason has been ignored" + mock_log.assert_called_once_with(msg) + + self.service_mock.enable.assert_called_with( + self.service.host, + self.service.binary ) self.assertIsNone(result) diff --git a/openstackclient/tests/fakes.py b/openstackclient/tests/fakes.py index 9fdcc7e..229b465 100644 --- a/openstackclient/tests/fakes.py +++ b/openstackclient/tests/fakes.py @@ -142,7 +142,7 @@ class FakeModule(object): class FakeResource(object): - def __init__(self, manager=None, info={}, loaded=False, methods={}): + def __init__(self, manager=None, info=None, loaded=False, methods=None): """Set attributes and methods for a resource. :param manager: @@ -154,6 +154,9 @@ class FakeResource(object): :param Dictionary methods: A dictionary with all methods """ + info = info or {} + methods = methods or {} + self.__name__ = type(self).__name__ self.manager = manager self._info = info @@ -183,12 +186,22 @@ class FakeResource(object): info = ", ".join("%s=%s" % (k, getattr(self, k)) for k in reprkeys) return "<%s %s>" % (self.__class__.__name__, info) + def keys(self): + return self._info.keys() + + @property + def info(self): + return self._info + class FakeResponse(requests.Response): - def __init__(self, headers={}, status_code=200, data=None, encoding=None): + def __init__(self, headers=None, status_code=200, + data=None, encoding=None): super(FakeResponse, self).__init__() + headers = headers or {} + self.status_code = status_code self.headers.update(headers) diff --git a/openstackclient/tests/identity/v2_0/test_catalog.py b/openstackclient/tests/identity/v2_0/test_catalog.py index 1e27bb3..d9ae6a8 100644 --- a/openstackclient/tests/identity/v2_0/test_catalog.py +++ b/openstackclient/tests/identity/v2_0/test_catalog.py @@ -36,6 +36,12 @@ class TestCatalog(utils.TestCommand): 'internalURL': 'https://internal.two.example.com', 'adminURL': 'https://admin.two.example.com', }, + { + 'region': None, + 'publicURL': 'https://public.none.example.com', + 'internalURL': 'https://internal.none.example.com', + 'adminURL': 'https://admin.none.example.com', + }, ], } @@ -87,7 +93,10 @@ class TestCatalogList(TestCatalog): 'adminURL: https://admin.one.example.com\n' 'two\n publicURL: https://public.two.example.com\n ' 'internalURL: https://internal.two.example.com\n ' - 'adminURL: https://admin.two.example.com\n', + 'adminURL: https://admin.two.example.com\n' + '<none>\n publicURL: https://public.none.example.com\n ' + 'internalURL: https://internal.none.example.com\n ' + 'adminURL: https://admin.none.example.com\n', ), ) self.assertEqual(datalist, tuple(data)) @@ -164,7 +173,10 @@ class TestCatalogShow(TestCatalog): 'adminURL: https://admin.one.example.com\n' 'two\n publicURL: https://public.two.example.com\n ' 'internalURL: https://internal.two.example.com\n ' - 'adminURL: https://admin.two.example.com\n', + 'adminURL: https://admin.two.example.com\n' + '<none>\n publicURL: https://public.none.example.com\n ' + 'internalURL: https://internal.none.example.com\n ' + 'adminURL: https://admin.none.example.com\n', 'qwertyuiop', 'supernova', 'compute', diff --git a/openstackclient/tests/identity/v2_0/test_endpoint.py b/openstackclient/tests/identity/v2_0/test_endpoint.py index 088fdcd..45ece45 100644 --- a/openstackclient/tests/identity/v2_0/test_endpoint.py +++ b/openstackclient/tests/identity/v2_0/test_endpoint.py @@ -132,11 +132,12 @@ class TestEndpointDelete(TestEndpoint): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.cmd.take_action(parsed_args) + result = self.cmd.take_action(parsed_args) self.endpoints_mock.delete.assert_called_with( identity_fakes.endpoint_id, ) + self.assertIsNone(result) class TestEndpointList(TestEndpoint): diff --git a/openstackclient/tests/identity/v2_0/test_project.py b/openstackclient/tests/identity/v2_0/test_project.py index 669c3ea..38684aa 100644 --- a/openstackclient/tests/identity/v2_0/test_project.py +++ b/openstackclient/tests/identity/v2_0/test_project.py @@ -307,12 +307,12 @@ class TestProjectDelete(TestProject): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - result = self.cmd.run(parsed_args) - self.assertEqual(0, result) + result = self.cmd.take_action(parsed_args) self.projects_mock.delete.assert_called_with( identity_fakes.project_id, ) + self.assertIsNone(result) class TestProjectList(TestProject): @@ -406,8 +406,9 @@ class TestProjectSet(TestProject): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - result = self.cmd.run(parsed_args) - self.assertEqual(0, result) + result = self.cmd.take_action(parsed_args) + + self.assertIsNone(result) def test_project_set_name(self): arglist = [ @@ -422,8 +423,7 @@ class TestProjectSet(TestProject): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - result = self.cmd.run(parsed_args) - self.assertEqual(0, result) + result = self.cmd.take_action(parsed_args) # Set expected values kwargs = { @@ -435,6 +435,7 @@ class TestProjectSet(TestProject): identity_fakes.project_id, **kwargs ) + self.assertIsNone(result) def test_project_set_description(self): arglist = [ @@ -449,8 +450,7 @@ class TestProjectSet(TestProject): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - result = self.cmd.run(parsed_args) - self.assertEqual(0, result) + result = self.cmd.take_action(parsed_args) # Set expected values kwargs = { @@ -462,6 +462,7 @@ class TestProjectSet(TestProject): identity_fakes.project_id, **kwargs ) + self.assertIsNone(result) def test_project_set_enable(self): arglist = [ @@ -475,8 +476,7 @@ class TestProjectSet(TestProject): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - result = self.cmd.run(parsed_args) - self.assertEqual(0, result) + result = self.cmd.take_action(parsed_args) # Set expected values kwargs = { @@ -488,6 +488,7 @@ class TestProjectSet(TestProject): identity_fakes.project_id, **kwargs ) + self.assertIsNone(result) def test_project_set_disable(self): arglist = [ @@ -501,8 +502,7 @@ class TestProjectSet(TestProject): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - result = self.cmd.run(parsed_args) - self.assertEqual(0, result) + result = self.cmd.take_action(parsed_args) # Set expected values kwargs = { @@ -514,6 +514,7 @@ class TestProjectSet(TestProject): identity_fakes.project_id, **kwargs ) + self.assertIsNone(result) def test_project_set_property(self): arglist = [ @@ -527,8 +528,7 @@ class TestProjectSet(TestProject): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - result = self.cmd.run(parsed_args) - self.assertEqual(0, result) + result = self.cmd.take_action(parsed_args) # Set expected values kwargs = { @@ -542,6 +542,7 @@ class TestProjectSet(TestProject): identity_fakes.project_id, **kwargs ) + self.assertIsNone(result) class TestProjectShow(TestProject): @@ -612,10 +613,9 @@ class TestProjectUnset(TestProject): verifylist = [ ('property', ['fee', 'fo']), ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.cmd.run(parsed_args) + result = self.cmd.take_action(parsed_args) # Set expected values kwargs = { 'description': identity_fakes.project_description, @@ -630,3 +630,4 @@ class TestProjectUnset(TestProject): identity_fakes.project_id, **kwargs ) + self.assertIsNone(result) diff --git a/openstackclient/tests/identity/v2_0/test_role.py b/openstackclient/tests/identity/v2_0/test_role.py index 03b7f92..3c4b79a 100644 --- a/openstackclient/tests/identity/v2_0/test_role.py +++ b/openstackclient/tests/identity/v2_0/test_role.py @@ -240,11 +240,12 @@ class TestRoleDelete(TestRole): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.cmd.take_action(parsed_args) + result = self.cmd.take_action(parsed_args) self.roles_mock.delete.assert_called_with( identity_fakes.role_id, ) + self.assertIsNone(result) class TestRoleList(TestRole): @@ -459,7 +460,7 @@ class TestRoleRemove(TestRole): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.cmd.take_action(parsed_args) + result = self.cmd.take_action(parsed_args) # RoleManager.remove_user_role(user, role, tenant=None) self.roles_mock.remove_user_role.assert_called_with( @@ -467,6 +468,7 @@ class TestRoleRemove(TestRole): identity_fakes.role_id, identity_fakes.project_id, ) + self.assertIsNone(result) class TestRoleShow(TestRole): diff --git a/openstackclient/tests/identity/v2_0/test_service.py b/openstackclient/tests/identity/v2_0/test_service.py index 606b143..dc0fbcd 100644 --- a/openstackclient/tests/identity/v2_0/test_service.py +++ b/openstackclient/tests/identity/v2_0/test_service.py @@ -194,11 +194,12 @@ class TestServiceDelete(TestService): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.cmd.take_action(parsed_args) + result = self.cmd.take_action(parsed_args) self.services_mock.delete.assert_called_with( identity_fakes.service_id, ) + self.assertIsNone(result) class TestServiceList(TestService): diff --git a/openstackclient/tests/identity/v2_0/test_token.py b/openstackclient/tests/identity/v2_0/test_token.py index c90477f..613139d 100644 --- a/openstackclient/tests/identity/v2_0/test_token.py +++ b/openstackclient/tests/identity/v2_0/test_token.py @@ -99,6 +99,7 @@ class TestTokenRevoke(TestToken): verifylist = [('token', self.TOKEN)] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.cmd.take_action(parsed_args) + result = self.cmd.take_action(parsed_args) self.tokens_mock.delete.assert_called_with(self.TOKEN) + self.assertIsNone(result) diff --git a/openstackclient/tests/identity/v2_0/test_user.py b/openstackclient/tests/identity/v2_0/test_user.py index a7332e6..921e215 100644 --- a/openstackclient/tests/identity/v2_0/test_user.py +++ b/openstackclient/tests/identity/v2_0/test_user.py @@ -414,11 +414,12 @@ class TestUserDelete(TestUser): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.cmd.take_action(parsed_args) + result = self.cmd.take_action(parsed_args) self.users_mock.delete.assert_called_with( identity_fakes.user_id, ) + self.assertIsNone(result) class TestUserList(TestUser): @@ -558,8 +559,9 @@ class TestUserSet(TestUser): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - result = self.cmd.run(parsed_args) - self.assertEqual(0, result) + result = self.cmd.take_action(parsed_args) + + self.assertIsNone(result) def test_user_set_name(self): arglist = [ @@ -577,7 +579,7 @@ class TestUserSet(TestUser): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.cmd.take_action(parsed_args) + result = self.cmd.take_action(parsed_args) # Set expected values kwargs = { @@ -589,6 +591,7 @@ class TestUserSet(TestUser): identity_fakes.user_id, **kwargs ) + self.assertIsNone(result) def test_user_set_password(self): arglist = [ @@ -607,13 +610,14 @@ class TestUserSet(TestUser): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.cmd.take_action(parsed_args) + result = self.cmd.take_action(parsed_args) # UserManager.update_password(user, password) self.users_mock.update_password.assert_called_with( identity_fakes.user_id, 'secret', ) + self.assertIsNone(result) def test_user_set_password_prompt(self): arglist = [ @@ -635,13 +639,14 @@ class TestUserSet(TestUser): mocker = mock.Mock() mocker.return_value = 'abc123' with mock.patch("openstackclient.common.utils.get_password", mocker): - self.cmd.take_action(parsed_args) + result = self.cmd.take_action(parsed_args) # UserManager.update_password(user, password) self.users_mock.update_password.assert_called_with( identity_fakes.user_id, 'abc123', ) + self.assertIsNone(result) def test_user_set_email(self): arglist = [ @@ -659,7 +664,7 @@ class TestUserSet(TestUser): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.cmd.take_action(parsed_args) + result = self.cmd.take_action(parsed_args) # Set expected values kwargs = { @@ -671,6 +676,7 @@ class TestUserSet(TestUser): identity_fakes.user_id, **kwargs ) + self.assertIsNone(result) def test_user_set_project(self): arglist = [ @@ -688,13 +694,14 @@ class TestUserSet(TestUser): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.cmd.take_action(parsed_args) + result = self.cmd.take_action(parsed_args) # UserManager.update_tenant(user, tenant) self.users_mock.update_tenant.assert_called_with( identity_fakes.user_id, identity_fakes.project_id, ) + self.assertIsNone(result) def test_user_set_enable(self): arglist = [ @@ -712,7 +719,7 @@ class TestUserSet(TestUser): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.cmd.take_action(parsed_args) + result = self.cmd.take_action(parsed_args) # Set expected values kwargs = { @@ -723,6 +730,7 @@ class TestUserSet(TestUser): identity_fakes.user_id, **kwargs ) + self.assertIsNone(result) def test_user_set_disable(self): arglist = [ @@ -740,7 +748,7 @@ class TestUserSet(TestUser): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.cmd.take_action(parsed_args) + result = self.cmd.take_action(parsed_args) # Set expected values kwargs = { @@ -751,6 +759,7 @@ class TestUserSet(TestUser): identity_fakes.user_id, **kwargs ) + self.assertIsNone(result) class TestUserShow(TestUser): diff --git a/openstackclient/tests/identity/v3/test_catalog.py b/openstackclient/tests/identity/v3/test_catalog.py index a03c9d3..1b8fa08 100644 --- a/openstackclient/tests/identity/v3/test_catalog.py +++ b/openstackclient/tests/identity/v3/test_catalog.py @@ -38,6 +38,11 @@ class TestCatalog(utils.TestCommand): 'url': 'https://internal.example.com', 'interface': 'internal', }, + { + 'region': None, + 'url': 'https://none.example.com', + 'interface': 'none', + }, ], } @@ -81,7 +86,8 @@ class TestCatalogList(TestCatalog): 'compute', 'onlyone\n public: https://public.example.com\n' 'onlyone\n admin: https://admin.example.com\n' - '<none>\n internal: https://internal.example.com\n', + '<none>\n internal: https://internal.example.com\n' + '<none>\n none: https://none.example.com\n', ), ) self.assertEqual(datalist, tuple(data)) @@ -114,7 +120,8 @@ class TestCatalogShow(TestCatalog): datalist = ( 'onlyone\n public: https://public.example.com\nonlyone\n' ' admin: https://admin.example.com\n' - '<none>\n internal: https://internal.example.com\n', + '<none>\n internal: https://internal.example.com\n' + '<none>\n none: https://none.example.com\n', 'qwertyuiop', 'supernova', 'compute', diff --git a/openstackclient/tests/identity/v3/test_consumer.py b/openstackclient/tests/identity/v3/test_consumer.py index 7f6af73..4a8cf08 100644 --- a/openstackclient/tests/identity/v3/test_consumer.py +++ b/openstackclient/tests/identity/v3/test_consumer.py @@ -87,12 +87,12 @@ class TestConsumerDelete(TestOAuth1): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - result = self.cmd.run(parsed_args) - self.assertEqual(0, result) + result = self.cmd.take_action(parsed_args) self.consumers_mock.delete.assert_called_with( identity_fakes.consumer_id, ) + self.assertIsNone(result) class TestConsumerList(TestOAuth1): @@ -136,83 +136,84 @@ class TestConsumerList(TestOAuth1): self.assertEqual(datalist, tuple(data)) -class TestConsumerShow(TestOAuth1): +class TestConsumerSet(TestOAuth1): def setUp(self): - super(TestConsumerShow, self).setUp() + super(TestConsumerSet, self).setUp() - consumer_no_secret = copy.deepcopy(identity_fakes.OAUTH_CONSUMER) - del consumer_no_secret['secret'] self.consumers_mock.get.return_value = fakes.FakeResource( None, - consumer_no_secret, + copy.deepcopy(identity_fakes.OAUTH_CONSUMER), loaded=True, ) - # Get the command object to test - self.cmd = consumer.ShowConsumer(self.app, None) + consumer_updated = copy.deepcopy(identity_fakes.OAUTH_CONSUMER) + consumer_updated['description'] = "consumer new description" + self.consumers_mock.update.return_value = fakes.FakeResource( + None, + consumer_updated, + loaded=True, + ) + + self.cmd = consumer.SetConsumer(self.app, None) + + def test_consumer_update(self): + new_description = "consumer new description" - def test_consumer_show(self): arglist = [ + '--description', new_description, identity_fakes.consumer_id, ] verifylist = [ + ('description', new_description), ('consumer', identity_fakes.consumer_id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - columns, data = self.cmd.take_action(parsed_args) - self.consumers_mock.get.assert_called_with( - identity_fakes.consumer_id, - ) + result = self.cmd.take_action(parsed_args) - collist = ('description', 'id') - self.assertEqual(collist, columns) - datalist = ( - identity_fakes.consumer_description, + kwargs = {'description': new_description} + self.consumers_mock.update.assert_called_with( identity_fakes.consumer_id, + **kwargs ) - self.assertEqual(datalist, data) + self.assertIsNone(result) -class TestConsumerSet(TestOAuth1): +class TestConsumerShow(TestOAuth1): def setUp(self): - super(TestConsumerSet, self).setUp() + super(TestConsumerShow, self).setUp() + consumer_no_secret = copy.deepcopy(identity_fakes.OAUTH_CONSUMER) + del consumer_no_secret['secret'] self.consumers_mock.get.return_value = fakes.FakeResource( None, - copy.deepcopy(identity_fakes.OAUTH_CONSUMER), - loaded=True, - ) - - consumer_updated = copy.deepcopy(identity_fakes.OAUTH_CONSUMER) - consumer_updated['description'] = "consumer new description" - self.consumers_mock.update.return_value = fakes.FakeResource( - None, - consumer_updated, + consumer_no_secret, loaded=True, ) - self.cmd = consumer.SetConsumer(self.app, None) - - def test_consumer_update(self): - new_description = "consumer new description" + # Get the command object to test + self.cmd = consumer.ShowConsumer(self.app, None) + def test_consumer_show(self): arglist = [ - '--description', new_description, identity_fakes.consumer_id, ] verifylist = [ - ('description', new_description), ('consumer', identity_fakes.consumer_id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - result = self.cmd.run(parsed_args) - self.assertEqual(0, result) + columns, data = self.cmd.take_action(parsed_args) - kwargs = {'description': new_description} - self.consumers_mock.update.assert_called_with( + self.consumers_mock.get.assert_called_with( identity_fakes.consumer_id, - **kwargs ) + + collist = ('description', 'id') + self.assertEqual(collist, columns) + datalist = ( + identity_fakes.consumer_description, + identity_fakes.consumer_id, + ) + self.assertEqual(datalist, data) diff --git a/openstackclient/tests/identity/v3/test_credential.py b/openstackclient/tests/identity/v3/test_credential.py index afff7b6..d886612 100644 --- a/openstackclient/tests/identity/v3/test_credential.py +++ b/openstackclient/tests/identity/v3/test_credential.py @@ -96,9 +96,11 @@ class TestCredentialSet(TestCredential): '--data', self.json_data, identity_fakes.credential_id, ] - parsed_args = self.check_parser(self.cmd, arglist, []) - self.cmd.take_action(parsed_args) + + result = self.cmd.take_action(parsed_args) + + self.assertIsNone(result) def test_credential_set_valid_with_project(self): arglist = [ @@ -108,6 +110,8 @@ class TestCredentialSet(TestCredential): '--project', identity_fakes.project_name, identity_fakes.credential_id, ] - parsed_args = self.check_parser(self.cmd, arglist, []) - self.cmd.take_action(parsed_args) + + result = self.cmd.take_action(parsed_args) + + self.assertIsNone(result) diff --git a/openstackclient/tests/identity/v3/test_domain.py b/openstackclient/tests/identity/v3/test_domain.py index 9de6b26..f3777f1 100644 --- a/openstackclient/tests/identity/v3/test_domain.py +++ b/openstackclient/tests/identity/v3/test_domain.py @@ -194,12 +194,12 @@ class TestDomainDelete(TestDomain): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - result = self.cmd.run(parsed_args) - self.assertEqual(0, result) + result = self.cmd.take_action(parsed_args) self.domains_mock.delete.assert_called_with( identity_fakes.domain_id, ) + self.assertIsNone(result) class TestDomainList(TestDomain): @@ -269,10 +269,10 @@ class TestDomainSet(TestDomain): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - result = self.cmd.run(parsed_args) - self.assertEqual(0, result) + result = self.cmd.take_action(parsed_args) self.assertNotCalled(self.domains_mock.update) + self.assertIsNone(result) def test_domain_set_name(self): arglist = [ @@ -285,8 +285,7 @@ class TestDomainSet(TestDomain): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - result = self.cmd.run(parsed_args) - self.assertEqual(0, result) + result = self.cmd.take_action(parsed_args) # Set expected values kwargs = { @@ -296,6 +295,7 @@ class TestDomainSet(TestDomain): identity_fakes.domain_id, **kwargs ) + self.assertIsNone(result) def test_domain_set_description(self): arglist = [ @@ -308,8 +308,7 @@ class TestDomainSet(TestDomain): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - result = self.cmd.run(parsed_args) - self.assertEqual(0, result) + result = self.cmd.take_action(parsed_args) # Set expected values kwargs = { @@ -319,6 +318,7 @@ class TestDomainSet(TestDomain): identity_fakes.domain_id, **kwargs ) + self.assertIsNone(result) def test_domain_set_enable(self): arglist = [ @@ -331,8 +331,7 @@ class TestDomainSet(TestDomain): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - result = self.cmd.run(parsed_args) - self.assertEqual(0, result) + result = self.cmd.take_action(parsed_args) # Set expected values kwargs = { @@ -342,6 +341,7 @@ class TestDomainSet(TestDomain): identity_fakes.domain_id, **kwargs ) + self.assertIsNone(result) def test_domain_set_disable(self): arglist = [ @@ -354,8 +354,7 @@ class TestDomainSet(TestDomain): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - result = self.cmd.run(parsed_args) - self.assertEqual(0, result) + result = self.cmd.take_action(parsed_args) # Set expected values kwargs = { @@ -365,6 +364,7 @@ class TestDomainSet(TestDomain): identity_fakes.domain_id, **kwargs ) + self.assertIsNone(result) class TestDomainShow(TestDomain): diff --git a/openstackclient/tests/identity/v3/test_endpoint.py b/openstackclient/tests/identity/v3/test_endpoint.py index 1c48193..d953459 100644 --- a/openstackclient/tests/identity/v3/test_endpoint.py +++ b/openstackclient/tests/identity/v3/test_endpoint.py @@ -277,12 +277,12 @@ class TestEndpointDelete(TestEndpoint): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - result = self.cmd.run(parsed_args) - self.assertEqual(0, result) + result = self.cmd.take_action(parsed_args) self.endpoints_mock.delete.assert_called_with( identity_fakes.endpoint_id, ) + self.assertIsNone(result) class TestEndpointList(TestEndpoint): @@ -483,10 +483,10 @@ class TestEndpointSet(TestEndpoint): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - result = self.cmd.run(parsed_args) - self.assertEqual(0, result) + result = self.cmd.take_action(parsed_args) self.assertNotCalled(self.endpoints_mock.update) + self.assertIsNone(result) def test_endpoint_set_interface(self): arglist = [ @@ -499,8 +499,7 @@ class TestEndpointSet(TestEndpoint): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - result = self.cmd.run(parsed_args) - self.assertEqual(0, result) + result = self.cmd.take_action(parsed_args) # Set expected values kwargs = { @@ -514,6 +513,7 @@ class TestEndpointSet(TestEndpoint): identity_fakes.endpoint_id, **kwargs ) + self.assertIsNone(result) def test_endpoint_set_url(self): arglist = [ @@ -526,8 +526,7 @@ class TestEndpointSet(TestEndpoint): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - result = self.cmd.run(parsed_args) - self.assertEqual(0, result) + result = self.cmd.take_action(parsed_args) # Set expected values kwargs = { @@ -541,6 +540,7 @@ class TestEndpointSet(TestEndpoint): identity_fakes.endpoint_id, **kwargs ) + self.assertIsNone(result) def test_endpoint_set_service(self): arglist = [ @@ -553,8 +553,7 @@ class TestEndpointSet(TestEndpoint): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - result = self.cmd.run(parsed_args) - self.assertEqual(0, result) + result = self.cmd.take_action(parsed_args) # Set expected values kwargs = { @@ -568,6 +567,7 @@ class TestEndpointSet(TestEndpoint): identity_fakes.endpoint_id, **kwargs ) + self.assertIsNone(result) def test_endpoint_set_region(self): arglist = [ @@ -580,8 +580,7 @@ class TestEndpointSet(TestEndpoint): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - result = self.cmd.run(parsed_args) - self.assertEqual(0, result) + result = self.cmd.take_action(parsed_args) # Set expected values kwargs = { @@ -595,6 +594,7 @@ class TestEndpointSet(TestEndpoint): identity_fakes.endpoint_id, **kwargs ) + self.assertIsNone(result) def test_endpoint_set_enable(self): arglist = [ @@ -607,8 +607,7 @@ class TestEndpointSet(TestEndpoint): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - result = self.cmd.run(parsed_args) - self.assertEqual(0, result) + result = self.cmd.take_action(parsed_args) # Set expected values kwargs = { @@ -622,6 +621,7 @@ class TestEndpointSet(TestEndpoint): identity_fakes.endpoint_id, **kwargs ) + self.assertIsNone(result) def test_endpoint_set_disable(self): arglist = [ @@ -634,8 +634,7 @@ class TestEndpointSet(TestEndpoint): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - result = self.cmd.run(parsed_args) - self.assertEqual(0, result) + result = self.cmd.take_action(parsed_args) # Set expected values kwargs = { @@ -649,6 +648,7 @@ class TestEndpointSet(TestEndpoint): identity_fakes.endpoint_id, **kwargs ) + self.assertIsNone(result) class TestEndpointShow(TestEndpoint): diff --git a/openstackclient/tests/identity/v3/test_identity_provider.py b/openstackclient/tests/identity/v3/test_identity_provider.py index ddad6ff..3ff7981 100644 --- a/openstackclient/tests/identity/v3/test_identity_provider.py +++ b/openstackclient/tests/identity/v3/test_identity_provider.py @@ -258,10 +258,13 @@ class TestIdentityProviderDelete(TestIdentityProvider): ('identity_provider', identity_fakes.idp_id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.cmd.take_action(parsed_args) + + result = self.cmd.take_action(parsed_args) + self.identity_providers_mock.delete.assert_called_with( identity_fakes.idp_id, ) + self.assertIsNone(result) class TestIdentityProviderList(TestIdentityProvider): @@ -307,46 +310,6 @@ class TestIdentityProviderList(TestIdentityProvider): self.assertEqual(datalist, tuple(data)) -class TestIdentityProviderShow(TestIdentityProvider): - - def setUp(self): - super(TestIdentityProviderShow, self).setUp() - - ret = fakes.FakeResource( - None, - copy.deepcopy(identity_fakes.IDENTITY_PROVIDER), - loaded=True, - ) - self.identity_providers_mock.get.return_value = ret - # Get the command object to test - self.cmd = identity_provider.ShowIdentityProvider(self.app, None) - - def test_identity_provider_show(self): - arglist = [ - identity_fakes.idp_id, - ] - verifylist = [ - ('identity_provider', identity_fakes.idp_id), - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - columns, data = self.cmd.take_action(parsed_args) - - self.identity_providers_mock.get.assert_called_with( - identity_fakes.idp_id, - ) - - collist = ('description', 'enabled', 'id', 'remote_ids') - self.assertEqual(collist, columns) - datalist = ( - identity_fakes.idp_description, - True, - identity_fakes.idp_id, - identity_fakes.formatted_idp_remote_ids - ) - self.assertEqual(datalist, data) - - class TestIdentityProviderSet(TestIdentityProvider): columns = ( @@ -624,3 +587,44 @@ class TestIdentityProviderSet(TestIdentityProvider): # neither --enable nor --disable was specified self.assertIsNone(columns) self.assertIsNone(data) + + +class TestIdentityProviderShow(TestIdentityProvider): + + def setUp(self): + super(TestIdentityProviderShow, self).setUp() + + ret = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.IDENTITY_PROVIDER), + loaded=True, + ) + self.identity_providers_mock.get.return_value = ret + # Get the command object to test + self.cmd = identity_provider.ShowIdentityProvider(self.app, None) + + def test_identity_provider_show(self): + arglist = [ + identity_fakes.idp_id, + ] + verifylist = [ + ('identity_provider', identity_fakes.idp_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.identity_providers_mock.get.assert_called_with( + identity_fakes.idp_id, + id='test_idp' + ) + + collist = ('description', 'enabled', 'id', 'remote_ids') + self.assertEqual(collist, columns) + datalist = ( + identity_fakes.idp_description, + True, + identity_fakes.idp_id, + identity_fakes.formatted_idp_remote_ids + ) + self.assertEqual(datalist, data) diff --git a/openstackclient/tests/identity/v3/test_mappings.py b/openstackclient/tests/identity/v3/test_mappings.py index e811c0b..b9e3b1d 100644 --- a/openstackclient/tests/identity/v3/test_mappings.py +++ b/openstackclient/tests/identity/v3/test_mappings.py @@ -92,11 +92,13 @@ class TestMappingDelete(TestMapping): verifylist = [ ('mapping', identity_fakes.mapping_id) ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.cmd.take_action(parsed_args) + + result = self.cmd.take_action(parsed_args) + self.mapping_mock.delete.assert_called_with( identity_fakes.mapping_id) + self.assertIsNone(result) class TestMappingList(TestMapping): @@ -143,41 +145,6 @@ class TestMappingList(TestMapping): self.assertEqual(datalist, data) -class TestMappingShow(TestMapping): - - def setUp(self): - super(TestMappingShow, self).setUp() - - self.mapping_mock.get.return_value = fakes.FakeResource( - None, - copy.deepcopy(identity_fakes.MAPPING_RESPONSE), - loaded=True - ) - - self.cmd = mapping.ShowMapping(self.app, None) - - def test_mapping_show(self): - arglist = [ - identity_fakes.mapping_id - ] - verifylist = [ - ('mapping', identity_fakes.mapping_id) - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - columns, data = self.cmd.take_action(parsed_args) - - self.mapping_mock.get.assert_called_with( - identity_fakes.mapping_id) - - collist = ('id', 'rules') - self.assertEqual(collist, columns) - - datalist = (identity_fakes.mapping_id, - identity_fakes.MAPPING_RULES) - self.assertEqual(datalist, data) - - class TestMappingSet(TestMapping): def setUp(self): @@ -234,10 +201,44 @@ class TestMappingSet(TestMapping): ('mapping', identity_fakes.mapping_id), ('rules', identity_fakes.mapping_rules_file_path) ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) self.assertRaises( exceptions.CommandError, self.cmd.take_action, parsed_args) + + +class TestMappingShow(TestMapping): + + def setUp(self): + super(TestMappingShow, self).setUp() + + self.mapping_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.MAPPING_RESPONSE), + loaded=True + ) + + self.cmd = mapping.ShowMapping(self.app, None) + + def test_mapping_show(self): + arglist = [ + identity_fakes.mapping_id + ] + verifylist = [ + ('mapping', identity_fakes.mapping_id) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.mapping_mock.get.assert_called_with( + identity_fakes.mapping_id) + + collist = ('id', 'rules') + self.assertEqual(collist, columns) + + datalist = (identity_fakes.mapping_id, + identity_fakes.MAPPING_RULES) + self.assertEqual(datalist, data) diff --git a/openstackclient/tests/identity/v3/test_oauth.py b/openstackclient/tests/identity/v3/test_oauth.py index dba6d03..d3cf365 100644 --- a/openstackclient/tests/identity/v3/test_oauth.py +++ b/openstackclient/tests/identity/v3/test_oauth.py @@ -32,52 +32,52 @@ class TestOAuth1(identity_fakes.TestOAuth1): self.roles_mock.reset_mock() -class TestRequestTokenCreate(TestOAuth1): +class TestAccessTokenCreate(TestOAuth1): def setUp(self): - super(TestRequestTokenCreate, self).setUp() - - self.request_tokens_mock.create.return_value = fakes.FakeResource( - None, - copy.deepcopy(identity_fakes.OAUTH_REQUEST_TOKEN), - loaded=True, - ) + super(TestAccessTokenCreate, self).setUp() - self.projects_mock.get.return_value = fakes.FakeResource( + self.access_tokens_mock.create.return_value = fakes.FakeResource( None, - copy.deepcopy(identity_fakes.PROJECT), + copy.deepcopy(identity_fakes.OAUTH_ACCESS_TOKEN), loaded=True, ) - self.cmd = token.CreateRequestToken(self.app, None) + self.cmd = token.CreateAccessToken(self.app, None) - def test_create_request_tokens(self): + def test_create_access_tokens(self): arglist = [ '--consumer-key', identity_fakes.consumer_id, '--consumer-secret', identity_fakes.consumer_secret, - '--project', identity_fakes.project_id, + '--request-key', identity_fakes.request_token_id, + '--request-secret', identity_fakes.request_token_secret, + '--verifier', identity_fakes.oauth_verifier_pin, ] verifylist = [ ('consumer_key', identity_fakes.consumer_id), ('consumer_secret', identity_fakes.consumer_secret), - ('project', identity_fakes.project_id), + ('request_key', identity_fakes.request_token_id), + ('request_secret', identity_fakes.request_token_secret), + ('verifier', identity_fakes.oauth_verifier_pin), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.request_tokens_mock.create.assert_called_with( + self.access_tokens_mock.create.assert_called_with( identity_fakes.consumer_id, identity_fakes.consumer_secret, - identity_fakes.project_id, + identity_fakes.request_token_id, + identity_fakes.request_token_secret, + identity_fakes.oauth_verifier_pin, ) collist = ('expires', 'id', 'key', 'secret') self.assertEqual(collist, columns) datalist = ( - identity_fakes.request_token_expires, - identity_fakes.request_token_id, - identity_fakes.request_token_id, - identity_fakes.request_token_secret, + identity_fakes.access_token_expires, + identity_fakes.access_token_id, + identity_fakes.access_token_id, + identity_fakes.access_token_secret, ) self.assertEqual(datalist, data) @@ -123,51 +123,51 @@ class TestRequestTokenAuthorize(TestOAuth1): self.assertEqual(datalist, data) -class TestAccessTokenCreate(TestOAuth1): +class TestRequestTokenCreate(TestOAuth1): def setUp(self): - super(TestAccessTokenCreate, self).setUp() + super(TestRequestTokenCreate, self).setUp() - self.access_tokens_mock.create.return_value = fakes.FakeResource( + self.request_tokens_mock.create.return_value = fakes.FakeResource( None, - copy.deepcopy(identity_fakes.OAUTH_ACCESS_TOKEN), + copy.deepcopy(identity_fakes.OAUTH_REQUEST_TOKEN), loaded=True, ) - self.cmd = token.CreateAccessToken(self.app, None) + self.projects_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.PROJECT), + loaded=True, + ) - def test_create_access_tokens(self): + self.cmd = token.CreateRequestToken(self.app, None) + + def test_create_request_tokens(self): arglist = [ '--consumer-key', identity_fakes.consumer_id, '--consumer-secret', identity_fakes.consumer_secret, - '--request-key', identity_fakes.request_token_id, - '--request-secret', identity_fakes.request_token_secret, - '--verifier', identity_fakes.oauth_verifier_pin, + '--project', identity_fakes.project_id, ] verifylist = [ ('consumer_key', identity_fakes.consumer_id), ('consumer_secret', identity_fakes.consumer_secret), - ('request_key', identity_fakes.request_token_id), - ('request_secret', identity_fakes.request_token_secret), - ('verifier', identity_fakes.oauth_verifier_pin), + ('project', identity_fakes.project_id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.access_tokens_mock.create.assert_called_with( + self.request_tokens_mock.create.assert_called_with( identity_fakes.consumer_id, identity_fakes.consumer_secret, - identity_fakes.request_token_id, - identity_fakes.request_token_secret, - identity_fakes.oauth_verifier_pin, + identity_fakes.project_id, ) collist = ('expires', 'id', 'key', 'secret') self.assertEqual(collist, columns) datalist = ( - identity_fakes.access_token_expires, - identity_fakes.access_token_id, - identity_fakes.access_token_id, - identity_fakes.access_token_secret, + identity_fakes.request_token_expires, + identity_fakes.request_token_id, + identity_fakes.request_token_id, + identity_fakes.request_token_secret, ) self.assertEqual(datalist, data) diff --git a/openstackclient/tests/identity/v3/test_project.py b/openstackclient/tests/identity/v3/test_project.py index b834e94..1e9d1c8 100644 --- a/openstackclient/tests/identity/v3/test_project.py +++ b/openstackclient/tests/identity/v3/test_project.py @@ -436,12 +436,12 @@ class TestProjectDelete(TestProject): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - result = self.cmd.run(parsed_args) - self.assertEqual(0, result) + result = self.cmd.take_action(parsed_args) self.projects_mock.delete.assert_called_with( identity_fakes.project_id, ) + self.assertIsNone(result) class TestProjectList(TestProject): @@ -593,8 +593,9 @@ class TestProjectSet(TestProject): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - result = self.cmd.run(parsed_args) - self.assertEqual(0, result) + result = self.cmd.take_action(parsed_args) + + self.assertIsNone(result) def test_project_set_name(self): arglist = [ @@ -611,8 +612,7 @@ class TestProjectSet(TestProject): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - result = self.cmd.run(parsed_args) - self.assertEqual(0, result) + result = self.cmd.take_action(parsed_args) # Set expected values kwargs = { @@ -624,6 +624,7 @@ class TestProjectSet(TestProject): identity_fakes.project_id, **kwargs ) + self.assertIsNone(result) def test_project_set_description(self): arglist = [ @@ -640,8 +641,7 @@ class TestProjectSet(TestProject): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - result = self.cmd.run(parsed_args) - self.assertEqual(0, result) + result = self.cmd.take_action(parsed_args) # Set expected values kwargs = { @@ -651,6 +651,7 @@ class TestProjectSet(TestProject): identity_fakes.project_id, **kwargs ) + self.assertIsNone(result) def test_project_set_enable(self): arglist = [ @@ -666,8 +667,7 @@ class TestProjectSet(TestProject): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - result = self.cmd.run(parsed_args) - self.assertEqual(0, result) + result = self.cmd.take_action(parsed_args) # Set expected values kwargs = { @@ -677,6 +677,7 @@ class TestProjectSet(TestProject): identity_fakes.project_id, **kwargs ) + self.assertIsNone(result) def test_project_set_disable(self): arglist = [ @@ -692,8 +693,7 @@ class TestProjectSet(TestProject): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - result = self.cmd.run(parsed_args) - self.assertEqual(0, result) + result = self.cmd.take_action(parsed_args) # Set expected values kwargs = { @@ -703,6 +703,7 @@ class TestProjectSet(TestProject): identity_fakes.project_id, **kwargs ) + self.assertIsNone(result) def test_project_set_property(self): arglist = [ @@ -718,8 +719,7 @@ class TestProjectSet(TestProject): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - result = self.cmd.run(parsed_args) - self.assertEqual(0, result) + result = self.cmd.take_action(parsed_args) # Set expected values kwargs = { @@ -730,6 +730,7 @@ class TestProjectSet(TestProject): identity_fakes.project_id, **kwargs ) + self.assertIsNone(result) class TestProjectShow(TestProject): diff --git a/openstackclient/tests/identity/v3/test_protocol.py b/openstackclient/tests/identity/v3/test_protocol.py index 3c9c3f0..238b0ff 100644 --- a/openstackclient/tests/identity/v3/test_protocol.py +++ b/openstackclient/tests/identity/v3/test_protocol.py @@ -92,9 +92,12 @@ class TestProtocolDelete(TestProtocol): ('identity_provider', identity_fakes.idp_id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.cmd.take_action(parsed_args) + + result = self.cmd.take_action(parsed_args) + self.protocols_mock.delete.assert_called_with( identity_fakes.idp_id, identity_fakes.protocol_id) + self.assertIsNone(result) class TestProtocolList(TestProtocol): diff --git a/openstackclient/tests/identity/v3/test_region.py b/openstackclient/tests/identity/v3/test_region.py index f5f5079..02dec56 100644 --- a/openstackclient/tests/identity/v3/test_region.py +++ b/openstackclient/tests/identity/v3/test_region.py @@ -157,12 +157,12 @@ class TestRegionDelete(TestRegion): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - result = self.cmd.run(parsed_args) - self.assertEqual(0, result) + result = self.cmd.take_action(parsed_args) self.regions_mock.delete.assert_called_with( identity_fakes.region_id, ) + self.assertIsNone(result) class TestRegionList(TestRegion): @@ -251,10 +251,10 @@ class TestRegionSet(TestRegion): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - result = self.cmd.run(parsed_args) - self.assertEqual(0, result) + result = self.cmd.take_action(parsed_args) self.assertNotCalled(self.regions_mock.update) + self.assertIsNone(result) def test_region_set_description(self): arglist = [ @@ -267,8 +267,7 @@ class TestRegionSet(TestRegion): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - result = self.cmd.run(parsed_args) - self.assertEqual(0, result) + result = self.cmd.take_action(parsed_args) # Set expected values kwargs = { @@ -278,6 +277,7 @@ class TestRegionSet(TestRegion): identity_fakes.region_id, **kwargs ) + self.assertIsNone(result) def test_region_set_parent_region_id(self): arglist = [ @@ -290,8 +290,7 @@ class TestRegionSet(TestRegion): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - result = self.cmd.run(parsed_args) - self.assertEqual(0, result) + result = self.cmd.take_action(parsed_args) # Set expected values kwargs = { @@ -301,6 +300,7 @@ class TestRegionSet(TestRegion): identity_fakes.region_id, **kwargs ) + self.assertIsNone(result) class TestRegionShow(TestRegion): diff --git a/openstackclient/tests/identity/v3/test_role.py b/openstackclient/tests/identity/v3/test_role.py index f366132..d2398e5 100644 --- a/openstackclient/tests/identity/v3/test_role.py +++ b/openstackclient/tests/identity/v3/test_role.py @@ -116,8 +116,7 @@ class TestRoleAdd(TestRole): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - result = self.cmd.run(parsed_args) - self.assertEqual(0, result) + result = self.cmd.take_action(parsed_args) # Set expected values kwargs = { @@ -130,6 +129,7 @@ class TestRoleAdd(TestRole): identity_fakes.role_id, **kwargs ) + self.assertIsNone(result) def test_role_add_user_project(self): arglist = [ @@ -149,8 +149,7 @@ class TestRoleAdd(TestRole): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - result = self.cmd.run(parsed_args) - self.assertEqual(0, result) + result = self.cmd.take_action(parsed_args) # Set expected values kwargs = { @@ -163,6 +162,7 @@ class TestRoleAdd(TestRole): identity_fakes.role_id, **kwargs ) + self.assertIsNone(result) def test_role_add_group_domain(self): arglist = [ @@ -182,8 +182,7 @@ class TestRoleAdd(TestRole): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - result = self.cmd.run(parsed_args) - self.assertEqual(0, result) + result = self.cmd.take_action(parsed_args) # Set expected values kwargs = { @@ -196,6 +195,7 @@ class TestRoleAdd(TestRole): identity_fakes.role_id, **kwargs ) + self.assertIsNone(result) def test_role_add_group_project(self): arglist = [ @@ -215,8 +215,7 @@ class TestRoleAdd(TestRole): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - result = self.cmd.run(parsed_args) - self.assertEqual(0, result) + result = self.cmd.take_action(parsed_args) # Set expected values kwargs = { @@ -229,6 +228,7 @@ class TestRoleAdd(TestRole): identity_fakes.role_id, **kwargs ) + self.assertIsNone(result) class TestRoleAddInherited(TestRoleAdd, TestRoleInherited): @@ -306,11 +306,12 @@ class TestRoleDelete(TestRole): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.cmd.take_action(parsed_args) + result = self.cmd.take_action(parsed_args) self.roles_mock.delete.assert_called_with( identity_fakes.role_id, ) + self.assertIsNone(result) class TestRoleList(TestRole): @@ -640,7 +641,7 @@ class TestRoleRemove(TestRole): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.cmd.take_action(parsed_args) + result = self.cmd.take_action(parsed_args) # Set expected values kwargs = { @@ -653,6 +654,7 @@ class TestRoleRemove(TestRole): identity_fakes.role_id, **kwargs ) + self.assertIsNone(result) def test_role_remove_user_project(self): arglist = [ @@ -672,7 +674,7 @@ class TestRoleRemove(TestRole): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.cmd.take_action(parsed_args) + result = self.cmd.take_action(parsed_args) # Set expected values kwargs = { @@ -685,6 +687,7 @@ class TestRoleRemove(TestRole): identity_fakes.role_id, **kwargs ) + self.assertIsNone(result) def test_role_remove_group_domain(self): arglist = [ @@ -705,7 +708,7 @@ class TestRoleRemove(TestRole): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.cmd.take_action(parsed_args) + result = self.cmd.take_action(parsed_args) # Set expected values kwargs = { @@ -718,6 +721,7 @@ class TestRoleRemove(TestRole): identity_fakes.role_id, **kwargs ) + self.assertIsNone(result) def test_role_remove_group_project(self): arglist = [ @@ -737,7 +741,7 @@ class TestRoleRemove(TestRole): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.cmd.take_action(parsed_args) + result = self.cmd.take_action(parsed_args) # Set expected values kwargs = { @@ -750,6 +754,7 @@ class TestRoleRemove(TestRole): identity_fakes.role_id, **kwargs ) + self.assertIsNone(result) class TestRoleSet(TestRole): @@ -778,7 +783,7 @@ class TestRoleSet(TestRole): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.cmd.take_action(parsed_args) + result = self.cmd.take_action(parsed_args) # Set expected values kwargs = { @@ -789,6 +794,7 @@ class TestRoleSet(TestRole): identity_fakes.role_id, **kwargs ) + self.assertIsNone(result) class TestRoleShow(TestRole): diff --git a/openstackclient/tests/identity/v3/test_service.py b/openstackclient/tests/identity/v3/test_service.py index 2bc5927..1e70383 100644 --- a/openstackclient/tests/identity/v3/test_service.py +++ b/openstackclient/tests/identity/v3/test_service.py @@ -204,12 +204,12 @@ class TestServiceDelete(TestService): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - result = self.cmd.run(parsed_args) - self.assertEqual(0, result) + result = self.cmd.take_action(parsed_args) self.services_mock.delete.assert_called_with( identity_fakes.service_id, ) + self.assertIsNone(result) class TestServiceList(TestService): @@ -310,8 +310,9 @@ class TestServiceSet(TestService): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - result = self.cmd.run(parsed_args) - self.assertEqual(0, result) + result = self.cmd.take_action(parsed_args) + + self.assertIsNone(result) def test_service_set_type(self): arglist = [ @@ -328,8 +329,7 @@ class TestServiceSet(TestService): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - result = self.cmd.run(parsed_args) - self.assertEqual(0, result) + result = self.cmd.take_action(parsed_args) # Set expected values kwargs = { @@ -340,6 +340,7 @@ class TestServiceSet(TestService): identity_fakes.service_id, **kwargs ) + self.assertIsNone(result) def test_service_set_name(self): arglist = [ @@ -356,8 +357,7 @@ class TestServiceSet(TestService): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - result = self.cmd.run(parsed_args) - self.assertEqual(0, result) + result = self.cmd.take_action(parsed_args) # Set expected values kwargs = { @@ -368,6 +368,7 @@ class TestServiceSet(TestService): identity_fakes.service_id, **kwargs ) + self.assertIsNone(result) def test_service_set_description(self): arglist = [ @@ -384,8 +385,7 @@ class TestServiceSet(TestService): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - result = self.cmd.run(parsed_args) - self.assertEqual(0, result) + result = self.cmd.take_action(parsed_args) # Set expected values kwargs = { @@ -396,6 +396,7 @@ class TestServiceSet(TestService): identity_fakes.service_id, **kwargs ) + self.assertIsNone(result) def test_service_set_enable(self): arglist = [ @@ -412,8 +413,7 @@ class TestServiceSet(TestService): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - result = self.cmd.run(parsed_args) - self.assertEqual(0, result) + result = self.cmd.take_action(parsed_args) # Set expected values kwargs = { @@ -424,6 +424,7 @@ class TestServiceSet(TestService): identity_fakes.service_id, **kwargs ) + self.assertIsNone(result) def test_service_set_disable(self): arglist = [ @@ -440,8 +441,7 @@ class TestServiceSet(TestService): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - result = self.cmd.run(parsed_args) - self.assertEqual(0, result) + result = self.cmd.take_action(parsed_args) # Set expected values kwargs = { @@ -452,6 +452,7 @@ class TestServiceSet(TestService): identity_fakes.service_id, **kwargs ) + self.assertIsNone(result) class TestServiceShow(TestService): diff --git a/openstackclient/tests/identity/v3/test_service_provider.py b/openstackclient/tests/identity/v3/test_service_provider.py index 39f779d..99ea1f7 100644 --- a/openstackclient/tests/identity/v3/test_service_provider.py +++ b/openstackclient/tests/identity/v3/test_service_provider.py @@ -188,10 +188,13 @@ class TestServiceProviderDelete(TestServiceProvider): ('service_provider', service_fakes.sp_id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.cmd.take_action(parsed_args) + + result = self.cmd.take_action(parsed_args) + self.service_providers_mock.delete.assert_called_with( service_fakes.sp_id, ) + self.assertIsNone(result) class TestServiceProviderList(TestServiceProvider): @@ -238,47 +241,6 @@ class TestServiceProviderList(TestServiceProvider): self.assertEqual(tuple(data), datalist) -class TestServiceProviderShow(TestServiceProvider): - - def setUp(self): - super(TestServiceProviderShow, self).setUp() - - ret = fakes.FakeResource( - None, - copy.deepcopy(service_fakes.SERVICE_PROVIDER), - loaded=True, - ) - self.service_providers_mock.get.return_value = ret - # Get the command object to test - self.cmd = service_provider.ShowServiceProvider(self.app, None) - - def test_service_provider_show(self): - arglist = [ - service_fakes.sp_id, - ] - verifylist = [ - ('service_provider', service_fakes.sp_id), - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - columns, data = self.cmd.take_action(parsed_args) - - self.service_providers_mock.get.assert_called_with( - service_fakes.sp_id, - ) - - collist = ('auth_url', 'description', 'enabled', 'id', 'sp_url') - self.assertEqual(collist, columns) - datalist = ( - service_fakes.sp_auth_url, - service_fakes.sp_description, - True, - service_fakes.sp_id, - service_fakes.service_provider_url - ) - self.assertEqual(data, datalist) - - class TestServiceProviderSet(TestServiceProvider): columns = ( @@ -417,3 +379,45 @@ class TestServiceProviderSet(TestServiceProvider): # was set. self.assertIsNone(columns) self.assertIsNone(data) + + +class TestServiceProviderShow(TestServiceProvider): + + def setUp(self): + super(TestServiceProviderShow, self).setUp() + + ret = fakes.FakeResource( + None, + copy.deepcopy(service_fakes.SERVICE_PROVIDER), + loaded=True, + ) + self.service_providers_mock.get.return_value = ret + # Get the command object to test + self.cmd = service_provider.ShowServiceProvider(self.app, None) + + def test_service_provider_show(self): + arglist = [ + service_fakes.sp_id, + ] + verifylist = [ + ('service_provider', service_fakes.sp_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.service_providers_mock.get.assert_called_with( + service_fakes.sp_id, + id='BETA' + ) + + collist = ('auth_url', 'description', 'enabled', 'id', 'sp_url') + self.assertEqual(collist, columns) + datalist = ( + service_fakes.sp_auth_url, + service_fakes.sp_description, + True, + service_fakes.sp_id, + service_fakes.service_provider_url + ) + self.assertEqual(data, datalist) diff --git a/openstackclient/tests/identity/v3/test_token.py b/openstackclient/tests/identity/v3/test_token.py index 80c397b..b68bc24 100644 --- a/openstackclient/tests/identity/v3/test_token.py +++ b/openstackclient/tests/identity/v3/test_token.py @@ -123,6 +123,7 @@ class TestTokenRevoke(TestToken): verifylist = [('token', self.TOKEN)] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.cmd.take_action(parsed_args) + result = self.cmd.take_action(parsed_args) self.tokens_mock.revoke_token.assert_called_with(self.TOKEN) + self.assertIsNone(result) diff --git a/openstackclient/tests/identity/v3/test_trust.py b/openstackclient/tests/identity/v3/test_trust.py index 2a74887..1ea2feb 100644 --- a/openstackclient/tests/identity/v3/test_trust.py +++ b/openstackclient/tests/identity/v3/test_trust.py @@ -141,11 +141,12 @@ class TestTrustDelete(TestTrust): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.cmd.take_action(parsed_args) + result = self.cmd.take_action(parsed_args) self.trusts_mock.delete.assert_called_with( identity_fakes.trust_id, ) + self.assertIsNone(result) class TestTrustList(TestTrust): diff --git a/openstackclient/tests/identity/v3/test_unscoped_saml.py b/openstackclient/tests/identity/v3/test_unscoped_saml.py index c2f1449..d12cb45 100644 --- a/openstackclient/tests/identity/v3/test_unscoped_saml.py +++ b/openstackclient/tests/identity/v3/test_unscoped_saml.py @@ -30,23 +30,23 @@ class TestUnscopedSAML(identity_fakes.TestFederatedIdentity): self.domains_mock.reset_mock() -class TestProjectList(TestUnscopedSAML): +class TestDomainList(TestUnscopedSAML): def setUp(self): - super(TestProjectList, self).setUp() + super(TestDomainList, self).setUp() - self.projects_mock.list.return_value = [ + self.domains_mock.list.return_value = [ fakes.FakeResource( None, - copy.deepcopy(identity_fakes.PROJECT), + copy.deepcopy(identity_fakes.DOMAIN), loaded=True, ), ] # Get the command object to test - self.cmd = unscoped_saml.ListAccessibleProjects(self.app, None) + self.cmd = unscoped_saml.ListAccessibleDomains(self.app, None) - def test_accessible_projects_list(self): + def test_accessible_domains_list(self): self.app.client_manager.auth_plugin_name = 'v3unscopedsaml' arglist = [] verifylist = [] @@ -57,19 +57,19 @@ class TestProjectList(TestUnscopedSAML): # containing the data to be listed. columns, data = self.cmd.take_action(parsed_args) - self.projects_mock.list.assert_called_with() + self.domains_mock.list.assert_called_with() - collist = ('ID', 'Domain ID', 'Enabled', 'Name') + collist = ('ID', 'Enabled', 'Name', 'Description') self.assertEqual(collist, columns) datalist = (( - identity_fakes.project_id, identity_fakes.domain_id, True, - identity_fakes.project_name, + identity_fakes.domain_name, + identity_fakes.domain_description, ), ) self.assertEqual(datalist, tuple(data)) - def test_accessible_projects_list_wrong_auth(self): + def test_accessible_domains_list_wrong_auth(self): auth = identity_fakes.FakeAuth("wrong auth") self.app.client_manager.identity.session.auth = auth arglist = [] @@ -81,23 +81,23 @@ class TestProjectList(TestUnscopedSAML): parsed_args) -class TestDomainList(TestUnscopedSAML): +class TestProjectList(TestUnscopedSAML): def setUp(self): - super(TestDomainList, self).setUp() + super(TestProjectList, self).setUp() - self.domains_mock.list.return_value = [ + self.projects_mock.list.return_value = [ fakes.FakeResource( None, - copy.deepcopy(identity_fakes.DOMAIN), + copy.deepcopy(identity_fakes.PROJECT), loaded=True, ), ] # Get the command object to test - self.cmd = unscoped_saml.ListAccessibleDomains(self.app, None) + self.cmd = unscoped_saml.ListAccessibleProjects(self.app, None) - def test_accessible_domains_list(self): + def test_accessible_projects_list(self): self.app.client_manager.auth_plugin_name = 'v3unscopedsaml' arglist = [] verifylist = [] @@ -108,19 +108,19 @@ class TestDomainList(TestUnscopedSAML): # containing the data to be listed. columns, data = self.cmd.take_action(parsed_args) - self.domains_mock.list.assert_called_with() + self.projects_mock.list.assert_called_with() - collist = ('ID', 'Enabled', 'Name', 'Description') + collist = ('ID', 'Domain ID', 'Enabled', 'Name') self.assertEqual(collist, columns) datalist = (( + identity_fakes.project_id, identity_fakes.domain_id, True, - identity_fakes.domain_name, - identity_fakes.domain_description, + identity_fakes.project_name, ), ) self.assertEqual(datalist, tuple(data)) - def test_accessible_domains_list_wrong_auth(self): + def test_accessible_projects_list_wrong_auth(self): auth = identity_fakes.FakeAuth("wrong auth") self.app.client_manager.identity.session.auth = auth arglist = [] diff --git a/openstackclient/tests/identity/v3/test_user.py b/openstackclient/tests/identity/v3/test_user.py index 3757c5f..5dafa77 100644 --- a/openstackclient/tests/identity/v3/test_user.py +++ b/openstackclient/tests/identity/v3/test_user.py @@ -499,11 +499,12 @@ class TestUserDelete(TestUser): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.cmd.take_action(parsed_args) + result = self.cmd.take_action(parsed_args) self.users_mock.delete.assert_called_with( identity_fakes.user_id, ) + self.assertIsNone(result) class TestUserList(TestUser): @@ -754,8 +755,9 @@ class TestUserSet(TestUser): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - result = self.cmd.run(parsed_args) - self.assertEqual(0, result) + result = self.cmd.take_action(parsed_args) + + self.assertIsNone(result) def test_user_set_name(self): arglist = [ @@ -773,7 +775,7 @@ class TestUserSet(TestUser): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.cmd.take_action(parsed_args) + result = self.cmd.take_action(parsed_args) # Set expected values kwargs = { @@ -786,6 +788,7 @@ class TestUserSet(TestUser): identity_fakes.user_id, **kwargs ) + self.assertIsNone(result) def test_user_set_password(self): arglist = [ @@ -804,7 +807,7 @@ class TestUserSet(TestUser): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.cmd.take_action(parsed_args) + result = self.cmd.take_action(parsed_args) # Set expected values kwargs = { @@ -817,6 +820,7 @@ class TestUserSet(TestUser): identity_fakes.user_id, **kwargs ) + self.assertIsNone(result) def test_user_set_password_prompt(self): arglist = [ @@ -838,7 +842,7 @@ class TestUserSet(TestUser): mocker = mock.Mock() mocker.return_value = 'abc123' with mock.patch("openstackclient.common.utils.get_password", mocker): - self.cmd.take_action(parsed_args) + result = self.cmd.take_action(parsed_args) # Set expected values kwargs = { @@ -851,6 +855,7 @@ class TestUserSet(TestUser): identity_fakes.user_id, **kwargs ) + self.assertIsNone(result) def test_user_set_email(self): arglist = [ @@ -868,7 +873,7 @@ class TestUserSet(TestUser): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.cmd.take_action(parsed_args) + result = self.cmd.take_action(parsed_args) # Set expected values kwargs = { @@ -881,6 +886,7 @@ class TestUserSet(TestUser): identity_fakes.user_id, **kwargs ) + self.assertIsNone(result) def test_user_set_project(self): arglist = [ @@ -898,7 +904,7 @@ class TestUserSet(TestUser): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.cmd.take_action(parsed_args) + result = self.cmd.take_action(parsed_args) # Set expected values kwargs = { @@ -911,6 +917,7 @@ class TestUserSet(TestUser): identity_fakes.user_id, **kwargs ) + self.assertIsNone(result) def test_user_set_project_domain(self): arglist = [ @@ -930,7 +937,7 @@ class TestUserSet(TestUser): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.cmd.take_action(parsed_args) + result = self.cmd.take_action(parsed_args) # Set expected values kwargs = { @@ -943,6 +950,7 @@ class TestUserSet(TestUser): identity_fakes.user_id, **kwargs ) + self.assertIsNone(result) def test_user_set_enable(self): arglist = [ @@ -960,7 +968,7 @@ class TestUserSet(TestUser): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.cmd.take_action(parsed_args) + result = self.cmd.take_action(parsed_args) # Set expected values kwargs = { @@ -972,6 +980,7 @@ class TestUserSet(TestUser): identity_fakes.user_id, **kwargs ) + self.assertIsNone(result) def test_user_set_disable(self): arglist = [ @@ -989,7 +998,7 @@ class TestUserSet(TestUser): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.cmd.take_action(parsed_args) + result = self.cmd.take_action(parsed_args) # Set expected values kwargs = { @@ -1001,6 +1010,7 @@ class TestUserSet(TestUser): identity_fakes.user_id, **kwargs ) + self.assertIsNone(result) class TestUserSetPassword(TestUser): @@ -1029,11 +1039,12 @@ class TestUserSetPassword(TestUser): # Mock getting user current password. with self._mock_get_password(current_pass): - self.cmd.take_action(parsed_args) + result = self.cmd.take_action(parsed_args) self.users_mock.update_password.assert_called_with( current_pass, new_pass ) + self.assertIsNone(result) def test_user_create_password_prompt(self): current_pass = 'old_pass' @@ -1042,11 +1053,12 @@ class TestUserSetPassword(TestUser): # Mock getting user current and new password. with self._mock_get_password(current_pass, new_pass): - self.cmd.take_action(parsed_args) + result = self.cmd.take_action(parsed_args) self.users_mock.update_password.assert_called_with( current_pass, new_pass ) + self.assertIsNone(result) def test_user_password_change_no_prompt(self): current_pass = 'old_pass' @@ -1061,11 +1073,12 @@ class TestUserSetPassword(TestUser): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.cmd.take_action(parsed_args) + result = self.cmd.take_action(parsed_args) self.users_mock.update_password.assert_called_with( current_pass, new_pass ) + self.assertIsNone(result) class TestUserShow(TestUser): diff --git a/openstackclient/tests/image/v2/fakes.py b/openstackclient/tests/image/v2/fakes.py index 3555d2d..a662a58 100644 --- a/openstackclient/tests/image/v2/fakes.py +++ b/openstackclient/tests/image/v2/fakes.py @@ -18,6 +18,9 @@ import mock import random import uuid +from glanceclient.v2 import schemas +import warlock + from openstackclient.common import utils as common_utils from openstackclient.tests import fakes from openstackclient.tests import utils @@ -181,7 +184,7 @@ class FakeImage(object): """ @staticmethod - def create_one_image(attrs={}): + def create_one_image(attrs=None): """Create a fake image. :param Dictionary attrs: @@ -190,9 +193,11 @@ class FakeImage(object): A FakeResource object with id, name, owner, protected, visibility and tags attrs """ + attrs = attrs or {} + # Set default attribute image_info = { - 'id': 'image-id' + uuid.uuid4().hex, + 'id': str(uuid.uuid4()), 'name': 'image-name' + uuid.uuid4().hex, 'owner': 'image-owner' + uuid.uuid4().hex, 'protected': bool(random.choice([0, 1])), @@ -203,14 +208,16 @@ class FakeImage(object): # Overwrite default attributes if there are some attributes set image_info.update(attrs) - image = fakes.FakeResource( - None, - image_info, - loaded=True) - return image + # Set up the schema + model = warlock.model_factory( + IMAGE_schema, + schemas.SchemaBasedModel, + ) + + return model(**image_info) @staticmethod - def create_images(attrs={}, count=2): + def create_images(attrs=None, count=2): """Create multiple fake images. :param Dictionary attrs: @@ -247,27 +254,6 @@ class FakeImage(object): return mock.MagicMock(side_effect=images) @staticmethod - def get_image_info(image=None): - """Get the image info from a faked image object. - - :param image: - A FakeResource objects faking image - :return - A dictionary which includes the faked image info as follows: - { - 'id': image_id, - 'name': image_name, - 'owner': image_owner, - 'protected': image_protected, - 'visibility': image_visibility, - 'tags': image_tags - } - """ - if image is not None: - return image._info - return {} - - @staticmethod def get_image_columns(image=None): """Get the image columns from a faked image object. @@ -278,9 +264,8 @@ class FakeImage(object): ('id', 'name', 'owner', 'protected', 'visibility', 'tags') """ if image is not None: - return tuple(k for k in sorted( - FakeImage.get_image_info(image).keys())) - return tuple([]) + return tuple(sorted(image)) + return IMAGE_columns @staticmethod def get_image_data(image=None): @@ -294,7 +279,7 @@ class FakeImage(object): """ data_list = [] if image is not None: - for x in sorted(FakeImage.get_image_info(image).keys()): + for x in sorted(image.keys()): if x == 'tags': # The 'tags' should be format_list data_list.append( diff --git a/openstackclient/tests/image/v2/test_image.py b/openstackclient/tests/image/v2/test_image.py index 0248f30..beebdef 100644 --- a/openstackclient/tests/image/v2/test_image.py +++ b/openstackclient/tests/image/v2/test_image.py @@ -20,6 +20,7 @@ import warlock from glanceclient.v2 import schemas from openstackclient.common import exceptions +from openstackclient.common import utils as common_utils from openstackclient.image.v2 import image from openstackclient.tests import fakes from openstackclient.tests.identity.v3 import fakes as identity_fakes @@ -74,7 +75,8 @@ class TestImageCreate(TestImage): # This is the return value for utils.find_resource() self.images_mock.get.return_value = copy.deepcopy( - image_fakes.FakeImage.get_image_info(self.new_image)) + self.new_image + ) self.images_mock.update.return_value = self.new_image # Get the command object to test @@ -341,28 +343,31 @@ class TestImageCreate(TestImage): class TestAddProjectToImage(TestImage): + _image = image_fakes.FakeImage.create_one_image() + columns = ( 'image_id', 'member_id', 'status', ) + datalist = ( - image_fakes.image_id, + _image.id, identity_fakes.project_id, - image_fakes.member_status, + image_fakes.member_status ) def setUp(self): super(TestAddProjectToImage, self).setUp() # This is the return value for utils.find_resource() - self.images_mock.get.return_value = fakes.FakeResource( - None, - copy.deepcopy(image_fakes.IMAGE), - loaded=True, - ) + self.images_mock.get.return_value = self._image + + # Update the image_id in the MEMBER dict + self.new_member = copy.deepcopy(image_fakes.MEMBER) + self.new_member['image_id'] = self._image.id self.image_members_mock.create.return_value = fakes.FakeModel( - copy.deepcopy(image_fakes.MEMBER), + self.new_member, ) self.project_mock.get.return_value = fakes.FakeResource( None, @@ -379,11 +384,11 @@ class TestAddProjectToImage(TestImage): def test_add_project_to_image_no_option(self): arglist = [ - image_fakes.image_id, + self._image.id, identity_fakes.project_id, ] verifylist = [ - ('image', image_fakes.image_id), + ('image', self._image.id), ('project', identity_fakes.project_id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -393,20 +398,21 @@ class TestAddProjectToImage(TestImage): # data to be shown. columns, data = self.cmd.take_action(parsed_args) self.image_members_mock.create.assert_called_with( - image_fakes.image_id, + self._image.id, identity_fakes.project_id ) + self.assertEqual(self.columns, columns) self.assertEqual(self.datalist, data) def test_add_project_to_image_with_option(self): arglist = [ - image_fakes.image_id, + self._image.id, identity_fakes.project_id, '--project-domain', identity_fakes.domain_id, ] verifylist = [ - ('image', image_fakes.image_id), + ('image', self._image.id), ('project', identity_fakes.project_id), ('project_domain', identity_fakes.domain_id), ] @@ -417,7 +423,7 @@ class TestAddProjectToImage(TestImage): # data to be shown. columns, data = self.cmd.take_action(parsed_args) self.image_members_mock.create.assert_called_with( - image_fakes.image_id, + self._image.id, identity_fakes.project_id ) self.assertEqual(self.columns, columns) @@ -468,25 +474,26 @@ class TestImageDelete(TestImage): class TestImageList(TestImage): + _image = image_fakes.FakeImage.create_one_image() + columns = ( 'ID', 'Name', 'Status', ) + datalist = ( - ( - image_fakes.image_id, - image_fakes.image_name, - '', - ), - ) + _image.id, + _image.name, + '', + ), def setUp(self): super(TestImageList, self).setUp() self.api_mock = mock.Mock() self.api_mock.image_list.side_effect = [ - [copy.deepcopy(image_fakes.IMAGE)], [], + [self._image], [], ] self.app.client_manager.image.api = self.api_mock @@ -611,24 +618,22 @@ class TestImageList(TestImage): self.assertEqual(collist, columns) datalist = (( - image_fakes.image_id, - image_fakes.image_name, - '', + self._image.id, + self._image.name, '', '', '', - 'public', - False, - image_fakes.image_owner, '', + self._image.visibility, + self._image.protected, + self._image.owner, + common_utils.format_list(self._image.tags), ), ) self.assertEqual(datalist, tuple(data)) @mock.patch('openstackclient.api.utils.simple_filter') def test_image_list_property_option(self, sf_mock): - sf_mock.return_value = [ - copy.deepcopy(image_fakes.IMAGE), - ] + sf_mock.return_value = [copy.deepcopy(self._image)] arglist = [ '--property', 'a=1', @@ -644,7 +649,7 @@ class TestImageList(TestImage): columns, data = self.cmd.take_action(parsed_args) self.api_mock.image_list.assert_called_with() sf_mock.assert_called_with( - [image_fakes.IMAGE], + [self._image], attr='a', value='1', property_field='properties', @@ -655,9 +660,7 @@ class TestImageList(TestImage): @mock.patch('openstackclient.common.utils.sort_items') def test_image_list_sort_option(self, si_mock): - si_mock.return_value = [ - copy.deepcopy(image_fakes.IMAGE) - ] + si_mock.return_value = [copy.deepcopy(self._image)] arglist = ['--sort', 'name:asc'] verifylist = [('sort', 'name:asc')] @@ -669,10 +672,9 @@ class TestImageList(TestImage): columns, data = self.cmd.take_action(parsed_args) self.api_mock.image_list.assert_called_with() si_mock.assert_called_with( - [image_fakes.IMAGE], + [self._image], 'name:asc' ) - self.assertEqual(self.columns, columns) self.assertEqual(self.datalist, tuple(data)) @@ -720,12 +722,10 @@ class TestRemoveProjectImage(TestImage): def setUp(self): super(TestRemoveProjectImage, self).setUp() + self._image = image_fakes.FakeImage.create_one_image() # This is the return value for utils.find_resource() - self.images_mock.get.return_value = fakes.FakeResource( - None, - copy.deepcopy(image_fakes.IMAGE), - loaded=True, - ) + self.images_mock.get.return_value = self._image + self.project_mock.get.return_value = fakes.FakeResource( None, copy.deepcopy(identity_fakes.PROJECT), @@ -742,11 +742,11 @@ class TestRemoveProjectImage(TestImage): def test_remove_project_image_no_options(self): arglist = [ - image_fakes.image_id, + self._image.id, identity_fakes.project_id, ] verifylist = [ - ('image', image_fakes.image_id), + ('image', self._image.id), ('project', identity_fakes.project_id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -754,19 +754,19 @@ class TestRemoveProjectImage(TestImage): result = self.cmd.take_action(parsed_args) self.image_members_mock.delete.assert_called_with( - image_fakes.image_id, + self._image.id, identity_fakes.project_id, ) self.assertIsNone(result) def test_remove_project_image_with_options(self): arglist = [ - image_fakes.image_id, + self._image.id, identity_fakes.project_id, '--project-domain', identity_fakes.domain_id, ] verifylist = [ - ('image', image_fakes.image_id), + ('image', self._image.id), ('project', identity_fakes.project_id), ('project_domain', identity_fakes.domain_id), ] @@ -775,7 +775,7 @@ class TestRemoveProjectImage(TestImage): result = self.cmd.take_action(parsed_args) self.image_members_mock.delete.assert_called_with( - image_fakes.image_id, + self._image.id, identity_fakes.project_id, ) self.assertIsNone(result) diff --git a/openstackclient/tests/network/v2/fakes.py b/openstackclient/tests/network/v2/fakes.py index cfd0572..417cf26 100644 --- a/openstackclient/tests/network/v2/fakes.py +++ b/openstackclient/tests/network/v2/fakes.py @@ -36,6 +36,9 @@ QUOTA = { "router": 10, "rbac_policy": -1, "port": 50, + "vip": 10, + "member": 10, + "health_monitor": 10, } @@ -71,20 +74,74 @@ class TestNetworkV2(utils.TestCommand): ) +class FakeAddressScope(object): + """Fake one or more address scopes.""" + + @staticmethod + def create_one_address_scope(attrs=None): + """Create a fake address scope. + + :param Dictionary attrs: + A dictionary with all attributes + :return: + A FakeResource object with name, id, etc. + """ + attrs = attrs or {} + + # Set default attributes. + address_scope_attrs = { + 'name': 'address-scope-name-' + uuid.uuid4().hex, + 'id': 'address-scope-id-' + uuid.uuid4().hex, + 'tenant_id': 'project-id-' + uuid.uuid4().hex, + 'shared': False, + 'ip_version': 4, + } + + # Overwrite default attributes. + address_scope_attrs.update(attrs) + + address_scope = fakes.FakeResource( + info=copy.deepcopy(address_scope_attrs), + loaded=True) + + # Set attributes with special mapping in OpenStack SDK. + address_scope.project_id = address_scope_attrs['tenant_id'] + + return address_scope + + @staticmethod + def create_address_scopes(attrs=None, count=2): + """Create multiple fake address scopes. + + :param Dictionary attrs: + A dictionary with all attributes + :param int count: + The number of address scopes to fake + :return: + A list of FakeResource objects faking the address scopes + """ + address_scopes = [] + for i in range(0, count): + address_scopes.append( + FakeAddressScope.create_one_address_scope(attrs)) + + return address_scopes + + class FakeAvailabilityZone(object): """Fake one or more network availability zones (AZs).""" @staticmethod - def create_one_availability_zone(attrs={}, methods={}): + def create_one_availability_zone(attrs=None): """Create a fake AZ. :param Dictionary attrs: A dictionary with all attributes - :param Dictionary methods: - A dictionary with all methods :return: A FakeResource object with name, state, etc. """ + attrs = attrs or {} + # Set default attributes. availability_zone = { 'name': uuid.uuid4().hex, @@ -97,18 +154,15 @@ class FakeAvailabilityZone(object): availability_zone = fakes.FakeResource( info=copy.deepcopy(availability_zone), - methods=methods, loaded=True) return availability_zone @staticmethod - def create_availability_zones(attrs={}, methods={}, count=2): + def create_availability_zones(attrs=None, count=2): """Create multiple fake AZs. :param Dictionary attrs: A dictionary with all attributes - :param Dictionary methods: - A dictionary with all methods :param int count: The number of AZs to fake :return: @@ -117,8 +171,7 @@ class FakeAvailabilityZone(object): availability_zones = [] for i in range(0, count): availability_zone = \ - FakeAvailabilityZone.create_one_availability_zone( - attrs, methods) + FakeAvailabilityZone.create_one_availability_zone(attrs) availability_zones.append(availability_zone) return availability_zones @@ -128,17 +181,16 @@ class FakeNetwork(object): """Fake one or more networks.""" @staticmethod - def create_one_network(attrs={}, methods={}): + def create_one_network(attrs=None): """Create a fake network. :param Dictionary attrs: A dictionary with all attributes - :param Dictionary methods: - A dictionary with all methods :return: - A FakeResource object, with id, name, admin_state_up, - router_external, status, subnets, tenant_id + A FakeResource object, with id, name, etc. """ + attrs = attrs or {} + # Set default attributes. network_attrs = { 'id': 'network-id-' + uuid.uuid4().hex, @@ -149,41 +201,30 @@ class FakeNetwork(object): 'shared': False, 'subnets': ['a', 'b'], 'provider_network_type': 'vlan', - 'router_external': True, + 'router:external': True, 'availability_zones': [], 'availability_zone_hints': [], + 'is_default': False, } # Overwrite default attributes. network_attrs.update(attrs) - # Set default methods. - network_methods = { - 'keys': ['id', 'name', 'admin_state_up', 'router_external', - 'status', 'subnets', 'tenant_id', 'availability_zones', - 'availability_zone_hints'], - } - - # Overwrite default methods. - network_methods.update(methods) - network = fakes.FakeResource(info=copy.deepcopy(network_attrs), - methods=copy.deepcopy(network_methods), loaded=True) # Set attributes with special mapping in OpenStack SDK. network.project_id = network_attrs['tenant_id'] + network.is_router_external = network_attrs['router:external'] return network @staticmethod - def create_networks(attrs={}, methods={}, count=2): + def create_networks(attrs=None, count=2): """Create multiple fake networks. :param Dictionary attrs: A dictionary with all attributes - :param Dictionary methods: - A dictionary with all methods :param int count: The number of networks to fake :return: @@ -191,7 +232,7 @@ class FakeNetwork(object): """ networks = [] for i in range(0, count): - networks.append(FakeNetwork.create_one_network(attrs, methods)) + networks.append(FakeNetwork.create_one_network(attrs)) return networks @@ -219,16 +260,16 @@ class FakePort(object): """Fake one or more ports.""" @staticmethod - def create_one_port(attrs={}, methods={}): + def create_one_port(attrs=None): """Create a fake port. :param Dictionary attrs: A dictionary with all attributes - :param Dictionary methods: - A dictionary with all methods :return: A FakeResource object, with id, name, etc. """ + attrs = attrs or {} + # Set default attributes. port_attrs = { 'admin_state_up': True, @@ -257,23 +298,7 @@ class FakePort(object): # Overwrite default attributes. port_attrs.update(attrs) - # Set default methods. - port_methods = { - 'keys': ['admin_state_up', 'allowed_address_pairs', - 'binding:host_id', 'binding:profile', - 'binding:vif_details', 'binding:vif_type', - 'binding:vnic_type', 'device_id', 'device_owner', - 'dns_assignment', 'dns_name', 'extra_dhcp_opts', - 'fixed_ips', 'id', 'mac_address', 'name', - 'network_id', 'port_security_enabled', - 'security_groups', 'status', 'tenant_id'], - } - - # Overwrite default methods. - port_methods.update(methods) - port = fakes.FakeResource(info=copy.deepcopy(port_attrs), - methods=copy.deepcopy(port_methods), loaded=True) # Set attributes with special mappings in OpenStack SDK. @@ -287,13 +312,11 @@ class FakePort(object): return port @staticmethod - def create_ports(attrs={}, methods={}, count=2): + def create_ports(attrs=None, count=2): """Create multiple fake ports. :param Dictionary attrs: A dictionary with all attributes - :param Dictionary methods: - A dictionary with all methods :param int count: The number of ports to fake :return: @@ -301,7 +324,7 @@ class FakePort(object): """ ports = [] for i in range(0, count): - ports.append(FakePort.create_one_port(attrs, methods)) + ports.append(FakePort.create_one_port(attrs)) return ports @@ -329,17 +352,17 @@ class FakeRouter(object): """Fake one or more routers.""" @staticmethod - def create_one_router(attrs={}, methods={}): + def create_one_router(attrs=None): """Create a fake router. :param Dictionary attrs: A dictionary with all attributes - :param Dictionary methods: - A dictionary with all methods :return: A FakeResource object, with id, name, admin_state_up, status, tenant_id """ + attrs = attrs or {} + # Set default attributes. router_attrs = { 'id': 'router-id-' + uuid.uuid4().hex, @@ -358,28 +381,20 @@ class FakeRouter(object): # Overwrite default attributes. router_attrs.update(attrs) - # Set default methods. - router_methods = { - 'keys': ['id', 'name', 'admin_state_up', 'distributed', 'ha', - 'tenant_id'], - } - - # Overwrite default methods. - router_methods.update(methods) - router = fakes.FakeResource(info=copy.deepcopy(router_attrs), - methods=copy.deepcopy(router_methods), loaded=True) + + # Set attributes with special mapping in OpenStack SDK. + router.project_id = router_attrs['tenant_id'] + return router @staticmethod - def create_routers(attrs={}, methods={}, count=2): + def create_routers(attrs=None, count=2): """Create multiple fake routers. :param Dictionary attrs: A dictionary with all attributes - :param Dictionary methods: - A dictionary with all methods :param int count: The number of routers to fake :return: @@ -387,7 +402,7 @@ class FakeRouter(object): """ routers = [] for i in range(0, count): - routers.append(FakeRouter.create_one_router(attrs, methods)) + routers.append(FakeRouter.create_one_router(attrs)) return routers @@ -415,16 +430,16 @@ class FakeSecurityGroup(object): """Fake one or more security groups.""" @staticmethod - def create_one_security_group(attrs={}, methods={}): + def create_one_security_group(attrs=None): """Create a fake security group. :param Dictionary attrs: A dictionary with all attributes - :param Dictionary methods: - A dictionary with all methods :return: A FakeResource object, with id, name, etc. """ + attrs = attrs or {} + # Set default attributes. security_group_attrs = { 'id': 'security-group-id-' + uuid.uuid4().hex, @@ -437,26 +452,21 @@ class FakeSecurityGroup(object): # Overwrite default attributes. security_group_attrs.update(attrs) - # Set default methods. - security_group_methods = {} - - # Overwrite default methods. - security_group_methods.update(methods) - security_group = fakes.FakeResource( info=copy.deepcopy(security_group_attrs), - methods=copy.deepcopy(security_group_methods), loaded=True) + + # Set attributes with special mapping in OpenStack SDK. + security_group.project_id = security_group_attrs['tenant_id'] + return security_group @staticmethod - def create_security_groups(attrs={}, methods={}, count=2): + def create_security_groups(attrs=None, count=2): """Create multiple fake security groups. :param Dictionary attrs: A dictionary with all attributes - :param Dictionary methods: - A dictionary with all methods :param int count: The number of security groups to fake :return: @@ -465,7 +475,7 @@ class FakeSecurityGroup(object): security_groups = [] for i in range(0, count): security_groups.append( - FakeSecurityGroup.create_one_security_group(attrs, methods)) + FakeSecurityGroup.create_one_security_group(attrs)) return security_groups @@ -474,16 +484,16 @@ class FakeSecurityGroupRule(object): """Fake one or more security group rules.""" @staticmethod - def create_one_security_group_rule(attrs={}, methods={}): + def create_one_security_group_rule(attrs=None): """Create a fake security group rule. :param Dictionary attrs: A dictionary with all attributes - :param Dictionary methods: - A dictionary with all methods :return: A FakeResource object, with id, etc. """ + attrs = attrs or {} + # Set default attributes. security_group_rule_attrs = { 'direction': 'ingress', @@ -491,9 +501,9 @@ class FakeSecurityGroupRule(object): 'id': 'security-group-rule-id-' + uuid.uuid4().hex, 'port_range_max': None, 'port_range_min': None, - 'protocol': None, - 'remote_group_id': 'remote-security-group-id-' + uuid.uuid4().hex, - 'remote_ip_prefix': None, + 'protocol': 'tcp', + 'remote_group_id': None, + 'remote_ip_prefix': '0.0.0.0/0', 'security_group_id': 'security-group-id-' + uuid.uuid4().hex, 'tenant_id': 'project-id-' + uuid.uuid4().hex, } @@ -501,34 +511,21 @@ class FakeSecurityGroupRule(object): # Overwrite default attributes. security_group_rule_attrs.update(attrs) - # Set default methods. - security_group_rule_methods = { - 'keys': ['direction', 'ethertype', 'id', 'port_range_max', - 'port_range_min', 'protocol', 'remote_group_id', - 'remote_ip_prefix', 'security_group_id', 'tenant_id'], - } - - # Overwrite default methods. - security_group_rule_methods.update(methods) - security_group_rule = fakes.FakeResource( info=copy.deepcopy(security_group_rule_attrs), - methods=copy.deepcopy(security_group_rule_methods), loaded=True) - # Set attributes with special mappings. + # Set attributes with special mapping in OpenStack SDK. security_group_rule.project_id = security_group_rule_attrs['tenant_id'] return security_group_rule @staticmethod - def create_security_group_rules(attrs={}, methods={}, count=2): + def create_security_group_rules(attrs=None, count=2): """Create multiple fake security group rules. :param Dictionary attrs: A dictionary with all attributes - :param Dictionary methods: - A dictionary with all methods :param int count: The number of security group rules to fake :return: @@ -537,8 +534,7 @@ class FakeSecurityGroupRule(object): security_group_rules = [] for i in range(0, count): security_group_rules.append( - FakeSecurityGroupRule.create_one_security_group_rule( - attrs, methods)) + FakeSecurityGroupRule.create_one_security_group_rule(attrs)) return security_group_rules @@ -547,16 +543,16 @@ class FakeSubnet(object): """Fake one or more subnets.""" @staticmethod - def create_one_subnet(attrs={}, methods={}): + def create_one_subnet(attrs=None): """Create a fake subnet. :param Dictionary attrs: A dictionary with all attributes - :param Dictionary methods: - A dictionary with all methods :return: A FakeResource object faking the subnet """ + attrs = attrs or {} + # Set default attributes. project_id = 'project-id-' + uuid.uuid4().hex subnet_attrs = { @@ -569,7 +565,7 @@ class FakeSubnet(object): 'dns_nameservers': [], 'allocation_pools': [], 'host_routes': [], - 'ip_version': '4', + 'ip_version': 4, 'gateway_ip': '10.10.10.1', 'ipv6_address_mode': 'None', 'ipv6_ra_mode': 'None', @@ -579,33 +575,20 @@ class FakeSubnet(object): # Overwrite default attributes. subnet_attrs.update(attrs) - # Set default methods. - subnet_methods = { - 'keys': ['id', 'name', 'network_id', 'cidr', 'enable_dhcp', - 'allocation_pools', 'dns_nameservers', 'gateway_ip', - 'host_routes', 'ip_version', 'tenant_id', - 'ipv6_address_mode', 'ipv6_ra_mode', 'subnetpool_id'] - } - - # Overwrite default methods. - subnet_methods.update(methods) - subnet = fakes.FakeResource(info=copy.deepcopy(subnet_attrs), - methods=copy.deepcopy(subnet_methods), loaded=True) + # Set attributes with special mappings in OpenStack SDK. subnet.project_id = subnet_attrs['tenant_id'] return subnet @staticmethod - def create_subnets(attrs={}, methods={}, count=2): + def create_subnets(attrs=None, count=2): """Create multiple fake subnets. :param Dictionary attrs: A dictionary with all attributes - :param Dictionary methods: - A dictionary with all methods :param int count: The number of subnets to fake :return: @@ -613,7 +596,7 @@ class FakeSubnet(object): """ subnets = [] for i in range(0, count): - subnets.append(FakeSubnet.create_one_subnet(attrs, methods)) + subnets.append(FakeSubnet.create_one_subnet(attrs)) return subnets @@ -622,16 +605,16 @@ class FakeFloatingIP(object): """Fake one or more floating ip.""" @staticmethod - def create_one_floating_ip(attrs={}, methods={}): + def create_one_floating_ip(attrs=None): """Create a fake floating ip. :param Dictionary attrs: A dictionary with all attributes - :param Dictionary methods: - A dictionary with all methods :return: A FakeResource object, with id, ip, and so on """ + attrs = attrs or {} + # Set default attributes. floating_ip_attrs = { 'id': 'floating-ip-id-' + uuid.uuid4().hex, @@ -649,19 +632,8 @@ class FakeFloatingIP(object): # Overwrite default attributes. floating_ip_attrs.update(attrs) - # Set default methods. - floating_ip_methods = { - 'keys': ['id', 'floating_ip_address', 'fixed_ip_address', - 'dns_domain', 'dns_name', 'status', 'router_id', - 'floating_network_id', 'port_id', 'tenant_id'] - } - - # Overwrite default methods. - floating_ip_methods.update(methods) - floating_ip = fakes.FakeResource( info=copy.deepcopy(floating_ip_attrs), - methods=copy.deepcopy(floating_ip_methods), loaded=True ) @@ -671,13 +643,11 @@ class FakeFloatingIP(object): return floating_ip @staticmethod - def create_floating_ips(attrs={}, methods={}, count=2): + def create_floating_ips(attrs=None, count=2): """Create multiple fake floating ips. :param Dictionary attrs: A dictionary with all attributes - :param Dictionary methods: - A dictionary with all methods :param int count: The number of floating ips to fake :return: @@ -685,10 +655,7 @@ class FakeFloatingIP(object): """ floating_ips = [] for i in range(0, count): - floating_ips.append(FakeFloatingIP.create_one_floating_ip( - attrs, - methods - )) + floating_ips.append(FakeFloatingIP.create_one_floating_ip(attrs)) return floating_ips @staticmethod @@ -715,49 +682,37 @@ class FakeSubnetPool(object): """Fake one or more subnet pools.""" @staticmethod - def create_one_subnet_pool(attrs={}, methods={}): + def create_one_subnet_pool(attrs=None): """Create a fake subnet pool. :param Dictionary attrs: A dictionary with all attributes - :param Dictionary methods: - A dictionary with all methods :return: A FakeResource object faking the subnet pool """ + attrs = attrs or {} + # Set default attributes. subnet_pool_attrs = { 'id': 'subnet-pool-id-' + uuid.uuid4().hex, 'name': 'subnet-pool-name-' + uuid.uuid4().hex, 'prefixes': ['10.0.0.0/24', '10.1.0.0/24'], - 'default_prefixlen': 8, + 'default_prefixlen': '8', 'address_scope_id': 'address-scope-id-' + uuid.uuid4().hex, 'tenant_id': 'project-id-' + uuid.uuid4().hex, 'is_default': False, 'shared': False, - 'max_prefixlen': 32, - 'min_prefixlen': 8, + 'max_prefixlen': '32', + 'min_prefixlen': '8', 'default_quota': None, - 'ip_version': 4, + 'ip_version': '4', } # Overwrite default attributes. subnet_pool_attrs.update(attrs) - # Set default methods. - subnet_pool_methods = { - 'keys': ['id', 'name', 'prefixes', 'default_prefixlen', - 'address_scope_id', 'tenant_id', 'is_default', - 'shared', 'max_prefixlen', 'min_prefixlen', - 'default_quota', 'ip_version'] - } - - # Overwrite default methods. - subnet_pool_methods.update(methods) - subnet_pool = fakes.FakeResource( info=copy.deepcopy(subnet_pool_attrs), - methods=copy.deepcopy(subnet_pool_methods), loaded=True ) @@ -767,13 +722,11 @@ class FakeSubnetPool(object): return subnet_pool @staticmethod - def create_subnet_pools(attrs={}, methods={}, count=2): + def create_subnet_pools(attrs=None, count=2): """Create multiple fake subnet pools. :param Dictionary attrs: A dictionary with all attributes - :param Dictionary methods: - A dictionary with all methods :param int count: The number of subnet pools to fake :return: @@ -782,7 +735,7 @@ class FakeSubnetPool(object): subnet_pools = [] for i in range(0, count): subnet_pools.append( - FakeSubnetPool.create_one_subnet_pool(attrs, methods) + FakeSubnetPool.create_one_subnet_pool(attrs) ) return subnet_pools diff --git a/openstackclient/tests/network/v2/test_address_scope.py b/openstackclient/tests/network/v2/test_address_scope.py new file mode 100644 index 0000000..ac94b48 --- /dev/null +++ b/openstackclient/tests/network/v2/test_address_scope.py @@ -0,0 +1,357 @@ +# 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 copy +import mock + +from openstackclient.common import exceptions +from openstackclient.network.v2 import address_scope +from openstackclient.tests import fakes +from openstackclient.tests.identity.v3 import fakes as identity_fakes_v3 +from openstackclient.tests.network.v2 import fakes as network_fakes +from openstackclient.tests import utils as tests_utils + + +class TestAddressScope(network_fakes.TestNetworkV2): + + def setUp(self): + super(TestAddressScope, self).setUp() + + # Get a shortcut to the network client + self.network = self.app.client_manager.network + + +class TestCreateAddressScope(TestAddressScope): + + # The new address scope created. + new_address_scope = ( + network_fakes.FakeAddressScope.create_one_address_scope( + attrs={ + 'tenant_id': identity_fakes_v3.project_id, + } + )) + columns = ( + 'id', + 'ip_version', + 'name', + 'project_id', + 'shared' + ) + data = ( + new_address_scope.id, + new_address_scope.ip_version, + new_address_scope.name, + new_address_scope.project_id, + new_address_scope.shared, + ) + + def setUp(self): + super(TestCreateAddressScope, self).setUp() + self.network.create_address_scope = mock.Mock( + return_value=self.new_address_scope) + + # Get the command object to test + self.cmd = address_scope.CreateAddressScope(self.app, self.namespace) + + # Set identity client v3. And get a shortcut to Identity client. + identity_client = identity_fakes_v3.FakeIdentityv3Client( + endpoint=fakes.AUTH_URL, + token=fakes.AUTH_TOKEN, + ) + self.app.client_manager.identity = identity_client + self.identity = self.app.client_manager.identity + + # Get a shortcut to the ProjectManager Mock + self.projects_mock = self.identity.projects + self.projects_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes_v3.PROJECT), + loaded=True, + ) + + # Get a shortcut to the DomainManager Mock + self.domains_mock = self.identity.domains + self.domains_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes_v3.DOMAIN), + loaded=True, + ) + + 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 = [ + self.new_address_scope.name, + ] + verifylist = [ + ('project', None), + ('ip_version', self.new_address_scope.ip_version), + ('name', self.new_address_scope.name), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = (self.cmd.take_action(parsed_args)) + + self.network.create_address_scope.assert_called_once_with(**{ + 'ip_version': self.new_address_scope.ip_version, + 'name': self.new_address_scope.name, + }) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + def test_create_all_options(self): + arglist = [ + '--ip-version', str(self.new_address_scope.ip_version), + '--share', + '--project', identity_fakes_v3.project_name, + '--project-domain', identity_fakes_v3.domain_name, + self.new_address_scope.name, + ] + verifylist = [ + ('ip_version', self.new_address_scope.ip_version), + ('share', True), + ('project', identity_fakes_v3.project_name), + ('project_domain', identity_fakes_v3.domain_name), + ('name', self.new_address_scope.name), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = (self.cmd.take_action(parsed_args)) + + self.network.create_address_scope.assert_called_once_with(**{ + 'ip_version': self.new_address_scope.ip_version, + 'shared': True, + 'tenant_id': identity_fakes_v3.project_id, + 'name': self.new_address_scope.name, + }) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + def test_create_no_share(self): + arglist = [ + '--no-share', + self.new_address_scope.name, + ] + verifylist = [ + ('no_share', True), + ('name', self.new_address_scope.name), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.network.create_address_scope.assert_called_once_with(**{ + 'ip_version': self.new_address_scope.ip_version, + 'shared': False, + 'name': self.new_address_scope.name, + }) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + +class TestDeleteAddressScope(TestAddressScope): + + # The address scope to delete. + _address_scope = ( + network_fakes.FakeAddressScope.create_one_address_scope()) + + def setUp(self): + super(TestDeleteAddressScope, self).setUp() + self.network.delete_address_scope = mock.Mock(return_value=None) + self.network.find_address_scope = mock.Mock( + return_value=self._address_scope) + + # Get the command object to test + self.cmd = address_scope.DeleteAddressScope(self.app, self.namespace) + + def test_delete(self): + arglist = [ + self._address_scope.name, + ] + verifylist = [ + ('address_scope', self._address_scope.name), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + self.network.delete_address_scope.assert_called_once_with( + self._address_scope) + self.assertIsNone(result) + + +class TestListAddressScope(TestAddressScope): + + # The address scopes to list up. + address_scopes = ( + network_fakes.FakeAddressScope.create_address_scopes(count=3)) + columns = ( + 'ID', + 'Name', + 'IP Version', + 'Shared', + 'Project', + ) + data = [] + for scope in address_scopes: + data.append(( + scope.id, + scope.name, + scope.ip_version, + scope.shared, + scope.project_id, + )) + + def setUp(self): + super(TestListAddressScope, self).setUp() + self.network.address_scopes = mock.Mock( + return_value=self.address_scopes) + + # Get the command object to test + self.cmd = address_scope.ListAddressScope(self.app, self.namespace) + + def test_address_scope_list(self): + arglist = [] + verifylist = [] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.network.address_scopes.assert_called_once_with(**{}) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, list(data)) + + +class TestSetAddressScope(TestAddressScope): + + # The address scope to set. + _address_scope = network_fakes.FakeAddressScope.create_one_address_scope() + + def setUp(self): + super(TestSetAddressScope, self).setUp() + self.network.update_address_scope = mock.Mock(return_value=None) + self.network.find_address_scope = mock.Mock( + return_value=self._address_scope) + + # Get the command object to test + self.cmd = address_scope.SetAddressScope(self.app, self.namespace) + + def test_set_nothing(self): + arglist = [self._address_scope.name, ] + verifylist = [ + ('address_scope', self._address_scope.name), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.assertRaises(exceptions.CommandError, self.cmd.take_action, + parsed_args) + + def test_set_name_and_share(self): + arglist = [ + '--name', 'new_address_scope', + '--share', + self._address_scope.name, + ] + verifylist = [ + ('name', 'new_address_scope'), + ('share', True), + ('address_scope', self._address_scope.name), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + attrs = { + 'name': "new_address_scope", + 'shared': True, + } + self.network.update_address_scope.assert_called_with( + self._address_scope, **attrs) + self.assertIsNone(result) + + def test_set_no_share(self): + arglist = [ + '--no-share', + self._address_scope.name, + ] + verifylist = [ + ('no_share', True), + ('address_scope', self._address_scope.name), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + attrs = { + 'shared': False, + } + self.network.update_address_scope.assert_called_with( + self._address_scope, **attrs) + self.assertIsNone(result) + + +class TestShowAddressScope(TestAddressScope): + + # The address scope to show. + _address_scope = ( + network_fakes.FakeAddressScope.create_one_address_scope()) + columns = ( + 'id', + 'ip_version', + 'name', + 'project_id', + 'shared', + ) + data = ( + _address_scope.id, + _address_scope.ip_version, + _address_scope.name, + _address_scope.project_id, + _address_scope.shared, + ) + + def setUp(self): + super(TestShowAddressScope, self).setUp() + self.network.find_address_scope = mock.Mock( + return_value=self._address_scope) + + # Get the command object to test + self.cmd = address_scope.ShowAddressScope(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._address_scope.name, + ] + verifylist = [ + ('address_scope', self._address_scope.name), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.network.find_address_scope.assert_called_once_with( + self._address_scope.name, ignore_missing=False) + self.assertEqual(self.columns, columns) + self.assertEqual(list(self.data), list(data)) diff --git a/openstackclient/tests/network/v2/test_floating_ip.py b/openstackclient/tests/network/v2/test_floating_ip.py index 1c1088a..f9ccfe1 100644 --- a/openstackclient/tests/network/v2/test_floating_ip.py +++ b/openstackclient/tests/network/v2/test_floating_ip.py @@ -16,6 +16,7 @@ import mock from openstackclient.network.v2 import floating_ip from openstackclient.tests.compute.v2 import fakes as compute_fakes from openstackclient.tests.network.v2 import fakes as network_fakes +from openstackclient.tests import utils as tests_utils # Tests for Neutron network @@ -29,6 +30,114 @@ class TestFloatingIPNetwork(network_fakes.TestNetworkV2): self.network = self.app.client_manager.network +class TestCreateFloatingIPNetwork(TestFloatingIPNetwork): + + # Fake data for option tests. + floating_network = network_fakes.FakeNetwork.create_one_network() + subnet = network_fakes.FakeSubnet.create_one_subnet() + port = network_fakes.FakePort.create_one_port() + + # The floating ip to be deleted. + floating_ip = network_fakes.FakeFloatingIP.create_one_floating_ip( + attrs={ + 'floating_network_id': floating_network.id, + 'port_id': port.id, + } + ) + + columns = ( + 'dns_domain', + 'dns_name', + 'fixed_ip_address', + 'floating_ip_address', + 'floating_network_id', + 'id', + 'port_id', + 'project_id', + 'router_id', + 'status', + ) + + data = ( + floating_ip.dns_domain, + floating_ip.dns_name, + floating_ip.fixed_ip_address, + floating_ip.floating_ip_address, + floating_ip.floating_network_id, + floating_ip.id, + floating_ip.port_id, + floating_ip.project_id, + floating_ip.router_id, + floating_ip.status, + ) + + def setUp(self): + super(TestCreateFloatingIPNetwork, self).setUp() + + self.network.create_ip = mock.Mock(return_value=self.floating_ip) + + self.network.find_network = mock.Mock( + return_value=self.floating_network) + self.network.find_subnet = mock.Mock(return_value=self.subnet) + self.network.find_port = mock.Mock(return_value=self.port) + + # Get the command object to test + self.cmd = floating_ip.CreateFloatingIP(self.app, self.namespace) + + def test_create_no_options(self): + arglist = [] + verifylist = [] + + self.assertRaises(tests_utils.ParserException, self.check_parser, + self.cmd, arglist, verifylist) + + def test_create_default_options(self): + arglist = [ + self.floating_ip.floating_network_id, + ] + verifylist = [ + ('network', self.floating_ip.floating_network_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.network.create_ip.assert_called_once_with(**{ + 'floating_network_id': self.floating_ip.floating_network_id, + }) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + def test_create_all_options(self): + arglist = [ + '--subnet', self.subnet.id, + '--port', self.floating_ip.port_id, + '--floating-ip-address', self.floating_ip.floating_ip_address, + '--fixed-ip-address', self.floating_ip.fixed_ip_address, + self.floating_ip.floating_network_id, + ] + verifylist = [ + ('subnet', self.subnet.id), + ('port', self.floating_ip.port_id), + ('floating_ip_address', self.floating_ip.floating_ip_address), + ('fixed_ip_address', self.floating_ip.fixed_ip_address), + ('network', self.floating_ip.floating_network_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.network.create_ip.assert_called_once_with(**{ + 'subnet_id': self.subnet.id, + 'port_id': self.floating_ip.port_id, + 'floating_ip_address': self.floating_ip.floating_ip_address, + 'fixed_ip_address': self.floating_ip.fixed_ip_address, + 'floating_network_id': self.floating_ip.floating_network_id, + }) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + class TestDeleteFloatingIPNetwork(TestFloatingIPNetwork): # The floating ip to be deleted. @@ -54,8 +163,8 @@ class TestDeleteFloatingIPNetwork(TestFloatingIPNetwork): result = self.cmd.take_action(parsed_args) - self.network.find_ip.assert_called_with(self.floating_ip.id) - self.network.delete_ip.assert_called_with(self.floating_ip) + self.network.find_ip.assert_called_once_with(self.floating_ip.id) + self.network.delete_ip.assert_called_once_with(self.floating_ip) self.assertIsNone(result) @@ -95,7 +204,7 @@ class TestListFloatingIPNetwork(TestFloatingIPNetwork): columns, data = self.cmd.take_action(parsed_args) - self.network.ips.assert_called_with(**{}) + self.network.ips.assert_called_once_with(**{}) self.assertEqual(self.columns, columns) self.assertEqual(self.data, list(data)) @@ -150,7 +259,7 @@ class TestShowFloatingIPNetwork(TestFloatingIPNetwork): columns, data = self.cmd.take_action(parsed_args) - self.network.find_ip.assert_called_with( + self.network.find_ip.assert_called_once_with( self.floating_ip.id, ignore_missing=False ) @@ -169,6 +278,61 @@ class TestFloatingIPCompute(compute_fakes.TestComputev2): self.compute = self.app.client_manager.compute +class TestCreateFloatingIPCompute(TestFloatingIPCompute): + + # The floating ip to be deleted. + floating_ip = compute_fakes.FakeFloatingIP.create_one_floating_ip() + + columns = ( + 'fixed_ip', + 'id', + 'instance_id', + 'ip', + 'pool', + ) + + data = ( + floating_ip.fixed_ip, + floating_ip.id, + floating_ip.instance_id, + floating_ip.ip, + floating_ip.pool, + ) + + def setUp(self): + super(TestCreateFloatingIPCompute, self).setUp() + + self.app.client_manager.network_endpoint_enabled = False + + self.compute.floating_ips.create.return_value = self.floating_ip + + # Get the command object to test + self.cmd = floating_ip.CreateFloatingIP(self.app, None) + + def test_create_no_options(self): + arglist = [] + verifylist = [] + + self.assertRaises(tests_utils.ParserException, self.check_parser, + self.cmd, arglist, verifylist) + + def test_create_default_options(self): + arglist = [ + self.floating_ip.pool, + ] + verifylist = [ + ('network', self.floating_ip.pool), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.compute.floating_ips.create.assert_called_once_with( + self.floating_ip.pool) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + class TestDeleteFloatingIPCompute(TestFloatingIPCompute): # The floating ip to be deleted. @@ -198,7 +362,7 @@ class TestDeleteFloatingIPCompute(TestFloatingIPCompute): result = self.cmd.take_action(parsed_args) - self.compute.floating_ips.delete.assert_called_with( + self.compute.floating_ips.delete.assert_called_once_with( self.floating_ip.id ) self.assertIsNone(result) @@ -244,7 +408,7 @@ class TestListFloatingIPCompute(TestFloatingIPCompute): columns, data = self.cmd.take_action(parsed_args) - self.compute.floating_ips.list.assert_called_with() + self.compute.floating_ips.list.assert_called_once_with() self.assertEqual(self.columns, columns) self.assertEqual(self.data, list(data)) diff --git a/openstackclient/tests/network/v2/test_network.py b/openstackclient/tests/network/v2/test_network.py index e70a66c..ba810f1 100644 --- a/openstackclient/tests/network/v2/test_network.py +++ b/openstackclient/tests/network/v2/test_network.py @@ -14,6 +14,7 @@ import copy import mock +from mock import call from openstackclient.common import exceptions from openstackclient.common import utils from openstackclient.network.v2 import network @@ -51,9 +52,12 @@ class TestCreateNetworkIdentityV3(TestNetwork): 'availability_zone_hints', 'availability_zones', 'id', + 'is_default', 'name', 'project_id', - 'router_external', + 'provider_network_type', + 'router:external', + 'shared', 'status', 'subnets', ) @@ -63,9 +67,12 @@ class TestCreateNetworkIdentityV3(TestNetwork): utils.format_list(_network.availability_zone_hints), utils.format_list(_network.availability_zones), _network.id, + _network.is_default, _network.name, _network.project_id, - network._format_router_external(_network.router_external), + _network.provider_network_type, + network._format_router_external(_network.is_router_external), + _network.shared, _network.status, utils.format_list(_network.subnets), ) @@ -106,7 +113,6 @@ class TestCreateNetworkIdentityV3(TestNetwork): arglist = [] verifylist = [] - # Missing required args should bail here self.assertRaises(tests_utils.ParserException, self.check_parser, self.cmd, arglist, verifylist) @@ -116,15 +122,16 @@ class TestCreateNetworkIdentityV3(TestNetwork): ] verifylist = [ ('name', self._network.name), - ('admin_state', True), - ('shared', None), + ('enable', True), + ('share', None), ('project', None), + ('external', False), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.network.create_network.assert_called_with(**{ + self.network.create_network.assert_called_once_with(**{ 'admin_state_up': True, 'name': self._network.name, }) @@ -138,26 +145,43 @@ class TestCreateNetworkIdentityV3(TestNetwork): "--project", identity_fakes_v3.project_name, "--project-domain", identity_fakes_v3.domain_name, "--availability-zone-hint", "nova", + "--external", "--default", + "--provider-network-type", "vlan", + "--provider-physical-network", "physnet1", + "--provider-segment", "400", + "--transparent-vlan", self._network.name, ] verifylist = [ - ('admin_state', False), - ('shared', True), + ('disable', True), + ('share', True), ('project', identity_fakes_v3.project_name), ('project_domain', identity_fakes_v3.domain_name), ('availability_zone_hints', ["nova"]), + ('external', True), + ('default', True), + ('provider_network_type', 'vlan'), + ('physical_network', 'physnet1'), + ('segmentation_id', '400'), + ('transparent_vlan', True), ('name', self._network.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = (self.cmd.take_action(parsed_args)) - self.network.create_network.assert_called_with(**{ + self.network.create_network.assert_called_once_with(**{ 'admin_state_up': False, 'availability_zone_hints': ["nova"], 'name': self._network.name, 'shared': True, 'tenant_id': identity_fakes_v3.project_id, + 'is_default': True, + 'router:external': True, + 'provider:network_type': 'vlan', + 'provider:physical_network': 'physnet1', + 'provider:segmentation_id': '400', + 'vlan_transparent': True, }) self.assertEqual(self.columns, columns) self.assertEqual(self.data, data) @@ -169,15 +193,16 @@ class TestCreateNetworkIdentityV3(TestNetwork): self._network.name, ] verifylist = [ - ('admin_state', True), - ('shared', False), + ('enable', True), + ('no_share', True), ('name', self._network.name), + ('external', False), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.network.create_network.assert_called_with(**{ + self.network.create_network.assert_called_once_with(**{ 'admin_state_up': True, 'name': self._network.name, 'shared': False, @@ -198,9 +223,12 @@ class TestCreateNetworkIdentityV2(TestNetwork): 'availability_zone_hints', 'availability_zones', 'id', + 'is_default', 'name', 'project_id', - 'router_external', + 'provider_network_type', + 'router:external', + 'shared', 'status', 'subnets', ) @@ -210,9 +238,12 @@ class TestCreateNetworkIdentityV2(TestNetwork): utils.format_list(_network.availability_zone_hints), utils.format_list(_network.availability_zones), _network.id, + _network.is_default, _network.name, _network.project_id, - network._format_router_external(_network.router_external), + _network.provider_network_type, + network._format_router_external(_network.is_router_external), + _network.shared, _network.status, utils.format_list(_network.subnets), ) @@ -249,16 +280,17 @@ class TestCreateNetworkIdentityV2(TestNetwork): self._network.name, ] verifylist = [ - ('admin_state', True), - ('shared', None), + ('enable', True), + ('share', None), ('name', self._network.name), ('project', identity_fakes_v2.project_name), + ('external', False), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.network.create_network.assert_called_with(**{ + self.network.create_network.assert_called_once_with(**{ 'admin_state_up': True, 'name': self._network.name, 'tenant_id': identity_fakes_v2.project_id, @@ -273,11 +305,12 @@ class TestCreateNetworkIdentityV2(TestNetwork): self._network.name, ] verifylist = [ - ('admin_state', True), - ('shared', None), + ('enable', True), + ('share', None), ('project', identity_fakes_v3.project_name), ('project_domain', identity_fakes_v3.domain_name), ('name', self._network.name), + ('external', False), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -291,33 +324,88 @@ class TestCreateNetworkIdentityV2(TestNetwork): class TestDeleteNetwork(TestNetwork): - # The network to delete. - _network = network_fakes.FakeNetwork.create_one_network() - def setUp(self): super(TestDeleteNetwork, self).setUp() + # The networks to delete + self._networks = network_fakes.FakeNetwork.create_networks(count=3) + self.network.delete_network = mock.Mock(return_value=None) - self.network.find_network = mock.Mock(return_value=self._network) + self.network.find_network = network_fakes.FakeNetwork.get_networks( + networks=self._networks) # Get the command object to test self.cmd = network.DeleteNetwork(self.app, self.namespace) - def test_delete(self): + def test_delete_one_network(self): arglist = [ - self._network.name, + self._networks[0].name, ] verifylist = [ - ('network', [self._network.name]), + ('network', [self._networks[0].name]), ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + self.network.delete_network.assert_called_once_with(self._networks[0]) + self.assertIsNone(result) + + def test_delete_multiple_networks(self): + arglist = [] + for n in self._networks: + arglist.append(n.id) + verifylist = [ + ('network', arglist), + ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) - self.network.delete_network.assert_called_with(self._network) + calls = [] + for n in self._networks: + calls.append(call(n)) + self.network.delete_network.assert_has_calls(calls) self.assertIsNone(result) + def test_delete_multiple_networks_exception(self): + arglist = [ + self._networks[0].id, + 'xxxx-yyyy-zzzz', + self._networks[1].id, + ] + verifylist = [ + ('network', arglist), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # Fake exception in find_network() + ret_find = [ + self._networks[0], + exceptions.NotFound('404'), + self._networks[1], + ] + self.network.find_network = mock.Mock(side_effect=ret_find) + + # Fake exception in delete_network() + ret_delete = [ + None, + exceptions.NotFound('404'), + ] + self.network.delete_network = mock.Mock(side_effect=ret_delete) + + self.assertRaises(exceptions.CommandError, self.cmd.take_action, + parsed_args) + + # The second call of find_network() should fail. So delete_network() + # was only called twice. + calls = [ + call(self._networks[0]), + call(self._networks[1]), + ] + self.network.delete_network.assert_has_calls(calls) + class TestListNetwork(TestNetwork): @@ -361,7 +449,7 @@ class TestListNetwork(TestNetwork): net.shared, utils.format_list(net.subnets), net.provider_network_type, - network._format_router_external(net.router_external), + network._format_router_external(net.is_router_external), utils.format_list(net.availability_zones), )) @@ -386,7 +474,7 @@ class TestListNetwork(TestNetwork): # containing the data to be listed. columns, data = self.cmd.take_action(parsed_args) - self.network.networks.assert_called_with() + self.network.networks.assert_called_once_with() self.assertEqual(self.columns, columns) self.assertEqual(self.data, list(data)) @@ -405,7 +493,7 @@ class TestListNetwork(TestNetwork): # containing the data to be listed. columns, data = self.cmd.take_action(parsed_args) - self.network.networks.assert_called_with( + self.network.networks.assert_called_once_with( **{'router:external': True} ) self.assertEqual(self.columns, columns) @@ -426,7 +514,7 @@ class TestListNetwork(TestNetwork): # containing the data to be listed. columns, data = self.cmd.take_action(parsed_args) - self.network.networks.assert_called_with() + self.network.networks.assert_called_once_with() self.assertEqual(self.columns_long, columns) self.assertEqual(self.data_long, list(data)) @@ -452,12 +540,24 @@ class TestSetNetwork(TestNetwork): '--enable', '--name', 'noob', '--share', + '--external', + '--default', + '--provider-network-type', 'vlan', + '--provider-physical-network', 'physnet1', + '--provider-segment', '400', + '--no-transparent-vlan', ] verifylist = [ ('network', self._network.name), - ('admin_state', True), + ('enable', True), ('name', 'noob'), - ('shared', True), + ('share', True), + ('external', True), + ('default', True), + ('provider_network_type', 'vlan'), + ('physical_network', 'physnet1'), + ('segmentation_id', '400'), + ('no_transparent_vlan', True), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -467,8 +567,15 @@ class TestSetNetwork(TestNetwork): 'name': 'noob', 'admin_state_up': True, 'shared': True, + 'router:external': True, + 'is_default': True, + 'provider:network_type': 'vlan', + 'provider:physical_network': 'physnet1', + 'provider:segmentation_id': '400', + 'vlan_transparent': False, } - self.network.update_network.assert_called_with(self._network, **attrs) + self.network.update_network.assert_called_once_with( + self._network, **attrs) self.assertIsNone(result) def test_set_that(self): @@ -476,11 +583,13 @@ class TestSetNetwork(TestNetwork): self._network.name, '--disable', '--no-share', + '--internal', ] verifylist = [ ('network', self._network.name), - ('admin_state', False), - ('shared', False), + ('disable', True), + ('no_share', True), + ('internal', True), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -489,8 +598,10 @@ class TestSetNetwork(TestNetwork): attrs = { 'admin_state_up': False, 'shared': False, + 'router:external': False, } - self.network.update_network.assert_called_with(self._network, **attrs) + self.network.update_network.assert_called_once_with( + self._network, **attrs) self.assertIsNone(result) def test_set_nothing(self): @@ -512,9 +623,12 @@ class TestShowNetwork(TestNetwork): 'availability_zone_hints', 'availability_zones', 'id', + 'is_default', 'name', 'project_id', - 'router_external', + 'provider_network_type', + 'router:external', + 'shared', 'status', 'subnets', ) @@ -524,9 +638,12 @@ class TestShowNetwork(TestNetwork): utils.format_list(_network.availability_zone_hints), utils.format_list(_network.availability_zones), _network.id, + _network.is_default, _network.name, _network.project_id, - network._format_router_external(_network.router_external), + _network.provider_network_type, + network._format_router_external(_network.is_router_external), + _network.shared, _network.status, utils.format_list(_network.subnets), ) @@ -543,7 +660,6 @@ class TestShowNetwork(TestNetwork): arglist = [] verifylist = [] - # Missing required args should bail here self.assertRaises(tests_utils.ParserException, self.check_parser, self.cmd, arglist, verifylist) @@ -558,11 +674,11 @@ class TestShowNetwork(TestNetwork): parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.network.find_network.assert_called_with(self._network.name, - ignore_missing=False) + self.network.find_network.assert_called_once_with( + self._network.name, ignore_missing=False) - self.assertEqual(tuple(self.columns), columns) - self.assertEqual(list(self.data), list(data)) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) # Tests for Nova network @@ -682,7 +798,7 @@ class TestCreateNetworkCompute(TestNetworkCompute): columns, data = self.cmd.take_action(parsed_args) - self.compute.networks.create.assert_called_with(**{ + self.compute.networks.create.assert_called_once_with(**{ 'cidr': self._network.cidr, 'label': self._network.label, }) @@ -692,36 +808,97 @@ class TestCreateNetworkCompute(TestNetworkCompute): class TestDeleteNetworkCompute(TestNetworkCompute): - # The network to delete. - _network = compute_fakes.FakeNetwork.create_one_network() - def setUp(self): super(TestDeleteNetworkCompute, self).setUp() self.app.client_manager.network_endpoint_enabled = False + # The networks to delete + self._networks = compute_fakes.FakeNetwork.create_networks(count=3) + self.compute.networks.delete.return_value = None # Return value of utils.find_resource() - self.compute.networks.get.return_value = self._network + self.compute.networks.get = \ + compute_fakes.FakeNetwork.get_networks(networks=self._networks) # Get the command object to test self.cmd = network.DeleteNetwork(self.app, None) - def test_network_delete(self): + def test_delete_one_network(self): arglist = [ - self._network.label, + self._networks[0].label, ] verifylist = [ - ('network', [self._network.label]), + ('network', [self._networks[0].label]), ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.compute.networks.delete.assert_called_once_with( + self._networks[0].id) + self.assertIsNone(result) + def test_delete_multiple_networks(self): + arglist = [] + for n in self._networks: + arglist.append(n.label) + verifylist = [ + ('network', arglist), + ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) - self.compute.networks.delete.assert_called_with(self._network.id) + calls = [] + for n in self._networks: + calls.append(call(n.id)) + self.compute.networks.delete.assert_has_calls(calls) self.assertIsNone(result) + def test_delete_multiple_networks_exception(self): + arglist = [ + self._networks[0].id, + 'xxxx-yyyy-zzzz', + self._networks[1].id, + ] + verifylist = [ + ('network', arglist), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # Fake exception in utils.find_resource() + # In compute v2, we use utils.find_resource() to find a network. + # It calls get() several times, but find() only one time. So we + # choose to fake get() always raise exception, then pass through. + # And fake find() to find the real network or not. + self.compute.networks.get.side_effect = Exception() + ret_find = [ + self._networks[0], + Exception(), + self._networks[1], + ] + self.compute.networks.find.side_effect = ret_find + + # Fake exception in delete() + ret_delete = [ + None, + Exception(), + ] + self.compute.networks.delete = mock.Mock(side_effect=ret_delete) + + self.assertRaises(exceptions.CommandError, self.cmd.take_action, + parsed_args) + + # The second call of utils.find_resource() should fail. So delete() + # was only called twice. + calls = [ + call(self._networks[0].id), + call(self._networks[1].id), + ] + self.compute.networks.delete.assert_has_calls(calls) + class TestListNetworkCompute(TestNetworkCompute): @@ -765,7 +942,7 @@ class TestListNetworkCompute(TestNetworkCompute): # containing the data to be listed. columns, data = self.cmd.take_action(parsed_args) - self.compute.networks.list.assert_called_with() + self.compute.networks.list.assert_called_once_with() self.assertEqual(self.columns, columns) self.assertEqual(self.data, list(data)) @@ -860,7 +1037,6 @@ class TestShowNetworkCompute(TestNetworkCompute): arglist = [] verifylist = [] - # Missing required args should bail here self.assertRaises(tests_utils.ParserException, self.check_parser, self.cmd, arglist, verifylist) @@ -875,5 +1051,5 @@ class TestShowNetworkCompute(TestNetworkCompute): parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.assertEqual(self.columns, tuple(columns)) + self.assertEqual(self.columns, columns) self.assertEqual(self.data, data) diff --git a/openstackclient/tests/network/v2/test_port.py b/openstackclient/tests/network/v2/test_port.py index bc246bd..f2aa26c 100644 --- a/openstackclient/tests/network/v2/test_port.py +++ b/openstackclient/tests/network/v2/test_port.py @@ -27,6 +27,150 @@ class TestPort(network_fakes.TestNetworkV2): # Get a shortcut to the network client self.network = self.app.client_manager.network + def _get_common_cols_data(self, fake_port): + columns = ( + 'admin_state_up', + 'allowed_address_pairs', + 'binding_host_id', + 'binding_profile', + 'binding_vif_details', + 'binding_vif_type', + 'binding_vnic_type', + 'device_id', + 'device_owner', + 'dns_assignment', + 'dns_name', + 'extra_dhcp_opts', + 'fixed_ips', + 'id', + 'mac_address', + 'name', + 'network_id', + 'port_security_enabled', + 'project_id', + 'security_groups', + 'status', + ) + + data = ( + port._format_admin_state(fake_port.admin_state_up), + utils.format_list_of_dicts(fake_port.allowed_address_pairs), + fake_port.binding_host_id, + utils.format_dict(fake_port.binding_profile), + utils.format_dict(fake_port.binding_vif_details), + fake_port.binding_vif_type, + fake_port.binding_vnic_type, + fake_port.device_id, + fake_port.device_owner, + utils.format_list_of_dicts(fake_port.dns_assignment), + fake_port.dns_name, + utils.format_list_of_dicts(fake_port.extra_dhcp_opts), + utils.format_list_of_dicts(fake_port.fixed_ips), + fake_port.id, + fake_port.mac_address, + fake_port.name, + fake_port.network_id, + fake_port.port_security_enabled, + fake_port.project_id, + utils.format_list(fake_port.security_groups), + fake_port.status, + ) + + return columns, data + + +class TestCreatePort(TestPort): + + _port = network_fakes.FakePort.create_one_port() + + def setUp(self): + super(TestCreatePort, self).setUp() + + self.network.create_port = mock.Mock(return_value=self._port) + fake_net = network_fakes.FakeNetwork.create_one_network({ + 'id': self._port.network_id, + }) + self.network.find_network = mock.Mock(return_value=fake_net) + self.fake_subnet = network_fakes.FakeSubnet.create_one_subnet() + self.network.find_subnet = mock.Mock(return_value=self.fake_subnet) + # Get the command object to test + self.cmd = port.CreatePort(self.app, self.namespace) + + def test_create_default_options(self): + arglist = [ + '--network', self._port.network_id, + 'test-port', + ] + verifylist = [ + ('network', self._port.network_id,), + ('enable', True), + ('name', 'test-port'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = (self.cmd.take_action(parsed_args)) + + self.network.create_port.assert_called_once_with(**{ + 'admin_state_up': True, + 'network_id': self._port.network_id, + 'name': 'test-port', + }) + + ref_columns, ref_data = self._get_common_cols_data(self._port) + self.assertEqual(ref_columns, columns) + self.assertEqual(ref_data, data) + + def test_create_full_options(self): + arglist = [ + '--mac-address', 'aa:aa:aa:aa:aa:aa', + '--fixed-ip', 'subnet=%s,ip-address=10.0.0.2' + % self.fake_subnet.id, + '--device', 'deviceid', + '--device-owner', 'fakeowner', + '--disable', + '--vnic-type', 'macvtap', + '--binding-profile', 'foo=bar', + '--binding-profile', 'foo2=bar2', + '--network', self._port.network_id, + 'test-port', + + ] + verifylist = [ + ('mac_address', 'aa:aa:aa:aa:aa:aa'), + ( + 'fixed_ip', + [{'subnet': self.fake_subnet.id, 'ip-address': '10.0.0.2'}] + ), + ('device', 'deviceid'), + ('device_owner', 'fakeowner'), + ('disable', True), + ('vnic_type', 'macvtap'), + ('binding_profile', {'foo': 'bar', 'foo2': 'bar2'}), + ('network', self._port.network_id), + ('name', 'test-port'), + + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = (self.cmd.take_action(parsed_args)) + + self.network.create_port.assert_called_once_with(**{ + 'mac_address': 'aa:aa:aa:aa:aa:aa', + 'fixed_ips': [{'subnet_id': self.fake_subnet.id, + 'ip_address': '10.0.0.2'}], + 'device_id': 'deviceid', + 'device_owner': 'fakeowner', + 'admin_state_up': False, + 'binding:vnic_type': 'macvtap', + 'binding:profile': {'foo': 'bar', 'foo2': 'bar2'}, + 'network_id': self._port.network_id, + 'name': 'test-port', + }) + + ref_columns, ref_data = self._get_common_cols_data(self._port) + self.assertEqual(ref_columns, columns) + self.assertEqual(ref_data, data) + class TestDeletePort(TestPort): @@ -51,62 +195,184 @@ class TestDeletePort(TestPort): parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) - self.network.delete_port.assert_called_with(self._port) + self.network.delete_port.assert_called_once_with(self._port) self.assertIsNone(result) -class TestShowPort(TestPort): +class TestListPort(TestPort): - # The port to show. - _port = network_fakes.FakePort.create_one_port() + _ports = network_fakes.FakePort.create_ports(count=3) columns = ( - 'admin_state_up', - 'allowed_address_pairs', - 'binding_host_id', - 'binding_profile', - 'binding_vif_details', - 'binding_vif_type', - 'binding_vnic_type', - 'device_id', - 'device_owner', - 'dns_assignment', - 'dns_name', - 'extra_dhcp_opts', - 'fixed_ips', - 'id', - 'mac_address', - 'name', - 'network_id', - 'port_security_enabled', - 'project_id', - 'security_groups', - 'status', + 'ID', + 'Name', + 'MAC Address', + 'Fixed IP Addresses', ) - data = ( - port._format_admin_state(_port.admin_state_up), - utils.format_list_of_dicts(_port.allowed_address_pairs), - _port.binding_host_id, - utils.format_dict(_port.binding_profile), - utils.format_dict(_port.binding_vif_details), - _port.binding_vif_type, - _port.binding_vnic_type, - _port.device_id, - _port.device_owner, - utils.format_list_of_dicts(_port.dns_assignment), - _port.dns_name, - utils.format_list_of_dicts(_port.extra_dhcp_opts), - utils.format_list_of_dicts(_port.fixed_ips), - _port.id, - _port.mac_address, - _port.name, - _port.network_id, - _port.port_security_enabled, - _port.project_id, - utils.format_list(_port.security_groups), - _port.status, - ) + data = [] + for prt in _ports: + data.append(( + prt.id, + prt.name, + prt.mac_address, + utils.format_list_of_dicts(prt.fixed_ips), + )) + + def setUp(self): + super(TestListPort, self).setUp() + + # Get the command object to test + self.cmd = port.ListPort(self.app, self.namespace) + self.network.ports = mock.Mock(return_value=self._ports) + fake_router = network_fakes.FakeRouter.create_one_router({ + 'id': 'fake-router-id', + }) + self.network.find_router = mock.Mock(return_value=fake_router) + + def test_port_list_no_options(self): + arglist = [] + verifylist = [] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.network.ports.assert_called_once_with() + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, list(data)) + + def test_port_list_router_opt(self): + arglist = [ + '--router', 'fake-router-name', + ] + + verifylist = [ + ('router', 'fake-router-name') + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.network.ports.assert_called_once_with(**{ + 'device_id': 'fake-router-id' + }) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, list(data)) + + +class TestSetPort(TestPort): + + _port = network_fakes.FakePort.create_one_port() + + def setUp(self): + super(TestSetPort, self).setUp() + self.fake_subnet = network_fakes.FakeSubnet.create_one_subnet() + self.network.find_subnet = mock.Mock(return_value=self.fake_subnet) + self.network.find_port = mock.Mock(return_value=self._port) + self.network.update_port = mock.Mock(return_value=None) + + # Get the command object to test + self.cmd = port.SetPort(self.app, self.namespace) + + def test_set_fixed_ip(self): + arglist = [ + '--fixed-ip', 'ip-address=10.0.0.11', + self._port.name, + ] + verifylist = [ + ('fixed_ip', [{'ip-address': '10.0.0.11'}]), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + attrs = { + 'fixed_ips': [{'ip_address': '10.0.0.11'}], + } + self.network.update_port.assert_called_once_with(self._port, **attrs) + self.assertIsNone(result) + + def test_append_fixed_ip(self): + _testport = network_fakes.FakePort.create_one_port( + {'fixed_ips': [{'ip_address': '0.0.0.1'}]}) + self.network.find_port = mock.Mock(return_value=_testport) + arglist = [ + '--fixed-ip', 'ip-address=10.0.0.12', + _testport.name, + ] + verifylist = [ + ('fixed_ip', [{'ip-address': '10.0.0.12'}]), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + attrs = { + 'fixed_ips': [ + {'ip_address': '10.0.0.12'}, {'ip_address': '0.0.0.1'}], + } + self.network.update_port.assert_called_once_with(_testport, **attrs) + self.assertIsNone(result) + + def test_set_this(self): + arglist = [ + '--disable', + '--no-fixed-ip', + '--no-binding-profile', + self._port.name, + ] + verifylist = [ + ('disable', True), + ('no_binding_profile', True), + ('no_fixed_ip', True), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + attrs = { + 'admin_state_up': False, + 'binding:profile': {}, + 'fixed_ips': [], + } + self.network.update_port.assert_called_once_with(self._port, **attrs) + self.assertIsNone(result) + + def test_set_that(self): + arglist = [ + '--enable', + '--vnic-type', 'macvtap', + '--binding-profile', 'foo=bar', + '--host', 'binding-host-id-xxxx', + '--name', 'newName', + self._port.name, + ] + verifylist = [ + ('enable', True), + ('vnic_type', 'macvtap'), + ('binding_profile', {'foo': 'bar'}), + ('host', 'binding-host-id-xxxx'), + ('name', 'newName') + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + attrs = { + 'admin_state_up': True, + 'binding:vnic_type': 'macvtap', + 'binding:profile': {'foo': 'bar'}, + 'binding:host_id': 'binding-host-id-xxxx', + 'name': 'newName', + } + self.network.update_port.assert_called_once_with(self._port, **attrs) + self.assertIsNone(result) + + +class TestShowPort(TestPort): + + # The port to show. + _port = network_fakes.FakePort.create_one_port() def setUp(self): super(TestShowPort, self).setUp() @@ -134,7 +400,9 @@ class TestShowPort(TestPort): columns, data = self.cmd.take_action(parsed_args) - self.network.find_port.assert_called_with(self._port.name, - ignore_missing=False) - self.assertEqual(tuple(self.columns), columns) - self.assertEqual(self.data, data) + self.network.find_port.assert_called_once_with( + self._port.name, ignore_missing=False) + + ref_columns, ref_data = self._get_common_cols_data(self._port) + self.assertEqual(ref_columns, columns) + self.assertEqual(ref_data, data) diff --git a/openstackclient/tests/network/v2/test_router.py b/openstackclient/tests/network/v2/test_router.py index 794f8ab..99b41d2 100644 --- a/openstackclient/tests/network/v2/test_router.py +++ b/openstackclient/tests/network/v2/test_router.py @@ -29,6 +29,85 @@ class TestRouter(network_fakes.TestNetworkV2): self.network = self.app.client_manager.network +class TestAddPortToRouter(TestRouter): + '''Add port to Router ''' + + _port = network_fakes.FakePort.create_one_port() + _router = network_fakes.FakeRouter.create_one_router( + attrs={'port': _port.id}) + + def setUp(self): + super(TestAddPortToRouter, self).setUp() + self.network.router_add_interface = mock.Mock() + self.cmd = router.AddPortToRouter(self.app, self.namespace) + self.network.find_router = mock.Mock(return_value=self._router) + self.network.find_port = mock.Mock(return_value=self._port) + + def test_add_port_no_option(self): + arglist = [] + verifylist = [] + + self.assertRaises(tests_utils.ParserException, self.check_parser, + self.cmd, arglist, verifylist) + + def test_add_port_required_options(self): + arglist = [ + self._router.id, + self._router.port, + ] + verifylist = [ + ('router', self._router.id), + ('port', self._router.port), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.network.router_add_interface.assert_called_with(self._router, **{ + 'port_id': self._router.port, + }) + self.assertIsNone(result) + + +class TestAddSubnetToRouter(TestRouter): + '''Add subnet to Router ''' + + _subnet = network_fakes.FakeSubnet.create_one_subnet() + _router = network_fakes.FakeRouter.create_one_router( + attrs={'subnet': _subnet.id}) + + def setUp(self): + super(TestAddSubnetToRouter, self).setUp() + self.network.router_add_interface = mock.Mock() + self.cmd = router.AddSubnetToRouter(self.app, self.namespace) + self.network.find_router = mock.Mock(return_value=self._router) + self.network.find_subnet = mock.Mock(return_value=self._subnet) + + def test_add_subnet_no_option(self): + arglist = [] + verifylist = [] + + self.assertRaises(tests_utils.ParserException, self.check_parser, + self.cmd, arglist, verifylist) + + def test_add_subnet_required_options(self): + arglist = [ + self._router.id, + self._router.subnet, + ] + verifylist = [ + ('router', self._router.id), + ('subnet', self._router.subnet), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + self.network.router_add_interface.assert_called_with( + self._router, **{'subnet_id': self._router.subnet}) + + self.assertIsNone(result) + + class TestCreateRouter(TestRouter): # The new router created. @@ -36,19 +115,29 @@ class TestCreateRouter(TestRouter): columns = ( 'admin_state_up', + 'availability_zone_hints', + 'availability_zones', 'distributed', + 'external_gateway_info', 'ha', 'id', 'name', 'project_id', + 'routes', + 'status', ) data = ( router._format_admin_state(new_router.admin_state_up), + osc_utils.format_list(new_router.availability_zone_hints), + osc_utils.format_list(new_router.availability_zones), new_router.distributed, + router._format_external_gateway_info(new_router.external_gateway_info), new_router.ha, new_router.id, new_router.name, new_router.tenant_id, + router._format_routes(new_router.routes), + new_router.status, ) def setUp(self): @@ -63,7 +152,6 @@ class TestCreateRouter(TestRouter): arglist = [] verifylist = [] - # Missing required args should bail here self.assertRaises(tests_utils.ParserException, self.check_parser, self.cmd, arglist, verifylist) @@ -73,17 +161,16 @@ class TestCreateRouter(TestRouter): ] verifylist = [ ('name', self.new_router.name), - ('admin_state_up', True), + ('enable', True), ('distributed', False), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = (self.cmd.take_action(parsed_args)) - self.network.create_router.assert_called_with(**{ + self.network.create_router.assert_called_once_with(**{ 'admin_state_up': True, 'name': self.new_router.name, - 'distributed': False, }) self.assertEqual(self.columns, columns) self.assertEqual(self.data, data) @@ -97,16 +184,15 @@ class TestCreateRouter(TestRouter): verifylist = [ ('name', self.new_router.name), ('availability_zone_hints', ['fake-az', 'fake-az2']), - ('admin_state_up', True), + ('enable', True), ('distributed', False), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = (self.cmd.take_action(parsed_args)) - self.network.create_router.assert_called_with(**{ + self.network.create_router.assert_called_once_with(**{ 'admin_state_up': True, 'name': self.new_router.name, - 'distributed': False, 'availability_zone_hints': ['fake-az', 'fake-az2'], }) @@ -139,7 +225,7 @@ class TestDeleteRouter(TestRouter): parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) - self.network.delete_router.assert_called_with(self._router) + self.network.delete_router.assert_called_once_with(self._router) self.assertIsNone(result) @@ -179,7 +265,7 @@ class TestListRouter(TestRouter): r = routers[i] data_long.append( data[i] + ( - r.routes, + router._format_routes(r.routes), router._format_external_gateway_info(r.external_gateway_info), osc_utils.format_list(r.availability_zones), ) @@ -205,7 +291,7 @@ class TestListRouter(TestRouter): # containing the data to be listed. columns, data = self.cmd.take_action(parsed_args) - self.network.routers.assert_called_with() + self.network.routers.assert_called_once_with() self.assertEqual(self.columns, columns) self.assertEqual(self.data, list(data)) @@ -223,15 +309,95 @@ class TestListRouter(TestRouter): # containing the data to be listed. columns, data = self.cmd.take_action(parsed_args) - self.network.routers.assert_called_with() + self.network.routers.assert_called_once_with() self.assertEqual(self.columns_long, columns) self.assertEqual(self.data_long, list(data)) +class TestRemovePortFromRouter(TestRouter): + '''Remove port from a Router ''' + + _port = network_fakes.FakePort.create_one_port() + _router = network_fakes.FakeRouter.create_one_router( + attrs={'port': _port.id}) + + def setUp(self): + super(TestRemovePortFromRouter, self).setUp() + self.network.router_remove_interface = mock.Mock() + self.cmd = router.RemovePortFromRouter(self.app, self.namespace) + self.network.find_router = mock.Mock(return_value=self._router) + self.network.find_port = mock.Mock(return_value=self._port) + + def test_remove_port_no_option(self): + arglist = [] + verifylist = [] + + self.assertRaises(tests_utils.ParserException, self.check_parser, + self.cmd, arglist, verifylist) + + def test_remove_port_required_options(self): + arglist = [ + self._router.id, + self._router.port, + ] + verifylist = [ + ('router', self._router.id), + ('port', self._router.port), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.network.router_remove_interface.assert_called_with( + self._router, **{'port_id': self._router.port}) + self.assertIsNone(result) + + +class TestRemoveSubnetFromRouter(TestRouter): + '''Remove subnet from Router ''' + + _subnet = network_fakes.FakeSubnet.create_one_subnet() + _router = network_fakes.FakeRouter.create_one_router( + attrs={'subnet': _subnet.id}) + + def setUp(self): + super(TestRemoveSubnetFromRouter, self).setUp() + self.network.router_remove_interface = mock.Mock() + self.cmd = router.RemoveSubnetFromRouter(self.app, self.namespace) + self.network.find_router = mock.Mock(return_value=self._router) + self.network.find_subnet = mock.Mock(return_value=self._subnet) + + def test_remove_subnet_no_option(self): + arglist = [] + verifylist = [] + + self.assertRaises(tests_utils.ParserException, self.check_parser, + self.cmd, arglist, verifylist) + + def test_remove_subnet_required_options(self): + arglist = [ + self._router.id, + self._router.subnet, + ] + verifylist = [ + ('subnet', self._router.subnet), + ('router', self._router.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + self.network.router_remove_interface.assert_called_with( + self._router, **{'subnet_id': self._router.subnet}) + self.assertIsNone(result) + + class TestSetRouter(TestRouter): # The router to set. - _router = network_fakes.FakeRouter.create_one_router() + _default_route = {'destination': '10.20.20.0/24', 'nexthop': '10.20.30.1'} + _router = network_fakes.FakeRouter.create_one_router( + attrs={'routes': [_default_route]} + ) def setUp(self): super(TestSetRouter, self).setUp() @@ -252,7 +418,7 @@ class TestSetRouter(TestRouter): ] verifylist = [ ('router', self._router.name), - ('admin_state_up', True), + ('enable', True), ('distributed', True), ('name', 'noob'), ] @@ -265,7 +431,8 @@ class TestSetRouter(TestRouter): 'distributed': True, 'name': 'noob', } - self.network.update_router.assert_called_with(self._router, **attrs) + self.network.update_router.assert_called_once_with( + self._router, **attrs) self.assertIsNone(result) def test_set_that(self): @@ -276,8 +443,8 @@ class TestSetRouter(TestRouter): ] verifylist = [ ('router', self._router.name), - ('admin_state_up', False), - ('distributed', False), + ('disable', True), + ('centralized', True), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -287,7 +454,8 @@ class TestSetRouter(TestRouter): 'admin_state_up': False, 'distributed': False, } - self.network.update_router.assert_called_with(self._router, **attrs) + self.network.update_router.assert_called_once_with( + self._router, **attrs) self.assertIsNone(result) def test_set_distributed_centralized(self): @@ -302,7 +470,6 @@ class TestSetRouter(TestRouter): ('distributed', False), ] - # Missing required args should bail here self.assertRaises(tests_utils.ParserException, self.check_parser, self.cmd, arglist, verifylist) @@ -321,12 +488,49 @@ class TestSetRouter(TestRouter): result = self.cmd.take_action(parsed_args) attrs = { - 'routes': [{'destination': '10.20.30.0/24', - 'gateway': '10.20.30.1'}], + 'routes': self._router.routes + [{'destination': '10.20.30.0/24', + 'nexthop': '10.20.30.1'}], } - self.network.update_router.assert_called_with(self._router, **attrs) + self.network.update_router.assert_called_once_with( + self._router, **attrs) self.assertIsNone(result) + def test_set_no_route(self): + arglist = [ + self._router.name, + '--no-route', + ] + verifylist = [ + ('router', self._router.name), + ('no_route', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + attrs = { + 'routes': [], + } + self.network.update_router.assert_called_once_with( + self._router, **attrs) + self.assertIsNone(result) + + def test_set_route_no_route(self): + arglist = [ + self._router.name, + '--route', 'destination=10.20.30.0/24,gateway=10.20.30.1', + '--no-route', + ] + verifylist = [ + ('router', self._router.name), + ('routes', [{'destination': '10.20.30.0/24', + 'gateway': '10.20.30.1'}]), + ('no_route', True), + ] + + self.assertRaises(tests_utils.ParserException, self.check_parser, + self.cmd, arglist, verifylist) + def test_set_clear_routes(self): arglist = [ self._router.name, @@ -343,7 +547,8 @@ class TestSetRouter(TestRouter): attrs = { 'routes': [], } - self.network.update_router.assert_called_with(self._router, **attrs) + self.network.update_router.assert_called_once_with( + self._router, **attrs) self.assertIsNone(result) def test_set_route_clear_routes(self): @@ -359,7 +564,6 @@ class TestSetRouter(TestRouter): ('clear_routes', True), ] - # Argument parse failing should bail here self.assertRaises(tests_utils.ParserException, self.check_parser, self.cmd, arglist, verifylist) @@ -379,20 +583,29 @@ class TestShowRouter(TestRouter): columns = ( 'admin_state_up', + 'availability_zone_hints', + 'availability_zones', 'distributed', + 'external_gateway_info', 'ha', 'id', 'name', - 'tenant_id', + 'project_id', + 'routes', + 'status', ) - data = ( router._format_admin_state(_router.admin_state_up), + osc_utils.format_list(_router.availability_zone_hints), + osc_utils.format_list(_router.availability_zones), _router.distributed, + router._format_external_gateway_info(_router.external_gateway_info), _router.ha, _router.id, _router.name, _router.tenant_id, + router._format_routes(_router.routes), + _router.status, ) def setUp(self): @@ -407,7 +620,6 @@ class TestShowRouter(TestRouter): arglist = [] verifylist = [] - # Missing required args should bail here self.assertRaises(tests_utils.ParserException, self.check_parser, self.cmd, arglist, verifylist) @@ -422,7 +634,7 @@ class TestShowRouter(TestRouter): columns, data = self.cmd.take_action(parsed_args) - self.network.find_router.assert_called_with(self._router.name, - ignore_missing=False) - self.assertEqual(tuple(self.columns), columns) + self.network.find_router.assert_called_once_with( + self._router.name, ignore_missing=False) + self.assertEqual(self.columns, columns) self.assertEqual(self.data, data) diff --git a/openstackclient/tests/network/v2/test_security_group.py b/openstackclient/tests/network/v2/test_security_group.py index ea96442..213367a 100644 --- a/openstackclient/tests/network/v2/test_security_group.py +++ b/openstackclient/tests/network/v2/test_security_group.py @@ -11,11 +11,15 @@ # under the License. # +import copy import mock from openstackclient.network.v2 import security_group from openstackclient.tests.compute.v2 import fakes as compute_fakes +from openstackclient.tests import fakes +from openstackclient.tests.identity.v3 import fakes as identity_fakes from openstackclient.tests.network.v2 import fakes as network_fakes +from openstackclient.tests import utils as tests_utils class TestSecurityGroupNetwork(network_fakes.TestNetworkV2): @@ -36,6 +40,191 @@ class TestSecurityGroupCompute(compute_fakes.TestComputev2): self.compute = self.app.client_manager.compute +class TestCreateSecurityGroupNetwork(TestSecurityGroupNetwork): + + # The security group to be created. + _security_group = \ + network_fakes.FakeSecurityGroup.create_one_security_group() + + columns = ( + 'description', + 'id', + 'name', + 'project_id', + 'rules', + ) + + data = ( + _security_group.description, + _security_group.id, + _security_group.name, + _security_group.project_id, + '', + ) + + def setUp(self): + super(TestCreateSecurityGroupNetwork, self).setUp() + + self.network.create_security_group = mock.Mock( + return_value=self._security_group) + + # Set identity client v3. And get a shortcut to Identity client. + identity_client = identity_fakes.FakeIdentityv3Client( + endpoint=fakes.AUTH_URL, + token=fakes.AUTH_TOKEN, + ) + self.app.client_manager.identity = identity_client + self.identity = self.app.client_manager.identity + + # Get a shortcut to the ProjectManager Mock + self.projects_mock = self.identity.projects + self.projects_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.PROJECT), + loaded=True, + ) + + # Get a shortcut to the DomainManager Mock + self.domains_mock = self.identity.domains + self.domains_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.DOMAIN), + loaded=True, + ) + + # Get the command object to test + self.cmd = security_group.CreateSecurityGroup(self.app, self.namespace) + + def test_create_no_options(self): + self.assertRaises(tests_utils.ParserException, + self.check_parser, self.cmd, [], []) + + def test_create_min_options(self): + arglist = [ + self._security_group.name, + ] + verifylist = [ + ('name', self._security_group.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.network.create_security_group.assert_called_once_with(**{ + 'description': self._security_group.name, + 'name': self._security_group.name, + }) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + def test_create_all_options(self): + arglist = [ + '--description', self._security_group.description, + '--project', identity_fakes.project_name, + '--project-domain', identity_fakes.domain_name, + self._security_group.name, + ] + verifylist = [ + ('description', self._security_group.description), + ('name', self._security_group.name), + ('project', identity_fakes.project_name), + ('project_domain', identity_fakes.domain_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.network.create_security_group.assert_called_once_with(**{ + 'description': self._security_group.description, + 'name': self._security_group.name, + 'tenant_id': identity_fakes.project_id, + }) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + +class TestCreateSecurityGroupCompute(TestSecurityGroupCompute): + + # The security group to be shown. + _security_group = \ + compute_fakes.FakeSecurityGroup.create_one_security_group() + + columns = ( + 'description', + 'id', + 'name', + 'project_id', + 'rules', + ) + + data = ( + _security_group.description, + _security_group.id, + _security_group.name, + _security_group.tenant_id, + '', + ) + + def setUp(self): + super(TestCreateSecurityGroupCompute, self).setUp() + + self.app.client_manager.network_endpoint_enabled = False + + self.compute.security_groups.create.return_value = self._security_group + + # Get the command object to test + self.cmd = security_group.CreateSecurityGroup(self.app, None) + + def test_create_no_options(self): + self.assertRaises(tests_utils.ParserException, + self.check_parser, self.cmd, [], []) + + def test_create_network_options(self): + arglist = [ + '--project', identity_fakes.project_name, + '--project-domain', identity_fakes.domain_name, + self._security_group.name, + ] + self.assertRaises(tests_utils.ParserException, + self.check_parser, self.cmd, arglist, []) + + def test_create_min_options(self): + arglist = [ + self._security_group.name, + ] + verifylist = [ + ('name', self._security_group.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.compute.security_groups.create.assert_called_once_with( + self._security_group.name, + self._security_group.name) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + def test_create_all_options(self): + arglist = [ + '--description', self._security_group.description, + self._security_group.name, + ] + verifylist = [ + ('description', self._security_group.description), + ('name', self._security_group.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.compute.security_groups.create.assert_called_once_with( + self._security_group.name, + self._security_group.description) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + class TestDeleteSecurityGroupNetwork(TestSecurityGroupNetwork): # The security group to be deleted. @@ -64,7 +253,7 @@ class TestDeleteSecurityGroupNetwork(TestSecurityGroupNetwork): parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) - self.network.delete_security_group.assert_called_with( + self.network.delete_security_group.assert_called_once_with( self._security_group) self.assertIsNone(result) @@ -99,7 +288,7 @@ class TestDeleteSecurityGroupCompute(TestSecurityGroupCompute): parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) - self.compute.security_groups.delete.assert_called_with( + self.compute.security_groups.delete.assert_called_once_with( self._security_group.id) self.assertIsNone(result) @@ -107,28 +296,30 @@ class TestDeleteSecurityGroupCompute(TestSecurityGroupCompute): class TestListSecurityGroupNetwork(TestSecurityGroupNetwork): # The security group to be listed. - _security_group = \ - network_fakes.FakeSecurityGroup.create_one_security_group() + _security_groups = \ + network_fakes.FakeSecurityGroup.create_security_groups(count=3) - expected_columns = ( + columns = ( 'ID', 'Name', 'Description', 'Project', ) - expected_data = (( - _security_group.id, - _security_group.name, - _security_group.description, - _security_group.tenant_id, - ),) + data = [] + for grp in _security_groups: + data.append(( + grp.id, + grp.name, + grp.description, + grp.tenant_id, + )) def setUp(self): super(TestListSecurityGroupNetwork, self).setUp() self.network.security_groups = mock.Mock( - return_value=[self._security_group]) + return_value=self._security_groups) # Get the command object to test self.cmd = security_group.ListSecurityGroup(self.app, self.namespace) @@ -142,9 +333,9 @@ class TestListSecurityGroupNetwork(TestSecurityGroupNetwork): columns, data = self.cmd.take_action(parsed_args) - self.network.security_groups.assert_called_with() - self.assertEqual(self.expected_columns, columns) - self.assertEqual(self.expected_data, tuple(data)) + self.network.security_groups.assert_called_once_with() + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, list(data)) def test_security_group_list_all_projects(self): arglist = [ @@ -157,46 +348,50 @@ class TestListSecurityGroupNetwork(TestSecurityGroupNetwork): columns, data = self.cmd.take_action(parsed_args) - self.network.security_groups.assert_called_with() - self.assertEqual(self.expected_columns, columns) - self.assertEqual(self.expected_data, tuple(data)) + self.network.security_groups.assert_called_once_with() + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, list(data)) class TestListSecurityGroupCompute(TestSecurityGroupCompute): # The security group to be listed. - _security_group = \ - compute_fakes.FakeSecurityGroup.create_one_security_group() + _security_groups = \ + compute_fakes.FakeSecurityGroup.create_security_groups(count=3) - expected_columns = ( + columns = ( 'ID', 'Name', 'Description', ) - expected_columns_all_projects = ( + columns_all_projects = ( 'ID', 'Name', 'Description', 'Project', ) - expected_data = (( - _security_group.id, - _security_group.name, - _security_group.description, - ),) - expected_data_all_projects = (( - _security_group.id, - _security_group.name, - _security_group.description, - _security_group.tenant_id, - ),) + data = [] + for grp in _security_groups: + data.append(( + grp.id, + grp.name, + grp.description, + )) + data_all_projects = [] + for grp in _security_groups: + data_all_projects.append(( + grp.id, + grp.name, + grp.description, + grp.tenant_id, + )) def setUp(self): super(TestListSecurityGroupCompute, self).setUp() self.app.client_manager.network_endpoint_enabled = False - self.compute.security_groups.list.return_value = [self._security_group] + self.compute.security_groups.list.return_value = self._security_groups # Get the command object to test self.cmd = security_group.ListSecurityGroup(self.app, None) @@ -211,9 +406,9 @@ class TestListSecurityGroupCompute(TestSecurityGroupCompute): columns, data = self.cmd.take_action(parsed_args) kwargs = {'search_opts': {'all_tenants': False}} - self.compute.security_groups.list.assert_called_with(**kwargs) - self.assertEqual(self.expected_columns, columns) - self.assertEqual(self.expected_data, tuple(data)) + self.compute.security_groups.list.assert_called_once_with(**kwargs) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, list(data)) def test_security_group_list_all_projects(self): arglist = [ @@ -227,6 +422,257 @@ class TestListSecurityGroupCompute(TestSecurityGroupCompute): columns, data = self.cmd.take_action(parsed_args) kwargs = {'search_opts': {'all_tenants': True}} - self.compute.security_groups.list.assert_called_with(**kwargs) - self.assertEqual(self.expected_columns_all_projects, columns) - self.assertEqual(self.expected_data_all_projects, tuple(data)) + self.compute.security_groups.list.assert_called_once_with(**kwargs) + self.assertEqual(self.columns_all_projects, columns) + self.assertEqual(self.data_all_projects, list(data)) + + +class TestSetSecurityGroupNetwork(TestSecurityGroupNetwork): + + # The security group to be set. + _security_group = \ + network_fakes.FakeSecurityGroup.create_one_security_group() + + def setUp(self): + super(TestSetSecurityGroupNetwork, self).setUp() + + self.network.update_security_group = mock.Mock(return_value=None) + + self.network.find_security_group = mock.Mock( + return_value=self._security_group) + + # Get the command object to test + self.cmd = security_group.SetSecurityGroup(self.app, self.namespace) + + def test_set_no_options(self): + self.assertRaises(tests_utils.ParserException, + self.check_parser, self.cmd, [], []) + + def test_set_no_updates(self): + arglist = [ + self._security_group.name, + ] + verifylist = [ + ('group', self._security_group.name), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + self.network.update_security_group.assert_called_once_with( + self._security_group, + **{} + ) + self.assertIsNone(result) + + def test_set_all_options(self): + new_name = 'new-' + self._security_group.name + new_description = 'new-' + self._security_group.description + arglist = [ + '--name', new_name, + '--description', new_description, + self._security_group.name, + ] + verifylist = [ + ('description', new_description), + ('group', self._security_group.name), + ('name', new_name), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + attrs = { + 'description': new_description, + 'name': new_name, + } + self.network.update_security_group.assert_called_once_with( + self._security_group, + **attrs + ) + self.assertIsNone(result) + + +class TestSetSecurityGroupCompute(TestSecurityGroupCompute): + + # The security group to be set. + _security_group = \ + compute_fakes.FakeSecurityGroup.create_one_security_group() + + def setUp(self): + super(TestSetSecurityGroupCompute, self).setUp() + + self.app.client_manager.network_endpoint_enabled = False + + self.compute.security_groups.update = mock.Mock(return_value=None) + + self.compute.security_groups.get = mock.Mock( + return_value=self._security_group) + + # Get the command object to test + self.cmd = security_group.SetSecurityGroup(self.app, None) + + def test_set_no_options(self): + self.assertRaises(tests_utils.ParserException, + self.check_parser, self.cmd, [], []) + + def test_set_no_updates(self): + arglist = [ + self._security_group.name, + ] + verifylist = [ + ('group', self._security_group.name), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + self.compute.security_groups.update.assert_called_once_with( + self._security_group, + self._security_group.name, + self._security_group.description + ) + self.assertIsNone(result) + + def test_set_all_options(self): + new_name = 'new-' + self._security_group.name + new_description = 'new-' + self._security_group.description + arglist = [ + '--name', new_name, + '--description', new_description, + self._security_group.name, + ] + verifylist = [ + ('description', new_description), + ('group', self._security_group.name), + ('name', new_name), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + self.compute.security_groups.update.assert_called_once_with( + self._security_group, + new_name, + new_description + ) + self.assertIsNone(result) + + +class TestShowSecurityGroupNetwork(TestSecurityGroupNetwork): + + # The security group rule to be shown with the group. + _security_group_rule = \ + network_fakes.FakeSecurityGroupRule.create_one_security_group_rule() + + # The security group to be shown. + _security_group = \ + network_fakes.FakeSecurityGroup.create_one_security_group( + attrs={'security_group_rules': [_security_group_rule._info]} + ) + + columns = ( + 'description', + 'id', + 'name', + 'project_id', + 'rules', + ) + + data = ( + _security_group.description, + _security_group.id, + _security_group.name, + _security_group.project_id, + security_group._format_network_security_group_rules( + [_security_group_rule._info]), + ) + + def setUp(self): + super(TestShowSecurityGroupNetwork, self).setUp() + + self.network.find_security_group = mock.Mock( + return_value=self._security_group) + + # Get the command object to test + self.cmd = security_group.ShowSecurityGroup(self.app, self.namespace) + + def test_show_no_options(self): + self.assertRaises(tests_utils.ParserException, + self.check_parser, self.cmd, [], []) + + def test_show_all_options(self): + arglist = [ + self._security_group.id, + ] + verifylist = [ + ('group', self._security_group.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.network.find_security_group.assert_called_once_with( + self._security_group.id, ignore_missing=False) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + +class TestShowSecurityGroupCompute(TestSecurityGroupCompute): + + # The security group rule to be shown with the group. + _security_group_rule = \ + compute_fakes.FakeSecurityGroupRule.create_one_security_group_rule() + + # The security group to be shown. + _security_group = \ + compute_fakes.FakeSecurityGroup.create_one_security_group( + attrs={'rules': [_security_group_rule._info]} + ) + + columns = ( + 'description', + 'id', + 'name', + 'project_id', + 'rules', + ) + + data = ( + _security_group.description, + _security_group.id, + _security_group.name, + _security_group.tenant_id, + security_group._format_compute_security_group_rules( + [_security_group_rule._info]), + ) + + def setUp(self): + super(TestShowSecurityGroupCompute, self).setUp() + + self.app.client_manager.network_endpoint_enabled = False + + self.compute.security_groups.get.return_value = self._security_group + + # Get the command object to test + self.cmd = security_group.ShowSecurityGroup(self.app, None) + + def test_show_no_options(self): + self.assertRaises(tests_utils.ParserException, + self.check_parser, self.cmd, [], []) + + def test_show_all_options(self): + arglist = [ + self._security_group.id, + ] + verifylist = [ + ('group', self._security_group.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.compute.security_groups.get.assert_called_once_with( + self._security_group.id) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) diff --git a/openstackclient/tests/network/v2/test_security_group_rule.py b/openstackclient/tests/network/v2/test_security_group_rule.py index db15d0e..2a64b88 100644 --- a/openstackclient/tests/network/v2/test_security_group_rule.py +++ b/openstackclient/tests/network/v2/test_security_group_rule.py @@ -14,9 +14,12 @@ import copy import mock +from openstackclient.common import exceptions +from openstackclient.network import utils as network_utils from openstackclient.network.v2 import security_group_rule from openstackclient.tests.compute.v2 import fakes as compute_fakes from openstackclient.tests import fakes +from openstackclient.tests.identity.v3 import fakes as identity_fakes from openstackclient.tests.network.v2 import fakes as network_fakes from openstackclient.tests import utils as tests_utils @@ -39,6 +42,629 @@ class TestSecurityGroupRuleCompute(compute_fakes.TestComputev2): self.compute = self.app.client_manager.compute +class TestCreateSecurityGroupRuleNetwork(TestSecurityGroupRuleNetwork): + + # The security group rule to be created. + _security_group_rule = None + + # The security group that will contain the rule created. + _security_group = \ + network_fakes.FakeSecurityGroup.create_one_security_group() + + expected_columns = ( + 'direction', + 'ethertype', + 'id', + 'port_range_max', + 'port_range_min', + 'project_id', + 'protocol', + 'remote_group_id', + 'remote_ip_prefix', + 'security_group_id', + ) + + expected_data = None + + def _setup_security_group_rule(self, attrs=None): + self._security_group_rule = \ + network_fakes.FakeSecurityGroupRule.create_one_security_group_rule( + attrs) + self.network.create_security_group_rule = mock.Mock( + return_value=self._security_group_rule) + self.expected_data = ( + self._security_group_rule.direction, + self._security_group_rule.ethertype, + self._security_group_rule.id, + self._security_group_rule.port_range_max, + self._security_group_rule.port_range_min, + self._security_group_rule.project_id, + self._security_group_rule.protocol, + self._security_group_rule.remote_group_id, + self._security_group_rule.remote_ip_prefix, + self._security_group_rule.security_group_id, + ) + + def setUp(self): + super(TestCreateSecurityGroupRuleNetwork, self).setUp() + + self.network.find_security_group = mock.Mock( + return_value=self._security_group) + + # Set identity client v3. And get a shortcut to Identity client. + identity_client = identity_fakes.FakeIdentityv3Client( + endpoint=fakes.AUTH_URL, + token=fakes.AUTH_TOKEN, + ) + self.app.client_manager.identity = identity_client + self.identity = self.app.client_manager.identity + + # Get a shortcut to the ProjectManager Mock + self.projects_mock = self.identity.projects + self.projects_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.PROJECT), + loaded=True, + ) + + # Get a shortcut to the DomainManager Mock + self.domains_mock = self.identity.domains + self.domains_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.DOMAIN), + loaded=True, + ) + + # Get the command object to test + self.cmd = security_group_rule.CreateSecurityGroupRule( + self.app, self.namespace) + + def test_create_no_options(self): + self.assertRaises(tests_utils.ParserException, + self.check_parser, self.cmd, [], []) + + def test_create_all_source_options(self): + arglist = [ + '--src-ip', '10.10.0.0/24', + '--src-group', self._security_group.id, + self._security_group.id, + ] + self.assertRaises(tests_utils.ParserException, + self.check_parser, self.cmd, arglist, []) + + def test_create_bad_ethertype(self): + arglist = [ + '--ethertype', 'foo', + self._security_group.id, + ] + self.assertRaises(tests_utils.ParserException, + self.check_parser, self.cmd, arglist, []) + + def test_create_all_protocol_options(self): + arglist = [ + '--protocol', 'tcp', + '--proto', 'tcp', + self._security_group.id, + ] + self.assertRaises(tests_utils.ParserException, + self.check_parser, self.cmd, arglist, []) + + def test_create_all_port_range_options(self): + arglist = [ + '--dst-port', '80:80', + '--icmp-type', '3', + '--icmp-code', '1', + self._security_group.id, + ] + verifylist = [ + ('dst_port', (80, 80)), + ('icmp_type', 3), + ('icmp_code', 1), + ('group', self._security_group.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.assertRaises(exceptions.CommandError, self.cmd.take_action, + parsed_args) + + def test_create_default_rule(self): + self._setup_security_group_rule({ + 'port_range_max': 443, + 'port_range_min': 443, + }) + arglist = [ + '--dst-port', str(self._security_group_rule.port_range_min), + self._security_group.id, + ] + verifylist = [ + ('dst_port', (self._security_group_rule.port_range_min, + self._security_group_rule.port_range_max)), + ('group', self._security_group.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.network.create_security_group_rule.assert_called_once_with(**{ + 'direction': self._security_group_rule.direction, + 'ethertype': self._security_group_rule.ethertype, + 'port_range_max': self._security_group_rule.port_range_max, + 'port_range_min': self._security_group_rule.port_range_min, + 'protocol': self._security_group_rule.protocol, + 'remote_ip_prefix': self._security_group_rule.remote_ip_prefix, + 'security_group_id': self._security_group.id, + }) + self.assertEqual(self.expected_columns, columns) + self.assertEqual(self.expected_data, data) + + def test_create_proto_option(self): + self._setup_security_group_rule({ + 'protocol': 'icmp', + 'remote_ip_prefix': '10.0.2.0/24', + }) + arglist = [ + '--proto', self._security_group_rule.protocol, + '--src-ip', self._security_group_rule.remote_ip_prefix, + self._security_group.id, + ] + verifylist = [ + ('proto', self._security_group_rule.protocol), + ('protocol', None), + ('src_ip', self._security_group_rule.remote_ip_prefix), + ('group', self._security_group.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.network.create_security_group_rule.assert_called_once_with(**{ + 'direction': self._security_group_rule.direction, + 'ethertype': self._security_group_rule.ethertype, + 'protocol': self._security_group_rule.protocol, + 'remote_ip_prefix': self._security_group_rule.remote_ip_prefix, + 'security_group_id': self._security_group.id, + }) + self.assertEqual(self.expected_columns, columns) + self.assertEqual(self.expected_data, data) + + def test_create_source_group(self): + self._setup_security_group_rule({ + 'port_range_max': 22, + 'port_range_min': 22, + 'remote_group_id': self._security_group.id, + }) + arglist = [ + '--dst-port', str(self._security_group_rule.port_range_min), + '--ingress', + '--src-group', self._security_group.name, + self._security_group.id, + ] + verifylist = [ + ('dst_port', (self._security_group_rule.port_range_min, + self._security_group_rule.port_range_max)), + ('ingress', True), + ('src_group', self._security_group.name), + ('group', self._security_group.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.network.create_security_group_rule.assert_called_once_with(**{ + 'direction': self._security_group_rule.direction, + 'ethertype': self._security_group_rule.ethertype, + 'port_range_max': self._security_group_rule.port_range_max, + 'port_range_min': self._security_group_rule.port_range_min, + 'protocol': self._security_group_rule.protocol, + 'remote_group_id': self._security_group_rule.remote_group_id, + 'security_group_id': self._security_group.id, + }) + self.assertEqual(self.expected_columns, columns) + self.assertEqual(self.expected_data, data) + + def test_create_source_ip(self): + self._setup_security_group_rule({ + 'protocol': 'icmp', + 'remote_ip_prefix': '10.0.2.0/24', + }) + arglist = [ + '--protocol', self._security_group_rule.protocol, + '--src-ip', self._security_group_rule.remote_ip_prefix, + self._security_group.id, + ] + verifylist = [ + ('protocol', self._security_group_rule.protocol), + ('src_ip', self._security_group_rule.remote_ip_prefix), + ('group', self._security_group.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.network.create_security_group_rule.assert_called_once_with(**{ + 'direction': self._security_group_rule.direction, + 'ethertype': self._security_group_rule.ethertype, + 'protocol': self._security_group_rule.protocol, + 'remote_ip_prefix': self._security_group_rule.remote_ip_prefix, + 'security_group_id': self._security_group.id, + }) + self.assertEqual(self.expected_columns, columns) + self.assertEqual(self.expected_data, data) + + def test_create_network_options(self): + self._setup_security_group_rule({ + 'direction': 'egress', + 'ethertype': 'IPv6', + 'port_range_max': 443, + 'port_range_min': 443, + 'protocol': '6', + 'remote_group_id': None, + 'remote_ip_prefix': None, + }) + arglist = [ + '--dst-port', str(self._security_group_rule.port_range_min), + '--egress', + '--ethertype', self._security_group_rule.ethertype, + '--project', identity_fakes.project_name, + '--project-domain', identity_fakes.domain_name, + '--protocol', self._security_group_rule.protocol, + self._security_group.id, + ] + verifylist = [ + ('dst_port', (self._security_group_rule.port_range_min, + self._security_group_rule.port_range_max)), + ('egress', True), + ('ethertype', self._security_group_rule.ethertype), + ('project', identity_fakes.project_name), + ('project_domain', identity_fakes.domain_name), + ('protocol', self._security_group_rule.protocol), + ('group', self._security_group.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.network.create_security_group_rule.assert_called_once_with(**{ + 'direction': self._security_group_rule.direction, + 'ethertype': self._security_group_rule.ethertype, + 'port_range_max': self._security_group_rule.port_range_max, + 'port_range_min': self._security_group_rule.port_range_min, + 'protocol': self._security_group_rule.protocol, + 'security_group_id': self._security_group.id, + 'tenant_id': identity_fakes.project_id, + }) + self.assertEqual(self.expected_columns, columns) + self.assertEqual(self.expected_data, data) + + def test_create_tcp_with_icmp_type(self): + arglist = [ + '--protocol', 'tcp', + '--icmp-type', '15', + self._security_group.id, + ] + verifylist = [ + ('protocol', 'tcp'), + ('icmp_type', 15), + ('group', self._security_group.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.assertRaises(exceptions.CommandError, self.cmd.take_action, + parsed_args) + + def test_create_icmp_code(self): + arglist = [ + '--protocol', '1', + '--icmp-code', '1', + self._security_group.id, + ] + verifylist = [ + ('protocol', '1'), + ('icmp_code', 1), + ('group', self._security_group.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.assertRaises(exceptions.CommandError, self.cmd.take_action, + parsed_args) + + def test_create_icmp_type(self): + self._setup_security_group_rule({ + 'port_range_min': 15, + 'protocol': 'icmp', + 'remote_ip_prefix': '0.0.0.0/0', + }) + arglist = [ + '--icmp-type', str(self._security_group_rule.port_range_min), + '--protocol', self._security_group_rule.protocol, + self._security_group.id, + ] + verifylist = [ + ('dst_port', None), + ('icmp_type', self._security_group_rule.port_range_min), + ('icmp_code', None), + ('protocol', self._security_group_rule.protocol), + ('group', self._security_group.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.network.create_security_group_rule.assert_called_once_with(**{ + 'direction': self._security_group_rule.direction, + 'ethertype': self._security_group_rule.ethertype, + 'port_range_min': self._security_group_rule.port_range_min, + 'protocol': self._security_group_rule.protocol, + 'remote_ip_prefix': self._security_group_rule.remote_ip_prefix, + 'security_group_id': self._security_group.id, + }) + self.assertEqual(self.expected_columns, columns) + self.assertEqual(self.expected_data, data) + + def test_create_ipv6_icmp_type_code(self): + self._setup_security_group_rule({ + 'ethertype': 'IPv6', + 'port_range_min': 139, + 'port_range_max': 2, + 'protocol': 'ipv6-icmp', + }) + arglist = [ + '--icmp-type', str(self._security_group_rule.port_range_min), + '--icmp-code', str(self._security_group_rule.port_range_max), + '--protocol', self._security_group_rule.protocol, + self._security_group.id, + ] + verifylist = [ + ('dst_port', None), + ('icmp_type', self._security_group_rule.port_range_min), + ('icmp_code', self._security_group_rule.port_range_max), + ('protocol', self._security_group_rule.protocol), + ('group', self._security_group.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.network.create_security_group_rule.assert_called_once_with(**{ + 'direction': self._security_group_rule.direction, + 'ethertype': self._security_group_rule.ethertype, + 'port_range_min': self._security_group_rule.port_range_min, + 'port_range_max': self._security_group_rule.port_range_max, + 'protocol': self._security_group_rule.protocol, + 'security_group_id': self._security_group.id, + }) + self.assertEqual(self.expected_columns, columns) + self.assertEqual(self.expected_data, data) + + def test_create_icmpv6_type(self): + self._setup_security_group_rule({ + 'ethertype': 'IPv6', + 'port_range_min': 139, + 'protocol': 'icmpv6', + }) + arglist = [ + '--icmp-type', str(self._security_group_rule.port_range_min), + '--protocol', self._security_group_rule.protocol, + self._security_group.id, + ] + verifylist = [ + ('dst_port', None), + ('icmp_type', self._security_group_rule.port_range_min), + ('icmp_code', None), + ('protocol', self._security_group_rule.protocol), + ('group', self._security_group.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.network.create_security_group_rule.assert_called_once_with(**{ + 'direction': self._security_group_rule.direction, + 'ethertype': self._security_group_rule.ethertype, + 'port_range_min': self._security_group_rule.port_range_min, + 'protocol': self._security_group_rule.protocol, + 'security_group_id': self._security_group.id, + }) + self.assertEqual(self.expected_columns, columns) + self.assertEqual(self.expected_data, data) + + +class TestCreateSecurityGroupRuleCompute(TestSecurityGroupRuleCompute): + + # The security group rule to be created. + _security_group_rule = None + + # The security group that will contain the rule created. + _security_group = \ + compute_fakes.FakeSecurityGroup.create_one_security_group() + + def _setup_security_group_rule(self, attrs=None): + self._security_group_rule = \ + compute_fakes.FakeSecurityGroupRule.create_one_security_group_rule( + attrs) + self.compute.security_group_rules.create.return_value = \ + self._security_group_rule + expected_columns, expected_data = \ + security_group_rule._format_security_group_rule_show( + self._security_group_rule._info) + return expected_columns, expected_data + + def setUp(self): + super(TestCreateSecurityGroupRuleCompute, self).setUp() + + self.app.client_manager.network_endpoint_enabled = False + + self.compute.security_groups.get.return_value = self._security_group + + # Get the command object to test + self.cmd = security_group_rule.CreateSecurityGroupRule(self.app, None) + + def test_create_no_options(self): + self.assertRaises(tests_utils.ParserException, + self.check_parser, self.cmd, [], []) + + def test_create_all_source_options(self): + arglist = [ + '--src-ip', '10.10.0.0/24', + '--src-group', self._security_group.id, + self._security_group.id, + ] + self.assertRaises(tests_utils.ParserException, + self.check_parser, self.cmd, arglist, []) + + def test_create_bad_protocol(self): + arglist = [ + '--protocol', 'foo', + self._security_group.id, + ] + self.assertRaises(tests_utils.ParserException, + self.check_parser, self.cmd, arglist, []) + + def test_create_all_protocol_options(self): + arglist = [ + '--protocol', 'tcp', + '--proto', 'tcp', + self._security_group.id, + ] + self.assertRaises(tests_utils.ParserException, + self.check_parser, self.cmd, arglist, []) + + def test_create_network_options(self): + arglist = [ + '--ingress', + '--ethertype', 'IPv4', + '--icmp-type', '3', + '--icmp-code', '11', + '--project', identity_fakes.project_name, + '--project-domain', identity_fakes.domain_name, + self._security_group.id, + ] + self.assertRaises(tests_utils.ParserException, + self.check_parser, self.cmd, arglist, []) + + def test_create_default_rule(self): + expected_columns, expected_data = self._setup_security_group_rule() + dst_port = str(self._security_group_rule.from_port) + ':' + \ + str(self._security_group_rule.to_port) + arglist = [ + '--dst-port', dst_port, + self._security_group.id, + ] + verifylist = [ + ('dst_port', (self._security_group_rule.from_port, + self._security_group_rule.to_port)), + ('group', self._security_group.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.compute.security_group_rules.create.assert_called_once_with( + self._security_group.id, + self._security_group_rule.ip_protocol, + self._security_group_rule.from_port, + self._security_group_rule.to_port, + self._security_group_rule.ip_range['cidr'], + None, + ) + self.assertEqual(expected_columns, columns) + self.assertEqual(expected_data, data) + + def test_create_source_group(self): + expected_columns, expected_data = self._setup_security_group_rule({ + 'from_port': 22, + 'to_port': 22, + 'group': {'name': self._security_group.name}, + }) + arglist = [ + '--dst-port', str(self._security_group_rule.from_port), + '--src-group', self._security_group.name, + self._security_group.id, + ] + verifylist = [ + ('dst_port', (self._security_group_rule.from_port, + self._security_group_rule.to_port)), + ('src_group', self._security_group.name), + ('group', self._security_group.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.compute.security_group_rules.create.assert_called_once_with( + self._security_group.id, + self._security_group_rule.ip_protocol, + self._security_group_rule.from_port, + self._security_group_rule.to_port, + self._security_group_rule.ip_range['cidr'], + self._security_group.id, + ) + self.assertEqual(expected_columns, columns) + self.assertEqual(expected_data, data) + + def test_create_source_ip(self): + expected_columns, expected_data = self._setup_security_group_rule({ + 'ip_protocol': 'icmp', + 'from_port': -1, + 'to_port': -1, + 'ip_range': {'cidr': '10.0.2.0/24'}, + }) + arglist = [ + '--protocol', self._security_group_rule.ip_protocol, + '--src-ip', self._security_group_rule.ip_range['cidr'], + self._security_group.id, + ] + verifylist = [ + ('protocol', self._security_group_rule.ip_protocol), + ('src_ip', self._security_group_rule.ip_range['cidr']), + ('group', self._security_group.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.compute.security_group_rules.create.assert_called_once_with( + self._security_group.id, + self._security_group_rule.ip_protocol, + self._security_group_rule.from_port, + self._security_group_rule.to_port, + self._security_group_rule.ip_range['cidr'], + None, + ) + self.assertEqual(expected_columns, columns) + self.assertEqual(expected_data, data) + + def test_create_proto_option(self): + expected_columns, expected_data = self._setup_security_group_rule({ + 'ip_protocol': 'icmp', + 'from_port': -1, + 'to_port': -1, + 'ip_range': {'cidr': '10.0.2.0/24'}, + }) + arglist = [ + '--proto', self._security_group_rule.ip_protocol, + '--src-ip', self._security_group_rule.ip_range['cidr'], + self._security_group.id, + ] + verifylist = [ + ('proto', self._security_group_rule.ip_protocol), + ('protocol', None), + ('src_ip', self._security_group_rule.ip_range['cidr']), + ('group', self._security_group.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.compute.security_group_rules.create.assert_called_once_with( + self._security_group.id, + self._security_group_rule.ip_protocol, + self._security_group_rule.from_port, + self._security_group_rule.to_port, + self._security_group_rule.ip_range['cidr'], + None, + ) + self.assertEqual(expected_columns, columns) + self.assertEqual(expected_data, data) + + class TestDeleteSecurityGroupRuleNetwork(TestSecurityGroupRuleNetwork): # The security group rule to be deleted. @@ -68,7 +694,7 @@ class TestDeleteSecurityGroupRuleNetwork(TestSecurityGroupRuleNetwork): result = self.cmd.take_action(parsed_args) - self.network.delete_security_group_rule.assert_called_with( + self.network.delete_security_group_rule.assert_called_once_with( self._security_group_rule) self.assertIsNone(result) @@ -98,11 +724,261 @@ class TestDeleteSecurityGroupRuleCompute(TestSecurityGroupRuleCompute): result = self.cmd.take_action(parsed_args) - self.compute.security_group_rules.delete.assert_called_with( + self.compute.security_group_rules.delete.assert_called_once_with( self._security_group_rule.id) self.assertIsNone(result) +class TestListSecurityGroupRuleNetwork(TestSecurityGroupRuleNetwork): + + # The security group to hold the rules. + _security_group = \ + network_fakes.FakeSecurityGroup.create_one_security_group() + + # The security group rule to be listed. + _security_group_rule_tcp = \ + network_fakes.FakeSecurityGroupRule.create_one_security_group_rule({ + 'protocol': 'tcp', + 'port_range_max': 80, + 'port_range_min': 80, + 'security_group_id': _security_group.id, + }) + _security_group_rule_icmp = \ + network_fakes.FakeSecurityGroupRule.create_one_security_group_rule({ + 'protocol': 'icmp', + 'remote_ip_prefix': '10.0.2.0/24', + 'security_group_id': _security_group.id, + }) + _security_group.security_group_rules = [_security_group_rule_tcp._info, + _security_group_rule_icmp._info] + _security_group_rules = [_security_group_rule_tcp, + _security_group_rule_icmp] + + expected_columns_with_group_and_long = ( + 'ID', + 'IP Protocol', + 'IP Range', + 'Port Range', + 'Direction', + 'Ethertype', + 'Remote Security Group', + ) + expected_columns_no_group = ( + 'ID', + 'IP Protocol', + 'IP Range', + 'Port Range', + 'Remote Security Group', + 'Security Group', + ) + + expected_data_with_group_and_long = [] + expected_data_no_group = [] + for _security_group_rule in _security_group_rules: + expected_data_with_group_and_long.append(( + _security_group_rule.id, + _security_group_rule.protocol, + _security_group_rule.remote_ip_prefix, + security_group_rule._format_network_port_range( + _security_group_rule), + _security_group_rule.direction, + _security_group_rule.ethertype, + _security_group_rule.remote_group_id, + )) + expected_data_no_group.append(( + _security_group_rule.id, + _security_group_rule.protocol, + _security_group_rule.remote_ip_prefix, + security_group_rule._format_network_port_range( + _security_group_rule), + _security_group_rule.remote_group_id, + _security_group_rule.security_group_id, + )) + + def setUp(self): + super(TestListSecurityGroupRuleNetwork, self).setUp() + + self.network.find_security_group = mock.Mock( + return_value=self._security_group) + self.network.security_group_rules = mock.Mock( + return_value=self._security_group_rules) + + # Get the command object to test + self.cmd = security_group_rule.ListSecurityGroupRule( + self.app, self.namespace) + + def test_list_default(self): + self._security_group_rule_tcp.port_range_min = 80 + parsed_args = self.check_parser(self.cmd, [], []) + + columns, data = self.cmd.take_action(parsed_args) + + self.network.security_group_rules.assert_called_once_with(**{}) + self.assertEqual(self.expected_columns_no_group, columns) + self.assertEqual(self.expected_data_no_group, list(data)) + + def test_list_with_group_and_long(self): + self._security_group_rule_tcp.port_range_min = 80 + arglist = [ + '--long', + self._security_group.id, + ] + verifylist = [ + ('long', True), + ('group', self._security_group.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.network.security_group_rules.assert_called_once_with(**{ + 'security_group_id': self._security_group.id, + }) + self.assertEqual(self.expected_columns_with_group_and_long, columns) + self.assertEqual(self.expected_data_with_group_and_long, list(data)) + + def test_list_with_ignored_options(self): + self._security_group_rule_tcp.port_range_min = 80 + arglist = [ + '--all-projects', + ] + verifylist = [ + ('all_projects', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.network.security_group_rules.assert_called_once_with(**{}) + self.assertEqual(self.expected_columns_no_group, columns) + self.assertEqual(self.expected_data_no_group, list(data)) + + +class TestListSecurityGroupRuleCompute(TestSecurityGroupRuleCompute): + + # The security group to hold the rules. + _security_group = \ + compute_fakes.FakeSecurityGroup.create_one_security_group() + + # The security group rule to be listed. + _security_group_rule_tcp = \ + compute_fakes.FakeSecurityGroupRule.create_one_security_group_rule({ + 'ip_protocol': 'tcp', + 'from_port': 80, + 'to_port': 80, + 'group': {'name': _security_group.name}, + }) + _security_group_rule_icmp = \ + compute_fakes.FakeSecurityGroupRule.create_one_security_group_rule({ + 'ip_protocol': 'icmp', + 'from_port': -1, + 'to_port': -1, + 'ip_range': {'cidr': '10.0.2.0/24'}, + 'group': {'name': _security_group.name}, + }) + _security_group.rules = [_security_group_rule_tcp._info, + _security_group_rule_icmp._info] + + expected_columns_with_group = ( + 'ID', + 'IP Protocol', + 'IP Range', + 'Port Range', + 'Remote Security Group', + ) + expected_columns_no_group = \ + expected_columns_with_group + ('Security Group',) + + expected_data_with_group = [] + expected_data_no_group = [] + for _security_group_rule in _security_group.rules: + rule = network_utils.transform_compute_security_group_rule( + _security_group_rule + ) + expected_rule_with_group = ( + rule['id'], + rule['ip_protocol'], + rule['ip_range'], + rule['port_range'], + rule['remote_security_group'], + ) + expected_rule_no_group = expected_rule_with_group + \ + (_security_group_rule['parent_group_id'],) + expected_data_with_group.append(expected_rule_with_group) + expected_data_no_group.append(expected_rule_no_group) + + def setUp(self): + super(TestListSecurityGroupRuleCompute, self).setUp() + + self.app.client_manager.network_endpoint_enabled = False + + self.compute.security_groups.get.return_value = \ + self._security_group + self.compute.security_groups.list.return_value = \ + [self._security_group] + + # Get the command object to test + self.cmd = security_group_rule.ListSecurityGroupRule(self.app, None) + + def test_list_default(self): + parsed_args = self.check_parser(self.cmd, [], []) + + columns, data = self.cmd.take_action(parsed_args) + self.compute.security_groups.list.assert_called_once_with( + search_opts={'all_tenants': False} + ) + self.assertEqual(self.expected_columns_no_group, columns) + self.assertEqual(self.expected_data_no_group, list(data)) + + def test_list_with_group(self): + arglist = [ + self._security_group.id, + ] + verifylist = [ + ('group', self._security_group.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + self.compute.security_groups.get.assert_called_once_with( + self._security_group.id + ) + self.assertEqual(self.expected_columns_with_group, columns) + self.assertEqual(self.expected_data_with_group, list(data)) + + def test_list_all_projects(self): + arglist = [ + '--all-projects', + ] + verifylist = [ + ('all_projects', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + self.compute.security_groups.list.assert_called_once_with( + search_opts={'all_tenants': True} + ) + self.assertEqual(self.expected_columns_no_group, columns) + self.assertEqual(self.expected_data_no_group, list(data)) + + def test_list_with_ignored_options(self): + arglist = [ + '--long', + ] + verifylist = [ + ('long', False), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + self.compute.security_groups.list.assert_called_once_with( + search_opts={'all_tenants': False} + ) + self.assertEqual(self.expected_columns_no_group, columns) + self.assertEqual(self.expected_data_no_group, list(data)) + + class TestShowSecurityGroupRuleNetwork(TestSecurityGroupRuleNetwork): # The security group rule to be shown. @@ -160,9 +1036,9 @@ class TestShowSecurityGroupRuleNetwork(TestSecurityGroupRuleNetwork): columns, data = self.cmd.take_action(parsed_args) - self.network.find_security_group_rule.assert_called_with( + self.network.find_security_group_rule.assert_called_once_with( self._security_group_rule.id, ignore_missing=False) - self.assertEqual(tuple(self.columns), columns) + self.assertEqual(self.columns, columns) self.assertEqual(self.data, data) @@ -207,6 +1083,6 @@ class TestShowSecurityGroupRuleCompute(TestSecurityGroupRuleCompute): columns, data = self.cmd.take_action(parsed_args) - self.compute.security_groups.list.assert_called_with() + self.compute.security_groups.list.assert_called_once_with() self.assertEqual(self.columns, columns) self.assertEqual(self.data, data) diff --git a/openstackclient/tests/network/v2/test_subnet.py b/openstackclient/tests/network/v2/test_subnet.py index a95635f..22c288f 100644 --- a/openstackclient/tests/network/v2/test_subnet.py +++ b/openstackclient/tests/network/v2/test_subnet.py @@ -11,10 +11,14 @@ # under the License. # +import copy import mock +from openstackclient.common import exceptions from openstackclient.common import utils from openstackclient.network.v2 import subnet as subnet_v2 +from openstackclient.tests import fakes +from openstackclient.tests.identity.v3 import fakes as identity_fakes_v3 from openstackclient.tests.network.v2 import fakes as network_fakes from openstackclient.tests import utils as tests_utils @@ -28,6 +32,333 @@ class TestSubnet(network_fakes.TestNetworkV2): self.network = self.app.client_manager.network +class TestCreateSubnet(TestSubnet): + + # An IPv4 subnet to be created with mostly default values + _subnet = network_fakes.FakeSubnet.create_one_subnet( + attrs={ + 'tenant_id': identity_fakes_v3.project_id, + } + ) + + # Subnet pool to be used to create a subnet from a pool + _subnet_pool = network_fakes.FakeSubnetPool.create_one_subnet_pool() + + # An IPv4 subnet to be created using a specific subnet pool + _subnet_from_pool = network_fakes.FakeSubnet.create_one_subnet( + attrs={ + 'tenant_id': identity_fakes_v3.project_id, + 'subnetpool_id': _subnet_pool.id, + 'dns_nameservers': ['8.8.8.8', + '8.8.4.4'], + 'host_routes': [{'destination': '10.20.20.0/24', + 'nexthop': '10.20.20.1'}, + {'destination': '10.30.30.0/24', + 'nexthop': '10.30.30.1'}], + } + ) + + # An IPv6 subnet to be created with most options specified + _subnet_ipv6 = network_fakes.FakeSubnet.create_one_subnet( + attrs={ + 'tenant_id': identity_fakes_v3.project_id, + 'cidr': 'fe80:0:0:a00a::/64', + 'enable_dhcp': True, + 'dns_nameservers': ['fe80:27ff:a00a:f00f::ffff', + 'fe80:37ff:a00a:f00f::ffff'], + 'allocation_pools': [{'start': 'fe80::a00a:0:c0de:0:100', + 'end': 'fe80::a00a:0:c0de:0:f000'}, + {'start': 'fe80::a00a:0:c0de:1:100', + 'end': 'fe80::a00a:0:c0de:1:f000'}], + 'host_routes': [{'destination': 'fe80:27ff:a00a:f00f::/64', + 'nexthop': 'fe80:27ff:a00a:f00f::1'}, + {'destination': 'fe80:37ff:a00a:f00f::/64', + 'nexthop': 'fe80:37ff:a00a:f00f::1'}], + 'ip_version': 6, + 'gateway_ip': 'fe80::a00a:0:c0de:0:1', + 'ipv6_address_mode': 'slaac', + 'ipv6_ra_mode': 'slaac', + 'subnetpool_id': 'None', + } + ) + + # The network to be returned from find_network + _network = network_fakes.FakeNetwork.create_one_network( + attrs={ + 'id': _subnet.network_id, + } + ) + + columns = ( + 'allocation_pools', + 'cidr', + 'dns_nameservers', + 'enable_dhcp', + 'gateway_ip', + 'host_routes', + 'id', + 'ip_version', + 'ipv6_address_mode', + 'ipv6_ra_mode', + 'name', + 'network_id', + 'project_id', + 'subnetpool_id', + ) + + data = ( + subnet_v2._format_allocation_pools(_subnet.allocation_pools), + _subnet.cidr, + utils.format_list(_subnet.dns_nameservers), + _subnet.enable_dhcp, + _subnet.gateway_ip, + subnet_v2._format_host_routes(_subnet.host_routes), + _subnet.id, + _subnet.ip_version, + _subnet.ipv6_address_mode, + _subnet.ipv6_ra_mode, + _subnet.name, + _subnet.network_id, + _subnet.project_id, + _subnet.subnetpool_id, + ) + + data_subnet_pool = ( + subnet_v2._format_allocation_pools(_subnet_from_pool.allocation_pools), + _subnet_from_pool.cidr, + utils.format_list(_subnet_from_pool.dns_nameservers), + _subnet_from_pool.enable_dhcp, + _subnet_from_pool.gateway_ip, + subnet_v2._format_host_routes(_subnet_from_pool.host_routes), + _subnet_from_pool.id, + _subnet_from_pool.ip_version, + _subnet_from_pool.ipv6_address_mode, + _subnet_from_pool.ipv6_ra_mode, + _subnet_from_pool.name, + _subnet_from_pool.network_id, + _subnet_from_pool.project_id, + _subnet_from_pool.subnetpool_id, + ) + + data_ipv6 = ( + subnet_v2._format_allocation_pools(_subnet_ipv6.allocation_pools), + _subnet_ipv6.cidr, + utils.format_list(_subnet_ipv6.dns_nameservers), + _subnet_ipv6.enable_dhcp, + _subnet_ipv6.gateway_ip, + subnet_v2._format_host_routes(_subnet_ipv6.host_routes), + _subnet_ipv6.id, + _subnet_ipv6.ip_version, + _subnet_ipv6.ipv6_address_mode, + _subnet_ipv6.ipv6_ra_mode, + _subnet_ipv6.name, + _subnet_ipv6.network_id, + _subnet_ipv6.project_id, + _subnet_ipv6.subnetpool_id, + ) + + def setUp(self): + super(TestCreateSubnet, self).setUp() + + # Get the command object to test + self.cmd = subnet_v2.CreateSubnet(self.app, self.namespace) + + # Set identity client v3. And get a shortcut to Identity client. + identity_client = identity_fakes_v3.FakeIdentityv3Client( + endpoint=fakes.AUTH_URL, + token=fakes.AUTH_TOKEN, + ) + self.app.client_manager.identity = identity_client + self.identity = self.app.client_manager.identity + + # Get a shortcut to the ProjectManager Mock + self.projects_mock = self.identity.projects + self.projects_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes_v3.PROJECT), + loaded=True, + ) + + # Get a shortcut to the DomainManager Mock + self.domains_mock = self.identity.domains + self.domains_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes_v3.DOMAIN), + loaded=True, + ) + + def test_create_no_options(self): + arglist = [] + verifylist = [] + + # Testing that a call without the required argument will fail and + # throw a "ParserExecption" + self.assertRaises(tests_utils.ParserException, + self.check_parser, self.cmd, arglist, verifylist) + + def test_create_default_options(self): + # Mock create_subnet and find_network sdk calls to return the + # values we want for this test + self.network.create_subnet = mock.Mock(return_value=self._subnet) + self._network.id = self._subnet.network_id + self.network.find_network = mock.Mock(return_value=self._network) + + arglist = [ + "--subnet-range", self._subnet.cidr, + "--network", self._subnet.network_id, + self._subnet.name, + ] + verifylist = [ + ('name', self._subnet.name), + ('subnet_range', self._subnet.cidr), + ('network', self._subnet.network_id), + ('ip_version', self._subnet.ip_version), + ('gateway', 'auto'), + + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.network.create_subnet.assert_called_once_with(**{ + 'cidr': self._subnet.cidr, + 'enable_dhcp': self._subnet.enable_dhcp, + 'ip_version': self._subnet.ip_version, + 'name': self._subnet.name, + 'network_id': self._subnet.network_id, + }) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + def test_create_from_subnet_pool_options(self): + # Mock create_subnet, find_subnet_pool, and find_network sdk calls + # to return the values we want for this test + self.network.create_subnet = \ + mock.Mock(return_value=self._subnet_from_pool) + self._network.id = self._subnet_from_pool.network_id + self.network.find_network = mock.Mock(return_value=self._network) + self.network.find_subnet_pool = \ + mock.Mock(return_value=self._subnet_pool) + + arglist = [ + self._subnet_from_pool.name, + "--subnet-pool", self._subnet_from_pool.subnetpool_id, + "--prefix-length", '24', + "--network", self._subnet_from_pool.network_id, + "--ip-version", str(self._subnet_from_pool.ip_version), + "--gateway", self._subnet_from_pool.gateway_ip, + "--dhcp", + ] + + for dns_addr in self._subnet_from_pool.dns_nameservers: + arglist.append('--dns-nameserver') + arglist.append(dns_addr) + + for host_route in self._subnet_from_pool.host_routes: + arglist.append('--host-route') + value = 'gateway=' + host_route.get('nexthop', '') + \ + ',destination=' + host_route.get('destination', '') + arglist.append(value) + + verifylist = [ + ('name', self._subnet_from_pool.name), + ('prefix_length', '24'), + ('network', self._subnet_from_pool.network_id), + ('ip_version', self._subnet_from_pool.ip_version), + ('gateway', self._subnet_from_pool.gateway_ip), + ('dns_nameservers', self._subnet_from_pool.dns_nameservers), + ('dhcp', self._subnet_from_pool.enable_dhcp), + ('host_routes', subnet_v2.convert_entries_to_gateway( + self._subnet_from_pool.host_routes)), + ('subnet_pool', self._subnet_from_pool.subnetpool_id), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.network.create_subnet.assert_called_once_with(**{ + 'dns_nameservers': self._subnet_from_pool.dns_nameservers, + 'enable_dhcp': self._subnet_from_pool.enable_dhcp, + 'gateway_ip': self._subnet_from_pool.gateway_ip, + 'host_routes': self._subnet_from_pool.host_routes, + 'ip_version': self._subnet_from_pool.ip_version, + 'name': self._subnet_from_pool.name, + 'network_id': self._subnet_from_pool.network_id, + 'prefixlen': '24', + 'subnetpool_id': self._subnet_from_pool.subnetpool_id, + }) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data_subnet_pool, data) + + def test_create_options_subnet_range_ipv6(self): + # Mock create_subnet and find_network sdk calls to return the + # values we want for this test + self.network.create_subnet = mock.Mock(return_value=self._subnet_ipv6) + self._network.id = self._subnet_ipv6.network_id + self.network.find_network = mock.Mock(return_value=self._network) + + arglist = [ + self._subnet_ipv6.name, + "--subnet-range", self._subnet_ipv6.cidr, + "--network", self._subnet_ipv6.network_id, + "--ip-version", str(self._subnet_ipv6.ip_version), + "--ipv6-ra-mode", self._subnet_ipv6.ipv6_ra_mode, + "--ipv6-address-mode", self._subnet_ipv6.ipv6_address_mode, + "--gateway", self._subnet_ipv6.gateway_ip, + "--dhcp", + ] + + for dns_addr in self._subnet_ipv6.dns_nameservers: + arglist.append('--dns-nameserver') + arglist.append(dns_addr) + + for host_route in self._subnet_ipv6.host_routes: + arglist.append('--host-route') + value = 'gateway=' + host_route.get('nexthop', '') + \ + ',destination=' + host_route.get('destination', '') + arglist.append(value) + + for pool in self._subnet_ipv6.allocation_pools: + arglist.append('--allocation-pool') + value = 'start=' + pool.get('start', '') + \ + ',end=' + pool.get('end', '') + arglist.append(value) + + verifylist = [ + ('name', self._subnet_ipv6.name), + ('subnet_range', self._subnet_ipv6.cidr), + ('network', self._subnet_ipv6.network_id), + ('ip_version', self._subnet_ipv6.ip_version), + ('ipv6_ra_mode', self._subnet_ipv6.ipv6_ra_mode), + ('ipv6_address_mode', self._subnet_ipv6.ipv6_address_mode), + ('gateway', self._subnet_ipv6.gateway_ip), + ('dns_nameservers', self._subnet_ipv6.dns_nameservers), + ('dhcp', self._subnet_ipv6.enable_dhcp), + ('host_routes', subnet_v2.convert_entries_to_gateway( + self._subnet_ipv6.host_routes)), + ('allocation_pools', self._subnet_ipv6.allocation_pools), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.network.create_subnet.assert_called_once_with(**{ + 'cidr': self._subnet_ipv6.cidr, + 'dns_nameservers': self._subnet_ipv6.dns_nameservers, + 'enable_dhcp': self._subnet_ipv6.enable_dhcp, + 'gateway_ip': self._subnet_ipv6.gateway_ip, + 'host_routes': self._subnet_ipv6.host_routes, + 'ip_version': self._subnet_ipv6.ip_version, + 'ipv6_address_mode': self._subnet_ipv6.ipv6_address_mode, + 'ipv6_ra_mode': self._subnet_ipv6.ipv6_ra_mode, + 'name': self._subnet_ipv6.name, + 'network_id': self._subnet_ipv6.network_id, + 'allocation_pools': self._subnet_ipv6.allocation_pools, + }) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data_ipv6, data) + + class TestDeleteSubnet(TestSubnet): # The subnet to delete. @@ -53,7 +384,7 @@ class TestDeleteSubnet(TestSubnet): parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) - self.network.delete_subnet.assert_called_with(self._subnet) + self.network.delete_subnet.assert_called_once_with(self._subnet) self.assertIsNone(result) @@ -65,7 +396,7 @@ class TestListSubnet(TestSubnet): 'ID', 'Name', 'Network', - 'Subnet' + 'Subnet', ) columns_long = columns + ( 'Project', @@ -74,7 +405,7 @@ class TestListSubnet(TestSubnet): 'Allocation Pools', 'Host Routes', 'IP Version', - 'Gateway' + 'Gateway', ) data = [] @@ -99,7 +430,7 @@ class TestListSubnet(TestSubnet): subnet_v2._format_allocation_pools(subnet.allocation_pools), utils.format_list(subnet.host_routes), subnet.ip_version, - subnet.gateway_ip + subnet.gateway_ip, )) def setUp(self): @@ -119,7 +450,7 @@ class TestListSubnet(TestSubnet): columns, data = self.cmd.take_action(parsed_args) - self.network.subnets.assert_called_with() + self.network.subnets.assert_called_once_with() self.assertEqual(self.columns, columns) self.assertEqual(self.data, list(data)) @@ -134,10 +465,113 @@ class TestListSubnet(TestSubnet): columns, data = self.cmd.take_action(parsed_args) - self.network.subnets.assert_called_with() + self.network.subnets.assert_called_once_with() self.assertEqual(self.columns_long, columns) self.assertEqual(self.data_long, list(data)) + def test_subnet_list_ip_version(self): + arglist = [ + '--ip-version', str(4), + ] + verifylist = [ + ('ip_version', 4), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + filters = {'ip_version': 4} + + self.network.subnets.assert_called_once_with(**filters) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, list(data)) + + +class TestSetSubnet(TestSubnet): + + _subnet = network_fakes.FakeSubnet.create_one_subnet() + + def setUp(self): + super(TestSetSubnet, self).setUp() + self.network.update_subnet = mock.Mock(return_value=None) + self.network.find_subnet = mock.Mock(return_value=self._subnet) + self.cmd = subnet_v2.SetSubnet(self.app, self.namespace) + + def test_set_this(self): + arglist = [ + "--name", "new_subnet", + "--dhcp", + "--gateway", self._subnet.gateway_ip, + self._subnet.name, + ] + verifylist = [ + ('name', "new_subnet"), + ('dhcp', True), + ('gateway', self._subnet.gateway_ip), + ('subnet', self._subnet.name), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + attrs = { + 'enable_dhcp': True, + 'gateway_ip': self._subnet.gateway_ip, + 'name': "new_subnet", + } + self.network.update_subnet.assert_called_with(self._subnet, **attrs) + self.assertIsNone(result) + + def test_set_that(self): + arglist = [ + "--name", "new_subnet", + "--no-dhcp", + "--gateway", "none", + self._subnet.name, + ] + verifylist = [ + ('name', "new_subnet"), + ('no_dhcp', True), + ('gateway', "none"), + ('subnet', self._subnet.name), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + attrs = { + 'enable_dhcp': False, + 'gateway_ip': None, + 'name': "new_subnet", + } + self.network.update_subnet.assert_called_with(self._subnet, **attrs) + self.assertIsNone(result) + + def test_set_nothing(self): + arglist = [self._subnet.name, ] + verifylist = [('subnet', self._subnet.name)] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.assertRaises(exceptions.CommandError, self.cmd.take_action, + parsed_args) + + def test_append_options(self): + _testsubnet = network_fakes.FakeSubnet.create_one_subnet( + {'dns_nameservers': ["10.0.0.1"]}) + self.network.find_subnet = mock.Mock(return_value=_testsubnet) + arglist = [ + '--dns-nameserver', '10.0.0.2', + _testsubnet.name, + ] + verifylist = [ + ('dns_nameservers', ['10.0.0.2']), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + attrs = { + 'dns_nameservers': ['10.0.0.2', '10.0.0.1'], + } + self.network.update_subnet.assert_called_once_with( + _testsubnet, **attrs) + self.assertIsNone(result) + class TestShowSubnet(TestSubnet): # The subnets to be shown @@ -205,8 +639,8 @@ class TestShowSubnet(TestSubnet): parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.network.find_subnet.assert_called_with(self._subnet.name, - ignore_missing=False) + self.network.find_subnet.assert_called_once_with( + self._subnet.name, ignore_missing=False) self.assertEqual(self.columns, columns) - self.assertEqual(list(self.data), list(data)) + self.assertEqual(self.data, data) diff --git a/openstackclient/tests/network/v2/test_subnet_pool.py b/openstackclient/tests/network/v2/test_subnet_pool.py index c4e3340..de12c9e 100644 --- a/openstackclient/tests/network/v2/test_subnet_pool.py +++ b/openstackclient/tests/network/v2/test_subnet_pool.py @@ -11,10 +11,15 @@ # under the License. # +import argparse +import copy import mock +from openstackclient.common import exceptions from openstackclient.common import utils from openstackclient.network.v2 import subnet_pool +from openstackclient.tests import fakes +from openstackclient.tests.identity.v3 import fakes as identity_fakes_v3 from openstackclient.tests.network.v2 import fakes as network_fakes from openstackclient.tests import utils as tests_utils @@ -28,6 +33,234 @@ class TestSubnetPool(network_fakes.TestNetworkV2): self.network = self.app.client_manager.network +class TestCreateSubnetPool(TestSubnetPool): + + # The new subnet pool to create. + _subnet_pool = network_fakes.FakeSubnetPool.create_one_subnet_pool() + + _address_scope = network_fakes.FakeAddressScope.create_one_address_scope() + + columns = ( + 'address_scope_id', + 'default_prefixlen', + 'default_quota', + 'id', + 'ip_version', + 'is_default', + 'max_prefixlen', + 'min_prefixlen', + 'name', + 'prefixes', + 'project_id', + 'shared', + ) + data = ( + _subnet_pool.address_scope_id, + _subnet_pool.default_prefixlen, + _subnet_pool.default_quota, + _subnet_pool.id, + _subnet_pool.ip_version, + _subnet_pool.is_default, + _subnet_pool.max_prefixlen, + _subnet_pool.min_prefixlen, + _subnet_pool.name, + utils.format_list(_subnet_pool.prefixes), + _subnet_pool.project_id, + _subnet_pool.shared, + ) + + def setUp(self): + super(TestCreateSubnetPool, self).setUp() + + self.network.create_subnet_pool = mock.Mock( + return_value=self._subnet_pool) + + # Get the command object to test + self.cmd = subnet_pool.CreateSubnetPool(self.app, self.namespace) + + self.network.find_address_scope = mock.Mock( + return_value=self._address_scope) + + # Set identity client. And get a shortcut to Identity client. + identity_client = identity_fakes_v3.FakeIdentityv3Client( + endpoint=fakes.AUTH_URL, + token=fakes.AUTH_TOKEN, + ) + self.app.client_manager.identity = identity_client + self.identity = self.app.client_manager.identity + + # Get a shortcut to the ProjectManager Mock + self.projects_mock = self.identity.projects + self.projects_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes_v3.PROJECT), + loaded=True, + ) + + # Get a shortcut to the DomainManager Mock + self.domains_mock = self.identity.domains + self.domains_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes_v3.DOMAIN), + loaded=True, + ) + + def test_create_no_options(self): + arglist = [] + verifylist = [] + + self.assertRaises(tests_utils.ParserException, self.check_parser, + self.cmd, arglist, verifylist) + + def test_create_no_pool_prefix(self): + """Make sure --pool-prefix is a required argument""" + arglist = [ + self._subnet_pool.name, + ] + verifylist = [ + ('name', self._subnet_pool.name), + ] + self.assertRaises(tests_utils.ParserException, self.check_parser, + self.cmd, arglist, verifylist) + + def test_create_default_options(self): + arglist = [ + '--pool-prefix', '10.0.10.0/24', + self._subnet_pool.name, + ] + verifylist = [ + ('prefixes', ['10.0.10.0/24']), + ('name', self._subnet_pool.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = (self.cmd.take_action(parsed_args)) + + self.network.create_subnet_pool.assert_called_once_with(**{ + 'prefixes': ['10.0.10.0/24'], + 'name': self._subnet_pool.name, + }) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + def test_create_prefixlen_options(self): + arglist = [ + '--default-prefix-length', self._subnet_pool.default_prefixlen, + '--max-prefix-length', self._subnet_pool.max_prefixlen, + '--min-prefix-length', self._subnet_pool.min_prefixlen, + '--pool-prefix', '10.0.10.0/24', + self._subnet_pool.name, + ] + verifylist = [ + ('default_prefix_length', + int(self._subnet_pool.default_prefixlen)), + ('max_prefix_length', int(self._subnet_pool.max_prefixlen)), + ('min_prefix_length', int(self._subnet_pool.min_prefixlen)), + ('name', self._subnet_pool.name), + ('prefixes', ['10.0.10.0/24']), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = (self.cmd.take_action(parsed_args)) + + self.network.create_subnet_pool.assert_called_once_with(**{ + 'default_prefixlen': int(self._subnet_pool.default_prefixlen), + 'max_prefixlen': int(self._subnet_pool.max_prefixlen), + 'min_prefixlen': int(self._subnet_pool.min_prefixlen), + 'prefixes': ['10.0.10.0/24'], + 'name': self._subnet_pool.name, + }) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + def test_create_len_negative(self): + arglist = [ + self._subnet_pool.name, + '--min-prefix-length', '-16', + ] + verifylist = [ + ('subnet_pool', self._subnet_pool.name), + ('min_prefix_length', '-16'), + ] + + self.assertRaises(argparse.ArgumentTypeError, self.check_parser, + self.cmd, arglist, verifylist) + + def test_create_project_domain(self): + arglist = [ + '--pool-prefix', '10.0.10.0/24', + "--project", identity_fakes_v3.project_name, + "--project-domain", identity_fakes_v3.domain_name, + self._subnet_pool.name, + ] + verifylist = [ + ('prefixes', ['10.0.10.0/24']), + ('project', identity_fakes_v3.project_name), + ('project_domain', identity_fakes_v3.domain_name), + ('name', self._subnet_pool.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = (self.cmd.take_action(parsed_args)) + + self.network.create_subnet_pool.assert_called_once_with(**{ + 'prefixes': ['10.0.10.0/24'], + 'tenant_id': identity_fakes_v3.project_id, + 'name': self._subnet_pool.name, + }) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + def test_create_address_scope_option(self): + arglist = [ + '--pool-prefix', '10.0.10.0/24', + '--address-scope', self._address_scope.id, + self._subnet_pool.name, + ] + verifylist = [ + ('prefixes', ['10.0.10.0/24']), + ('address_scope', self._address_scope.id), + ('name', self._subnet_pool.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = (self.cmd.take_action(parsed_args)) + + self.network.create_subnet_pool.assert_called_once_with(**{ + 'prefixes': ['10.0.10.0/24'], + 'address_scope_id': self._address_scope.id, + 'name': self._subnet_pool.name, + }) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + def test_create_default_and_shared_options(self): + arglist = [ + '--pool-prefix', '10.0.10.0/24', + '--default', + '--share', + self._subnet_pool.name, + ] + verifylist = [ + ('prefixes', ['10.0.10.0/24']), + ('default', True), + ('share', True), + ('name', self._subnet_pool.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = (self.cmd.take_action(parsed_args)) + + self.network.create_subnet_pool.assert_called_once_with(**{ + 'is_default': True, + 'name': self._subnet_pool.name, + 'prefixes': ['10.0.10.0/24'], + 'shared': True, + }) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + class TestDeleteSubnetPool(TestSubnetPool): # The subnet pool to delete. @@ -56,7 +289,8 @@ class TestDeleteSubnetPool(TestSubnetPool): result = self.cmd.take_action(parsed_args) - self.network.delete_subnet_pool.assert_called_with(self._subnet_pool) + self.network.delete_subnet_pool.assert_called_once_with( + self._subnet_pool) self.assertIsNone(result) @@ -72,6 +306,8 @@ class TestListSubnetPool(TestSubnetPool): columns_long = columns + ( 'Default Prefix Length', 'Address Scope', + 'Default Subnet Pool', + 'Shared', ) data = [] @@ -79,7 +315,7 @@ class TestListSubnetPool(TestSubnetPool): data.append(( pool.id, pool.name, - pool.prefixes, + utils.format_list(pool.prefixes), )) data_long = [] @@ -87,9 +323,11 @@ class TestListSubnetPool(TestSubnetPool): data_long.append(( pool.id, pool.name, - pool.prefixes, + utils.format_list(pool.prefixes), pool.default_prefixlen, pool.address_scope_id, + pool.is_default, + pool.shared, )) def setUp(self): @@ -109,7 +347,7 @@ class TestListSubnetPool(TestSubnetPool): columns, data = self.cmd.take_action(parsed_args) - self.network.subnet_pools.assert_called_with() + self.network.subnet_pools.assert_called_once_with() self.assertEqual(self.columns, columns) self.assertEqual(self.data, list(data)) @@ -124,11 +362,218 @@ class TestListSubnetPool(TestSubnetPool): columns, data = self.cmd.take_action(parsed_args) - self.network.subnet_pools.assert_called_with() + self.network.subnet_pools.assert_called_once_with() self.assertEqual(self.columns_long, columns) self.assertEqual(self.data_long, list(data)) +class TestSetSubnetPool(TestSubnetPool): + + # The subnet_pool to set. + _subnet_pool = network_fakes.FakeSubnetPool.create_one_subnet_pool() + + _address_scope = network_fakes.FakeAddressScope.create_one_address_scope() + + def setUp(self): + super(TestSetSubnetPool, self).setUp() + + self.network.update_subnet_pool = mock.Mock(return_value=None) + + self.network.find_subnet_pool = mock.Mock( + return_value=self._subnet_pool) + + self.network.find_address_scope = mock.Mock( + return_value=self._address_scope) + + # Get the command object to test + self.cmd = subnet_pool.SetSubnetPool(self.app, self.namespace) + + def test_set_this(self): + arglist = [ + '--name', 'noob', + '--default-prefix-length', '8', + '--min-prefix-length', '8', + self._subnet_pool.name, + ] + verifylist = [ + ('name', 'noob'), + ('default_prefix_length', 8), + ('min_prefix_length', 8), + ('subnet_pool', self._subnet_pool.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + attrs = { + 'name': 'noob', + 'default_prefixlen': 8, + 'min_prefixlen': 8, + } + self.network.update_subnet_pool.assert_called_once_with( + self._subnet_pool, **attrs) + self.assertIsNone(result) + + def test_set_that(self): + arglist = [ + '--pool-prefix', '10.0.1.0/24', + '--pool-prefix', '10.0.2.0/24', + '--max-prefix-length', '16', + self._subnet_pool.name, + ] + verifylist = [ + ('prefixes', ['10.0.1.0/24', '10.0.2.0/24']), + ('max_prefix_length', 16), + ('subnet_pool', self._subnet_pool.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + prefixes = ['10.0.1.0/24', '10.0.2.0/24'] + prefixes.extend(self._subnet_pool.prefixes) + attrs = { + 'prefixes': prefixes, + 'max_prefixlen': 16, + } + self.network.update_subnet_pool.assert_called_once_with( + self._subnet_pool, **attrs) + self.assertIsNone(result) + + def test_set_nothing(self): + arglist = [self._subnet_pool.name, ] + verifylist = [('subnet_pool', self._subnet_pool.name), ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.assertRaises(exceptions.CommandError, self.cmd.take_action, + parsed_args) + + def test_set_len_negative(self): + arglist = [ + '--max-prefix-length', '-16', + self._subnet_pool.name, + ] + verifylist = [ + ('max_prefix_length', '-16'), + ('subnet_pool', self._subnet_pool.name), + ] + + self.assertRaises(argparse.ArgumentTypeError, self.check_parser, + self.cmd, arglist, verifylist) + + def test_set_address_scope(self): + arglist = [ + '--address-scope', self._address_scope.id, + self._subnet_pool.name, + ] + verifylist = [ + ('address_scope', self._address_scope.id), + ('subnet_pool', self._subnet_pool.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + attrs = { + 'address_scope_id': self._address_scope.id, + } + self.network.update_subnet_pool.assert_called_once_with( + self._subnet_pool, **attrs) + self.assertIsNone(result) + + def test_set_no_address_scope(self): + arglist = [ + '--no-address-scope', + self._subnet_pool.name, + ] + verifylist = [ + ('no_address_scope', True), + ('subnet_pool', self._subnet_pool.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + attrs = { + 'address_scope_id': None, + } + self.network.update_subnet_pool.assert_called_once_with( + self._subnet_pool, **attrs) + self.assertIsNone(result) + + def test_set_no_address_scope_conflict(self): + arglist = [ + '--address-scope', self._address_scope.id, + '--no-address-scope', + self._subnet_pool.name, + ] + verifylist = [ + ('address_scope', self._address_scope.id), + ('no_address_scope', True), + ('subnet_pool', self._subnet_pool.name), + ] + + # Exclusive arguments will conflict here. + self.assertRaises(tests_utils.ParserException, self.check_parser, + self.cmd, arglist, verifylist) + + def test_set_default(self): + arglist = [ + '--default', + self._subnet_pool.name, + ] + verifylist = [ + ('default', True), + ('subnet_pool', self._subnet_pool.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + attrs = { + 'is_default': True + } + self.network.update_subnet_pool.assert_called_once_with( + self._subnet_pool, **attrs) + self.assertIsNone(result) + + def test_set_no_default(self): + arglist = [ + '--no-default', + self._subnet_pool.name, + ] + verifylist = [ + ('no_default', True), + ('subnet_pool', self._subnet_pool.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + attrs = { + 'is_default': False, + } + self.network.update_subnet_pool.assert_called_once_with( + self._subnet_pool, **attrs) + self.assertIsNone(result) + + def test_set_no_default_conflict(self): + arglist = [ + '--default', + '--no-default', + self._subnet_pool.name, + ] + verifylist = [ + ('default', True), + ('no_default', True), + ('subnet_pool', self._subnet_pool.name), + ] + + # Exclusive arguments will conflict here. + self.assertRaises(tests_utils.ParserException, self.check_parser, + self.cmd, arglist, verifylist) + + class TestShowSubnetPool(TestSubnetPool): # The subnet_pool to set. @@ -178,7 +623,6 @@ class TestShowSubnetPool(TestSubnetPool): arglist = [] verifylist = [] - # Missing required args should bail here self.assertRaises(tests_utils.ParserException, self.check_parser, self.cmd, arglist, verifylist) @@ -189,14 +633,13 @@ class TestShowSubnetPool(TestSubnetPool): verifylist = [ ('subnet_pool', self._subnet_pool.name), ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) - self.network.find_subnet_pool.assert_called_with( + self.network.find_subnet_pool.assert_called_once_with( self._subnet_pool.name, ignore_missing=False ) - self.assertEqual(self.columns, columns) self.assertEqual(self.data, data) diff --git a/openstackclient/tests/test_shell.py b/openstackclient/tests/test_shell.py index ea3c6fe..90454fc 100644 --- a/openstackclient/tests/test_shell.py +++ b/openstackclient/tests/test_shell.py @@ -14,6 +14,7 @@ # import copy +import fixtures import mock import os import testtools @@ -79,6 +80,8 @@ CLOUD_2 = { 'region_name': 'occ-cloud,krikkit,occ-env', 'log_file': '/tmp/test_log_file', 'log_level': 'debug', + 'cert': 'mycert', + 'key': 'mickey', } } } @@ -109,7 +112,7 @@ global_options = { '--os-default-domain': (DEFAULT_DOMAIN_NAME, True, True), '--os-cacert': ('/dev/null', True, True), '--timing': (True, True, False), - '--profile': ('SECRET_KEY', True, False), + '--os-profile': ('SECRET_KEY', True, False), '--os-interface': (DEFAULT_INTERFACE, True, True) } @@ -161,6 +164,23 @@ def fake_execute(shell, cmd): return shell.run(cmd.split()) +class EnvFixture(fixtures.Fixture): + """Environment Fixture. + + This fixture replaces os.environ with provided env or an empty env. + """ + + def __init__(self, env=None): + self.new_env = env or {} + + def _setUp(self): + self.orig_env, os.environ = os.environ, self.new_env + self.addCleanup(self.revert) + + def revert(self): + os.environ = self.orig_env + + class TestShell(utils.TestCase): def setUp(self): @@ -168,12 +188,9 @@ class TestShell(utils.TestCase): patch = "openstackclient.shell.OpenStackShell.run_subcommand" self.cmd_patch = mock.patch(patch) self.cmd_save = self.cmd_patch.start() + self.addCleanup(self.cmd_patch.stop) self.app = mock.Mock("Test Shell") - def tearDown(self): - super(TestShell, self).tearDown() - self.cmd_patch.stop() - def _assert_initialize_app_arg(self, cmd_options, default_args): """Check the args passed to initialize_app() @@ -285,11 +302,7 @@ class TestShellHelp(TestShell): def setUp(self): super(TestShellHelp, self).setUp() - self.orig_env, os.environ = os.environ, {} - - def tearDown(self): - super(TestShellHelp, self).tearDown() - os.environ = self.orig_env + self.useFixture(EnvFixture()) @testtools.skip("skip until bug 1444983 is resolved") def test_help_options(self): @@ -310,11 +323,7 @@ class TestShellOptions(TestShell): def setUp(self): super(TestShellOptions, self).setUp() - self.orig_env, os.environ = os.environ, {} - - def tearDown(self): - super(TestShellOptions, self).tearDown() - os.environ = self.orig_env + self.useFixture(EnvFixture()) def _test_options_init_app(self, test_opts): for opt in test_opts.keys(): @@ -402,11 +411,7 @@ class TestShellTokenAuthEnv(TestShell): "OS_TOKEN": DEFAULT_TOKEN, "OS_AUTH_URL": DEFAULT_AUTH_URL, } - self.orig_env, os.environ = os.environ, env.copy() - - def tearDown(self): - super(TestShellTokenAuthEnv, self).tearDown() - os.environ = self.orig_env + self.useFixture(EnvFixture(env.copy())) def test_env(self): flag = "" @@ -450,11 +455,7 @@ class TestShellTokenEndpointAuthEnv(TestShell): "OS_TOKEN": DEFAULT_TOKEN, "OS_URL": DEFAULT_SERVICE_URL, } - self.orig_env, os.environ = os.environ, env.copy() - - def tearDown(self): - super(TestShellTokenEndpointAuthEnv, self).tearDown() - os.environ = self.orig_env + self.useFixture(EnvFixture(env.copy())) def test_env(self): flag = "" @@ -501,11 +502,7 @@ class TestShellCli(TestShell): "OS_VOLUME_API_VERSION": DEFAULT_VOLUME_API_VERSION, "OS_NETWORK_API_VERSION": DEFAULT_NETWORK_API_VERSION, } - self.orig_env, os.environ = os.environ, env.copy() - - def tearDown(self): - super(TestShellCli, self).tearDown() - os.environ = self.orig_env + self.useFixture(EnvFixture(env.copy())) def test_shell_args_no_options(self): _shell = make_shell() @@ -567,6 +564,24 @@ class TestShellCli(TestShell): self.assertEqual('foo', _shell.options.cacert) self.assertFalse(_shell.verify) + def test_shell_args_cert_options(self): + _shell = make_shell() + + # Default + fake_execute(_shell, "list user") + self.assertEqual('', _shell.options.cert) + self.assertEqual('', _shell.options.key) + + # --os-cert + fake_execute(_shell, "--os-cert mycert list user") + self.assertEqual('mycert', _shell.options.cert) + self.assertEqual('', _shell.options.key) + + # --os-key + fake_execute(_shell, "--os-key mickey list user") + self.assertEqual('', _shell.options.cert) + self.assertEqual('mickey', _shell.options.key) + def test_default_env(self): flag = "" kwargs = { @@ -670,6 +685,9 @@ class TestShellCli(TestShell): _shell.cloud.config['region_name'], ) + self.assertEqual('mycert', _shell.cloud.config['cert']) + self.assertEqual('mickey', _shell.cloud.config['key']) + @mock.patch("os_client_config.config.OpenStackConfig._load_vendor_file") @mock.patch("os_client_config.config.OpenStackConfig._load_config_file") def test_shell_args_precedence(self, config_mock, vendor_mock): @@ -719,11 +737,7 @@ class TestShellCliEnv(TestShell): env = { 'OS_REGION_NAME': 'occ-env', } - self.orig_env, os.environ = os.environ, env.copy() - - def tearDown(self): - super(TestShellCliEnv, self).tearDown() - os.environ = self.orig_env + self.useFixture(EnvFixture(env.copy())) @mock.patch("os_client_config.config.OpenStackConfig._load_vendor_file") @mock.patch("os_client_config.config.OpenStackConfig._load_config_file") diff --git a/openstackclient/tests/utils.py b/openstackclient/tests/utils.py index d3f3853..319c1c1 100644 --- a/openstackclient/tests/utils.py +++ b/openstackclient/tests/utils.py @@ -17,7 +17,6 @@ import os import fixtures -import sys import testtools from openstackclient.tests import fakes @@ -50,29 +49,6 @@ class TestCase(testtools.TestCase): msg = 'method %s should not have been called' % m self.fail(msg) - # 2.6 doesn't have the assert dict equals so make sure that it exists - if tuple(sys.version_info)[0:2] < (2, 7): - - def assertIsInstance(self, obj, cls, msg=None): - """self.assertTrue(isinstance(obj, cls)), with a nicer message""" - - if not isinstance(obj, cls): - standardMsg = '%s is not an instance of %r' % (obj, cls) - self.fail(self._formatMessage(msg, standardMsg)) - - def assertDictEqual(self, d1, d2, msg=None): - # Simple version taken from 2.7 - self.assertIsInstance(d1, dict, - 'First argument is not a dictionary') - self.assertIsInstance(d2, dict, - 'Second argument is not a dictionary') - if d1 != d2: - if msg: - self.fail(msg) - else: - standardMsg = '%r != %r' % (d1, d2) - self.fail(standardMsg) - class TestCommand(TestCase): """Test cliff command classes""" diff --git a/openstackclient/tests/volume/v1/fakes.py b/openstackclient/tests/volume/v1/fakes.py index 42673ef..d6c4643 100644 --- a/openstackclient/tests/volume/v1/fakes.py +++ b/openstackclient/tests/volume/v1/fakes.py @@ -129,6 +129,97 @@ QOS_WITH_ASSOCIATIONS = { } +class FakeServiceClient(object): + + def __init__(self, **kwargs): + self.services = mock.Mock() + self.services.resource_class = fakes.FakeResource(None, {}) + + +class TestService(utils.TestCommand): + + def setUp(self): + super(TestService, self).setUp() + + self.app.client_manager.volume = FakeServiceClient( + endpoint=fakes.AUTH_URL, + token=fakes.AUTH_TOKEN + ) + + +class FakeService(object): + """Fake one or more Services.""" + + @staticmethod + def create_one_service(attrs=None): + """Create a fake service. + + :param Dictionary attrs: + A dictionary with all attributes of service + :retrun: + A FakeResource object with host, status, etc. + """ + # Set default attribute + service_info = { + 'host': 'host_test', + 'binary': 'cinder_test', + 'status': 'enabled', + 'disabled_reason': 'LongHoliday-GoldenWeek', + 'zone': 'fake_zone', + 'updated_at': 'fake_date', + 'state': 'fake_state', + } + + # Overwrite default attributes if there are some attributes set + if attrs is None: + attrs = {} + service_info.update(attrs) + + service = fakes.FakeResource( + None, + service_info, + loaded=True) + + return service + + @staticmethod + def create_services(attrs=None, count=2): + """Create multiple fake services. + + :param Dictionary attrs: + A dictionary with all attributes of service + :param Integer count: + The number of services to be faked + :return: + A list of FakeResource objects + """ + services = [] + for n in range(0, count): + services.append(FakeService.create_one_service(attrs)) + + return services + + @staticmethod + def get_services(services=None, count=2): + """Get an iterable MagicMock object with a list of faked services. + + If services list is provided, then initialize the Mock object with the + list. Otherwise create one. + + :param List services: + A list of FakeResource objects faking services + :param Integer count: + The number of services to be faked + :return + An iterable Mock object with side_effect set to a list of faked + services + """ + if services is None: + services = FakeService.create_services(count) + + return mock.MagicMock(side_effect=services) + + class FakeImagev1Client(object): def __init__(self, **kwargs): diff --git a/openstackclient/tests/volume/v1/test_qos_specs.py b/openstackclient/tests/volume/v1/test_qos_specs.py index 1a6c0fa..4943f5d 100644 --- a/openstackclient/tests/volume/v1/test_qos_specs.py +++ b/openstackclient/tests/volume/v1/test_qos_specs.py @@ -62,11 +62,13 @@ class TestQosAssociate(TestQos): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.cmd.take_action(parsed_args) + result = self.cmd.take_action(parsed_args) + self.qos_mock.associate.assert_called_with( volume_fakes.qos_id, volume_fakes.type_id ) + self.assertIsNone(result) class TestQosCreate(TestQos): @@ -204,11 +206,12 @@ class TestQosDelete(TestQos): verifylist = [ ('qos_specs', [volume_fakes.qos_id]) ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.cmd.take_action(parsed_args) + result = self.cmd.take_action(parsed_args) + self.qos_mock.delete.assert_called_with(volume_fakes.qos_id) + self.assertIsNone(result) def test_qos_delete_with_name(self): arglist = [ @@ -217,11 +220,12 @@ class TestQosDelete(TestQos): verifylist = [ ('qos_specs', [volume_fakes.qos_name]) ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.cmd.take_action(parsed_args) + result = self.cmd.take_action(parsed_args) + self.qos_mock.delete.assert_called_with(volume_fakes.qos_id) + self.assertIsNone(result) class TestQosDisassociate(TestQos): @@ -253,11 +257,13 @@ class TestQosDisassociate(TestQos): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.cmd.take_action(parsed_args) + result = self.cmd.take_action(parsed_args) + self.qos_mock.disassociate.assert_called_with( volume_fakes.qos_id, volume_fakes.type_id ) + self.assertIsNone(result) def test_qos_disassociate_with_all_volume_types(self): self.qos_mock.get.return_value = fakes.FakeResource( @@ -275,8 +281,10 @@ class TestQosDisassociate(TestQos): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.cmd.take_action(parsed_args) + result = self.cmd.take_action(parsed_args) + self.qos_mock.disassociate_all.assert_called_with(volume_fakes.qos_id) + self.assertIsNone(result) class TestQosList(TestQos): @@ -351,11 +359,13 @@ class TestQosSet(TestQos): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.cmd.take_action(parsed_args) + result = self.cmd.take_action(parsed_args) + self.qos_mock.set_keys.assert_called_with( volume_fakes.qos_id, volume_fakes.qos_specs ) + self.assertIsNone(result) class TestQosShow(TestQos): @@ -436,8 +446,10 @@ class TestQosUnset(TestQos): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.cmd.take_action(parsed_args) + result = self.cmd.take_action(parsed_args) + self.qos_mock.unset_keys.assert_called_with( volume_fakes.qos_id, ['iops', 'foo'] ) + self.assertIsNone(result) diff --git a/openstackclient/tests/volume/v1/test_service.py b/openstackclient/tests/volume/v1/test_service.py new file mode 100644 index 0000000..7168434 --- /dev/null +++ b/openstackclient/tests/volume/v1/test_service.py @@ -0,0 +1,141 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# + + +from openstackclient.tests.volume.v1 import fakes as service_fakes +from openstackclient.volume.v1 import service + + +class TestService(service_fakes.TestService): + + def setUp(self): + super(TestService, self).setUp() + + # Get a shortcut to the ServiceManager Mock + self.service_mock = self.app.client_manager.volume.services + self.service_mock.reset_mock() + + +class TestServiceList(TestService): + + # The service to be listed + services = service_fakes.FakeService.create_one_service() + + def setUp(self): + super(TestServiceList, self).setUp() + + self.service_mock.list.return_value = [self.services] + + # Get the command object to test + self.cmd = service.ListService(self.app, None) + + def test_service_list(self): + arglist = [ + '--host', self.services.host, + '--service', self.services.binary, + ] + verifylist = [ + ('host', self.services.host), + ('service', self.services.binary), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # In base command class Lister in cliff, abstract method take_action() + # returns a tuple containing the column names and an iterable + # containing the data to be listed. + columns, data = self.cmd.take_action(parsed_args) + + expected_columns = [ + 'Binary', + 'Host', + 'Zone', + 'Status', + 'State', + 'Updated At', + ] + + # confirming if all expected columns are present in the result. + self.assertEqual(expected_columns, columns) + + datalist = (( + self.services.binary, + self.services.host, + self.services.zone, + self.services.status, + self.services.state, + self.services.updated_at, + ), ) + + # confirming if all expected values are present in the result. + self.assertEqual(datalist, tuple(data)) + + # checking if proper call was made to list services + self.service_mock.list.assert_called_with( + self.services.host, + self.services.binary, + ) + + # checking if prohibited columns are present in output + self.assertNotIn("Disabled Reason", columns) + self.assertNotIn(self.services.disabled_reason, + tuple(data)) + + def test_service_list_with_long_option(self): + arglist = [ + '--host', self.services.host, + '--service', self.services.binary, + '--long' + ] + verifylist = [ + ('host', self.services.host), + ('service', self.services.binary), + ('long', True) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # In base command class Lister in cliff, abstract method take_action() + # returns a tuple containing the column names and an iterable + # containing the data to be listed. + columns, data = self.cmd.take_action(parsed_args) + + expected_columns = [ + 'Binary', + 'Host', + 'Zone', + 'Status', + 'State', + 'Updated At', + 'Disabled Reason' + ] + + # confirming if all expected columns are present in the result. + self.assertEqual(expected_columns, columns) + + datalist = (( + self.services.binary, + self.services.host, + self.services.zone, + self.services.status, + self.services.state, + self.services.updated_at, + self.services.disabled_reason, + ), ) + + # confirming if all expected values are present in the result. + self.assertEqual(datalist, tuple(data)) + + self.service_mock.list.assert_called_with( + self.services.host, + self.services.binary, + ) diff --git a/openstackclient/tests/volume/v1/test_volume.py b/openstackclient/tests/volume/v1/test_volume.py index 00c509b..e0fd1c0 100644 --- a/openstackclient/tests/volume/v1/test_volume.py +++ b/openstackclient/tests/volume/v1/test_volume.py @@ -577,10 +577,11 @@ class TestVolumeSet(TestVolume): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - result = self.cmd.run(parsed_args) - self.assertEqual(0, result) + result = self.cmd.take_action(parsed_args) + self.assertEqual("No changes requested\n", self.app.log.messages.get('error')) + self.assertIsNone(result) def test_volume_set_name(self): arglist = [ @@ -596,7 +597,7 @@ class TestVolumeSet(TestVolume): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.cmd.take_action(parsed_args) + result = self.cmd.take_action(parsed_args) # Set expected values kwargs = { @@ -606,6 +607,7 @@ class TestVolumeSet(TestVolume): volume_fakes.volume_id, **kwargs ) + self.assertIsNone(result) def test_volume_set_description(self): arglist = [ @@ -621,7 +623,7 @@ class TestVolumeSet(TestVolume): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.cmd.take_action(parsed_args) + result = self.cmd.take_action(parsed_args) # Set expected values kwargs = { @@ -631,6 +633,7 @@ class TestVolumeSet(TestVolume): volume_fakes.volume_id, **kwargs ) + self.assertIsNone(result) def test_volume_set_size(self): arglist = [ @@ -646,15 +649,15 @@ class TestVolumeSet(TestVolume): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.cmd.take_action(parsed_args) + result = self.cmd.take_action(parsed_args) # Set expected values size = 130 - self.volumes_mock.extend.assert_called_with( volume_fakes.volume_id, size ) + self.assertIsNone(result) def test_volume_set_size_smaller(self): arglist = [ @@ -670,11 +673,12 @@ class TestVolumeSet(TestVolume): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - result = self.cmd.run(parsed_args) - self.assertEqual(0, result) + result = self.cmd.take_action(parsed_args) + self.assertEqual("New size must be greater than %s GB" % volume_fakes.volume_size, self.app.log.messages.get('error')) + self.assertIsNone(result) def test_volume_set_size_not_available(self): self.volumes_mock.get.return_value.status = 'error' @@ -691,11 +695,12 @@ class TestVolumeSet(TestVolume): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - result = self.cmd.run(parsed_args) - self.assertEqual(0, result) + result = self.cmd.take_action(parsed_args) + self.assertEqual("Volume is in %s state, it must be available before " "size can be extended" % 'error', self.app.log.messages.get('error')) + self.assertIsNone(result) def test_volume_set_property(self): arglist = [ @@ -711,7 +716,7 @@ class TestVolumeSet(TestVolume): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.cmd.take_action(parsed_args) + result = self.cmd.take_action(parsed_args) # Set expected values metadata = { @@ -721,3 +726,4 @@ class TestVolumeSet(TestVolume): volume_fakes.volume_id, metadata ) + self.assertIsNone(result) diff --git a/openstackclient/tests/volume/v2/fakes.py b/openstackclient/tests/volume/v2/fakes.py index 61d9df3..181552d 100644 --- a/openstackclient/tests/volume/v2/fakes.py +++ b/openstackclient/tests/volume/v2/fakes.py @@ -211,6 +211,117 @@ IMAGE = { 'name': image_name } +extension_name = 'SchedulerHints' +extension_namespace = 'http://docs.openstack.org/'\ + 'block-service/ext/scheduler-hints/api/v2' +extension_description = 'Pass arbitrary key/value'\ + 'pairs to the scheduler.' +extension_updated = '2013-04-18T00:00:00+00:00' +extension_alias = 'OS-SCH-HNT' +extension_links = '[{"href":'\ + '"https://github.com/openstack/block-api", "type":'\ + ' "text/html", "rel": "describedby"}]' + +EXTENSION = { + 'name': extension_name, + 'namespace': extension_namespace, + 'description': extension_description, + 'updated': extension_updated, + 'alias': extension_alias, + 'links': extension_links, +} + + +class FakeServiceClient(object): + + def __init__(self, **kwargs): + self.services = mock.Mock() + self.services.resource_class = fakes.FakeResource(None, {}) + + +class TestService(utils.TestCommand): + + def setUp(self): + super(TestService, self).setUp() + + self.app.client_manager.volume = FakeServiceClient( + endpoint=fakes.AUTH_URL, + token=fakes.AUTH_TOKEN + ) + + +class FakeService(object): + """Fake one or more Services.""" + + @staticmethod + def create_one_service(attrs=None): + """Create a fake service. + + :param Dictionary attrs: + A dictionary with all attributes of service + :retrun: + A FakeResource object with host, status, etc. + """ + # Set default attribute + service_info = { + 'host': 'host_test', + 'binary': 'cinder_test', + 'status': 'enabled', + 'disabled_reason': 'LongHoliday-GoldenWeek', + 'zone': 'fake_zone', + 'updated_at': 'fake_date', + 'state': 'fake_state', + } + + # Overwrite default attributes if there are some attributes set + if attrs is None: + attrs = {} + service_info.update(attrs) + + service = fakes.FakeResource( + None, + service_info, + loaded=True) + + return service + + @staticmethod + def create_services(attrs=None, count=2): + """Create multiple fake services. + + :param Dictionary attrs: + A dictionary with all attributes of service + :param Integer count: + The number of services to be faked + :return: + A list of FakeResource objects + """ + services = [] + for n in range(0, count): + services.append(FakeService.create_one_service(attrs)) + + return services + + @staticmethod + def get_services(services=None, count=2): + """Get an iterable MagicMock object with a list of faked services. + + If services list is provided, then initialize the Mock object with the + list. Otherwise create one. + + :param List services: + A list of FakeResource objects faking services + :param Integer count: + The number of services to be faked + :return + An iterable Mock object with side_effect set to a list of faked + services + """ + if services is None: + services = FakeService.create_services(count) + + return mock.MagicMock(side_effect=services) + class FakeVolumeClient(object): @@ -223,6 +334,8 @@ class FakeVolumeClient(object): self.backups.resource_class = fakes.FakeResource(None, {}) self.volume_types = mock.Mock() self.volume_types.resource_class = fakes.FakeResource(None, {}) + self.volume_type_access = mock.Mock() + self.volume_type_access.resource_class = fakes.FakeResource(None, {}) self.restores = mock.Mock() self.restores.resource_class = fakes.FakeResource(None, {}) self.qos_specs = mock.Mock() @@ -259,7 +372,7 @@ class FakeVolume(object): """ @staticmethod - def create_one_volume(attrs={}): + def create_one_volume(attrs=None): """Create a fake volume. :param Dictionary attrs: @@ -267,6 +380,8 @@ class FakeVolume(object): :retrun: A FakeResource object with id, name, status, etc. """ + attrs = attrs or {} + # Set default attribute volume_info = { 'id': 'volume-id' + uuid.uuid4().hex, @@ -276,6 +391,8 @@ class FakeVolume(object): 'size': random.randint(1, 20), 'volume_type': random.choice(['fake_lvmdriver-1', 'fake_lvmdriver-2']), + 'bootable': + random.randint(0, 1), 'metadata': { 'key' + uuid.uuid4().hex: 'val' + uuid.uuid4().hex, 'key' + uuid.uuid4().hex: 'val' + uuid.uuid4().hex, @@ -298,7 +415,7 @@ class FakeVolume(object): return volume @staticmethod - def create_volumes(attrs={}, count=2): + def create_volumes(attrs=None, count=2): """Create multiple fake volumes. :param Dictionary attrs: @@ -334,21 +451,59 @@ class FakeVolume(object): return mock.MagicMock(side_effect=volumes) + @staticmethod + def get_volume_columns(volume=None): + """Get the volume columns from a faked volume object. + + :param volume: + A FakeResource objects faking volume + :return + A tuple which may include the following keys: + ('id', 'name', 'description', 'status', 'size', 'volume_type', + 'metadata', 'snapshot', 'availability_zone', 'attachments') + """ + if volume is not None: + return tuple(k for k in sorted(volume.keys())) + return tuple([]) + + @staticmethod + def get_volume_data(volume=None): + """Get the volume data from a faked volume object. + + :param volume: + A FakeResource objects faking volume + :return + A tuple which may include the following values: + ('ce26708d', 'fake_volume', 'fake description', 'available', + 20, 'fake_lvmdriver-1', "Alpha='a', Beta='b', Gamma='g'", + 1, 'nova', [{'device': '/dev/ice', 'server_id': '1233'}]) + """ + data_list = [] + if volume is not None: + for x in sorted(volume.keys()): + if x == 'tags': + # The 'tags' should be format_list + data_list.append( + common_utils.format_list(volume.info.get(x))) + else: + data_list.append(volume.info.get(x)) + return tuple(data_list) + class FakeAvailabilityZone(object): """Fake one or more volume availability zones (AZs).""" @staticmethod - def create_one_availability_zone(attrs={}, methods={}): + def create_one_availability_zone(attrs=None): """Create a fake AZ. :param Dictionary attrs: A dictionary with all attributes - :param Dictionary methods: - A dictionary with all methods :return: A FakeResource object with zoneName, zoneState, etc. """ + attrs = attrs or {} + # Set default attributes. availability_zone = { 'zoneName': uuid.uuid4().hex, @@ -360,18 +515,15 @@ class FakeAvailabilityZone(object): availability_zone = fakes.FakeResource( info=copy.deepcopy(availability_zone), - methods=methods, loaded=True) return availability_zone @staticmethod - def create_availability_zones(attrs={}, methods={}, count=2): + def create_availability_zones(attrs=None, count=2): """Create multiple fake AZs. :param Dictionary attrs: A dictionary with all attributes - :param Dictionary methods: - A dictionary with all methods :param int count: The number of AZs to fake :return: @@ -380,8 +532,167 @@ class FakeAvailabilityZone(object): availability_zones = [] for i in range(0, count): availability_zone = \ - FakeAvailabilityZone.create_one_availability_zone( - attrs, methods) + FakeAvailabilityZone.create_one_availability_zone(attrs) availability_zones.append(availability_zone) return availability_zones + + +class FakeBackup(object): + """Fake one or more backup.""" + + @staticmethod + def create_one_backup(attrs=None): + """Create a fake backup. + + :param Dictionary attrs: + A dictionary with all attributes + :return: + A FakeResource object with id, name, volume_id, etc. + """ + attrs = attrs or {} + + # Set default attributes. + backup_info = { + "id": 'backup-id-' + uuid.uuid4().hex, + "name": 'backup-name-' + uuid.uuid4().hex, + "volume_id": 'volume-id-' + uuid.uuid4().hex, + "description": 'description-' + uuid.uuid4().hex, + "object_count": None, + "container": 'container-' + uuid.uuid4().hex, + "size": random.randint(1, 20), + "status": "error", + "availability_zone": 'zone' + uuid.uuid4().hex, + } + + # Overwrite default attributes. + backup_info.update(attrs) + + backup = fakes.FakeResource( + info=copy.deepcopy(backup_info), + loaded=True) + return backup + + @staticmethod + def create_backups(attrs=None, count=2): + """Create multiple fake backups. + + :param Dictionary attrs: + A dictionary with all attributes + :param int count: + The number of backups to fake + :return: + A list of FakeResource objects faking the backups + """ + backups = [] + for i in range(0, count): + backup = FakeBackup.create_one_backup(attrs) + backups.append(backup) + + return backups + + +class FakeSnapshot(object): + """Fake one or more snapshot.""" + + @staticmethod + def create_one_snapshot(attrs=None): + """Create a fake snapshot. + + :param Dictionary attrs: + A dictionary with all attributes + :return: + A FakeResource object with id, name, description, etc. + """ + attrs = attrs or {} + + # Set default attributes. + snapshot_info = { + "id": 'snapshot-id-' + uuid.uuid4().hex, + "name": 'snapshot-name-' + uuid.uuid4().hex, + "description": 'snapshot-description-' + uuid.uuid4().hex, + "size": 10, + "status": "available", + "metadata": {"foo": "bar"}, + "created_at": "2015-06-03T18:49:19.000000", + "volume_id": 'vloume-id-' + uuid.uuid4().hex, + } + + # Overwrite default attributes. + snapshot_info.update(attrs) + + snapshot = fakes.FakeResource( + info=copy.deepcopy(snapshot_info), + loaded=True) + return snapshot + + @staticmethod + def create_snapshots(attrs=None, count=2): + """Create multiple fake snapshots. + + :param Dictionary attrs: + A dictionary with all attributes + :param int count: + The number of snapshots to fake + :return: + A list of FakeResource objects faking the snapshots + """ + snapshots = [] + for i in range(0, count): + snapshot = FakeSnapshot.create_one_snapshot(attrs) + snapshots.append(snapshot) + + return snapshots + + +class FakeType(object): + """Fake one or more type.""" + + @staticmethod + def create_one_type(attrs=None, methods=None): + """Create a fake type. + + :param Dictionary attrs: + A dictionary with all attributes + :param Dictionary methods: + A dictionary with all methods + :return: + A FakeResource object with id, name, description, etc. + """ + attrs = attrs or {} + methods = methods or {} + + # Set default attributes. + type_info = { + "id": 'type-id-' + uuid.uuid4().hex, + "name": 'type-name-' + uuid.uuid4().hex, + "description": 'type-description-' + uuid.uuid4().hex, + "extra_specs": {"foo": "bar"}, + } + + # Overwrite default attributes. + type_info.update(attrs) + + volume_type = fakes.FakeResource( + info=copy.deepcopy(type_info), + methods=methods, + loaded=True) + return volume_type + + @staticmethod + def create_types(attrs=None, count=2): + """Create multiple fake types. + + :param Dictionary attrs: + A dictionary with all attributes + :param int count: + The number of types to fake + :return: + A list of FakeResource objects faking the types + """ + volume_types = [] + for i in range(0, count): + volume_type = FakeType.create_one_type(attrs) + volume_types.append(volume_type) + + return volume_types diff --git a/openstackclient/tests/volume/v2/test_backup.py b/openstackclient/tests/volume/v2/test_backup.py index 6fe3f66..8a151a9 100644 --- a/openstackclient/tests/volume/v2/test_backup.py +++ b/openstackclient/tests/volume/v2/test_backup.py @@ -12,9 +12,6 @@ # under the License. # -import copy - -from openstackclient.tests import fakes from openstackclient.tests.volume.v2 import fakes as volume_fakes from openstackclient.volume.v2 import backup @@ -34,59 +31,101 @@ class TestBackup(volume_fakes.TestVolume): class TestBackupCreate(TestBackup): + volume = volume_fakes.FakeVolume.create_one_volume() + new_backup = volume_fakes.FakeBackup.create_one_backup( + attrs={'volume_id': volume.id}) + + columns = ( + 'availability_zone', + 'container', + 'description', + 'id', + 'name', + 'object_count', + 'size', + 'status', + 'volume_id', + ) + data = ( + new_backup.availability_zone, + new_backup.container, + new_backup.description, + new_backup.id, + new_backup.name, + new_backup.object_count, + new_backup.size, + new_backup.status, + new_backup.volume_id, + ) + def setUp(self): super(TestBackupCreate, self).setUp() - self.volumes_mock.get.return_value = fakes.FakeResource( - None, - copy.deepcopy(volume_fakes.VOLUME), - loaded=True - ) + self.volumes_mock.get.return_value = self.volume + self.backups_mock.create.return_value = self.new_backup - self.backups_mock.create.return_value = fakes.FakeResource( - None, - copy.deepcopy(volume_fakes.BACKUP), - loaded=True - ) # Get the command object to test self.cmd = backup.CreateBackup(self.app, None) def test_backup_create(self): arglist = [ - volume_fakes.volume_id, - "--name", volume_fakes.backup_name, - "--description", volume_fakes.backup_description, - "--container", volume_fakes.backup_name + "--name", self.new_backup.name, + "--description", self.new_backup.description, + "--container", self.new_backup.container, + self.new_backup.volume_id, ] verifylist = [ - ("volume", volume_fakes.volume_id), - ("name", volume_fakes.backup_name), - ("description", volume_fakes.backup_description), - ("container", volume_fakes.backup_name) + ("name", self.new_backup.name), + ("description", self.new_backup.description), + ("container", self.new_backup.container), + ("volume", self.new_backup.volume_id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) self.backups_mock.create.assert_called_with( - volume_fakes.volume_id, - container=volume_fakes.backup_name, - name=volume_fakes.backup_name, - description=volume_fakes.backup_description + self.new_backup.volume_id, + container=self.new_backup.container, + name=self.new_backup.name, + description=self.new_backup.description ) - self.assertEqual(columns, volume_fakes.BACKUP_columns) - self.assertEqual(data, volume_fakes.BACKUP_data) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + def test_backup_create_without_name(self): + arglist = [ + "--description", self.new_backup.description, + "--container", self.new_backup.container, + self.new_backup.volume_id, + ] + verifylist = [ + ("description", self.new_backup.description), + ("container", self.new_backup.container), + ("volume", self.new_backup.volume_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.backups_mock.create.assert_called_with( + self.new_backup.volume_id, + container=self.new_backup.container, + name=None, + description=self.new_backup.description + ) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) class TestBackupDelete(TestBackup): + backup = volume_fakes.FakeBackup.create_one_backup() + def setUp(self): super(TestBackupDelete, self).setUp() - self.backups_mock.get.return_value = fakes.FakeResource( - None, - copy.deepcopy(volume_fakes.BACKUP), - loaded=True) + self.backups_mock.get.return_value = self.backup self.backups_mock.delete.return_value = None # Get the command object to mock @@ -94,20 +133,25 @@ class TestBackupDelete(TestBackup): def test_backup_delete(self): arglist = [ - volume_fakes.backup_id + self.backup.id ] verifylist = [ - ("backups", [volume_fakes.backup_id]) + ("backups", [self.backup.id]) ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.cmd.take_action(parsed_args) - self.backups_mock.delete.assert_called_with(volume_fakes.backup_id) + result = self.cmd.take_action(parsed_args) + + self.backups_mock.delete.assert_called_with(self.backup.id) + self.assertIsNone(result) class TestBackupList(TestBackup): + volume = volume_fakes.FakeVolume.create_one_volume() + backups = volume_fakes.FakeBackup.create_backups( + attrs={'volume_id': volume.name}, count=3) + columns = [ 'ID', 'Name', @@ -115,33 +159,39 @@ class TestBackupList(TestBackup): 'Status', 'Size', ] - datalist = ( - ( - volume_fakes.backup_id, - volume_fakes.backup_name, - volume_fakes.backup_description, - volume_fakes.backup_status, - volume_fakes.backup_size - ), - ) + columns_long = columns + [ + 'Availability Zone', + 'Volume', + 'Container', + ] + + data = [] + for b in backups: + data.append(( + b.id, + b.name, + b.description, + b.status, + b.size, + )) + data_long = [] + for b in backups: + data_long.append(( + b.id, + b.name, + b.description, + b.status, + b.size, + b.availability_zone, + b.volume_id, + b.container, + )) def setUp(self): super(TestBackupList, self).setUp() - self.volumes_mock.list.return_value = [ - fakes.FakeResource( - None, - copy.deepcopy(volume_fakes.VOLUME), - loaded=True - ) - ] - self.backups_mock.list.return_value = [ - fakes.FakeResource( - None, - copy.deepcopy(volume_fakes.BACKUP), - loaded=True - ) - ] + self.volumes_mock.list.return_value = [self.volume] + self.backups_mock.list.return_value = self.backups # Get the command to test self.cmd = backup.ListBackup(self.app, None) @@ -153,7 +203,7 @@ class TestBackupList(TestBackup): columns, data = self.cmd.take_action(parsed_args) self.assertEqual(self.columns, columns) - self.assertEqual(self.datalist, tuple(data)) + self.assertEqual(self.data, list(data)) def test_backup_list_with_options(self): arglist = ["--long"] @@ -162,85 +212,87 @@ class TestBackupList(TestBackup): parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - columns = self.columns + [ - 'Availability Zone', - 'Volume', - 'Container', - ] - - self.assertEqual(columns, columns) - - datalist = (( - volume_fakes.backup_id, - volume_fakes.backup_name, - volume_fakes.backup_description, - volume_fakes.backup_status, - volume_fakes.backup_size, - volume_fakes.volume_availability_zone, - volume_fakes.backup_volume_id, - volume_fakes.backup_container - ),) - self.assertEqual(datalist, tuple(data)) + self.assertEqual(self.columns_long, columns) + self.assertEqual(self.data_long, list(data)) class TestBackupRestore(TestBackup): + volume = volume_fakes.FakeVolume.create_one_volume() + backup = volume_fakes.FakeBackup.create_one_backup( + attrs={'volume_id': volume.id}) + def setUp(self): super(TestBackupRestore, self).setUp() - self.backups_mock.get.return_value = fakes.FakeResource( - None, - copy.deepcopy(volume_fakes.BACKUP), - loaded=True - ) - self.volumes_mock.get.return_value = fakes.FakeResource( - None, - copy.deepcopy(volume_fakes.VOLUME), - loaded=True - ) + self.backups_mock.get.return_value = self.backup + self.volumes_mock.get.return_value = self.volume self.restores_mock.restore.return_value = None # Get the command object to mock self.cmd = backup.RestoreBackup(self.app, None) def test_backup_restore(self): arglist = [ - volume_fakes.backup_id, - volume_fakes.volume_id + self.backup.id, + self.backup.volume_id ] verifylist = [ - ("backup", volume_fakes.backup_id), - ("volume", volume_fakes.volume_id) + ("backup", self.backup.id), + ("volume", self.backup.volume_id) ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.cmd.take_action(parsed_args) - self.restores_mock.restore.assert_called_with(volume_fakes.backup_id, - volume_fakes.volume_id) + result = self.cmd.take_action(parsed_args) + self.restores_mock.restore.assert_called_with(self.backup.id, + self.backup.volume_id) + self.assertIsNone(result) class TestBackupShow(TestBackup): + backup = volume_fakes.FakeBackup.create_one_backup() + + columns = ( + 'availability_zone', + 'container', + 'description', + 'id', + 'name', + 'object_count', + 'size', + 'status', + 'volume_id', + ) + data = ( + backup.availability_zone, + backup.container, + backup.description, + backup.id, + backup.name, + backup.object_count, + backup.size, + backup.status, + backup.volume_id, + ) + def setUp(self): super(TestBackupShow, self).setUp() - self.backups_mock.get.return_value = fakes.FakeResource( - None, - copy.deepcopy(volume_fakes.BACKUP), - loaded=True) + self.backups_mock.get.return_value = self.backup # Get the command object to test self.cmd = backup.ShowBackup(self.app, None) def test_backup_show(self): arglist = [ - volume_fakes.backup_id + self.backup.id ] verifylist = [ - ("backup", volume_fakes.backup_id) + ("backup", self.backup.id) ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.backups_mock.get.assert_called_with(volume_fakes.backup_id) + self.backups_mock.get.assert_called_with(self.backup.id) - self.assertEqual(volume_fakes.BACKUP_columns, columns) - self.assertEqual(volume_fakes.BACKUP_data, data) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) diff --git a/openstackclient/tests/volume/v2/test_qos_specs.py b/openstackclient/tests/volume/v2/test_qos_specs.py index c826925..5232285 100644 --- a/openstackclient/tests/volume/v2/test_qos_specs.py +++ b/openstackclient/tests/volume/v2/test_qos_specs.py @@ -62,11 +62,13 @@ class TestQosAssociate(TestQos): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.cmd.take_action(parsed_args) + result = self.cmd.take_action(parsed_args) + self.qos_mock.associate.assert_called_with( volume_fakes.qos_id, volume_fakes.type_id ) + self.assertIsNone(result) class TestQosCreate(TestQos): @@ -205,11 +207,12 @@ class TestQosDelete(TestQos): verifylist = [ ('qos_specs', [volume_fakes.qos_id]) ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.cmd.take_action(parsed_args) + result = self.cmd.take_action(parsed_args) + self.qos_mock.delete.assert_called_with(volume_fakes.qos_id) + self.assertIsNone(result) def test_qos_delete_with_name(self): arglist = [ @@ -218,11 +221,12 @@ class TestQosDelete(TestQos): verifylist = [ ('qos_specs', [volume_fakes.qos_name]) ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.cmd.take_action(parsed_args) + result = self.cmd.take_action(parsed_args) + self.qos_mock.delete.assert_called_with(volume_fakes.qos_id) + self.assertIsNone(result) class TestQosDisassociate(TestQos): @@ -254,11 +258,13 @@ class TestQosDisassociate(TestQos): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.cmd.take_action(parsed_args) + result = self.cmd.take_action(parsed_args) + self.qos_mock.disassociate.assert_called_with( volume_fakes.qos_id, volume_fakes.type_id ) + self.assertIsNone(result) def test_qos_disassociate_with_all_volume_types(self): self.qos_mock.get.return_value = fakes.FakeResource( @@ -276,8 +282,10 @@ class TestQosDisassociate(TestQos): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.cmd.take_action(parsed_args) + result = self.cmd.take_action(parsed_args) + self.qos_mock.disassociate_all.assert_called_with(volume_fakes.qos_id) + self.assertIsNone(result) class TestQosList(TestQos): @@ -352,11 +360,13 @@ class TestQosSet(TestQos): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.cmd.take_action(parsed_args) + result = self.cmd.take_action(parsed_args) + self.qos_mock.set_keys.assert_called_with( volume_fakes.qos_id, volume_fakes.qos_specs ) + self.assertIsNone(result) class TestQosShow(TestQos): @@ -430,15 +440,16 @@ class TestQosUnset(TestQos): '--property', 'iops', '--property', 'foo' ] - verifylist = [ ('qos_spec', volume_fakes.qos_id), ('property', ['iops', 'foo']) ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.cmd.take_action(parsed_args) + result = self.cmd.take_action(parsed_args) + self.qos_mock.unset_keys.assert_called_with( volume_fakes.qos_id, ['iops', 'foo'] ) + self.assertIsNone(result) diff --git a/openstackclient/tests/volume/v2/test_service.py b/openstackclient/tests/volume/v2/test_service.py new file mode 100644 index 0000000..ba2e1b3 --- /dev/null +++ b/openstackclient/tests/volume/v2/test_service.py @@ -0,0 +1,141 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# + + +from openstackclient.tests.volume.v2 import fakes as service_fakes +from openstackclient.volume.v2 import service + + +class TestService(service_fakes.TestService): + + def setUp(self): + super(TestService, self).setUp() + + # Get a shortcut to the ServiceManager Mock + self.service_mock = self.app.client_manager.volume.services + self.service_mock.reset_mock() + + +class TestServiceList(TestService): + + # The service to be listed + services = service_fakes.FakeService.create_one_service() + + def setUp(self): + super(TestServiceList, self).setUp() + + self.service_mock.list.return_value = [self.services] + + # Get the command object to test + self.cmd = service.ListService(self.app, None) + + def test_service_list(self): + arglist = [ + '--host', self.services.host, + '--service', self.services.binary, + ] + verifylist = [ + ('host', self.services.host), + ('service', self.services.binary), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # In base command class Lister in cliff, abstract method take_action() + # returns a tuple containing the column names and an iterable + # containing the data to be listed. + columns, data = self.cmd.take_action(parsed_args) + + expected_columns = [ + 'Binary', + 'Host', + 'Zone', + 'Status', + 'State', + 'Updated At', + ] + + # confirming if all expected columns are present in the result. + self.assertEqual(expected_columns, columns) + + datalist = (( + self.services.binary, + self.services.host, + self.services.zone, + self.services.status, + self.services.state, + self.services.updated_at, + ), ) + + # confirming if all expected values are present in the result. + self.assertEqual(datalist, tuple(data)) + + # checking if proper call was made to list services + self.service_mock.list.assert_called_with( + self.services.host, + self.services.binary, + ) + + # checking if prohibited columns are present in output + self.assertNotIn("Disabled Reason", columns) + self.assertNotIn(self.services.disabled_reason, + tuple(data)) + + def test_service_list_with_long_option(self): + arglist = [ + '--host', self.services.host, + '--service', self.services.binary, + '--long' + ] + verifylist = [ + ('host', self.services.host), + ('service', self.services.binary), + ('long', True) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # In base command class Lister in cliff, abstract method take_action() + # returns a tuple containing the column names and an iterable + # containing the data to be listed. + columns, data = self.cmd.take_action(parsed_args) + + expected_columns = [ + 'Binary', + 'Host', + 'Zone', + 'Status', + 'State', + 'Updated At', + 'Disabled Reason' + ] + + # confirming if all expected columns are present in the result. + self.assertEqual(expected_columns, columns) + + datalist = (( + self.services.binary, + self.services.host, + self.services.zone, + self.services.status, + self.services.state, + self.services.updated_at, + self.services.disabled_reason, + ), ) + + # confirming if all expected values are present in the result. + self.assertEqual(datalist, tuple(data)) + + self.service_mock.list.assert_called_with( + self.services.host, + self.services.binary, + ) diff --git a/openstackclient/tests/volume/v2/test_snapshot.py b/openstackclient/tests/volume/v2/test_snapshot.py index 349e8da..fe6fbb5 100644 --- a/openstackclient/tests/volume/v2/test_snapshot.py +++ b/openstackclient/tests/volume/v2/test_snapshot.py @@ -12,9 +12,7 @@ # under the License. # -import copy - -from openstackclient.tests import fakes +from openstackclient.common import utils from openstackclient.tests.volume.v2 import fakes as volume_fakes from openstackclient.volume.v2 import snapshot @@ -32,34 +30,75 @@ class TestSnapshot(volume_fakes.TestVolume): class TestSnapshotCreate(TestSnapshot): + columns = ( + 'created_at', + 'description', + 'id', + 'name', + 'properties', + 'size', + 'status', + 'volume_id', + ) + def setUp(self): super(TestSnapshotCreate, self).setUp() - self.volumes_mock.get.return_value = fakes.FakeResource( - None, - copy.deepcopy(volume_fakes.VOLUME), - loaded=True + self.volume = volume_fakes.FakeVolume.create_one_volume() + self.new_snapshot = volume_fakes.FakeSnapshot.create_one_snapshot( + attrs={'volume_id': self.volume.id}) + + self.data = ( + self.new_snapshot.created_at, + self.new_snapshot.description, + self.new_snapshot.id, + self.new_snapshot.name, + utils.format_dict(self.new_snapshot.metadata), + self.new_snapshot.size, + self.new_snapshot.status, + self.new_snapshot.volume_id, ) - self.snapshots_mock.create.return_value = fakes.FakeResource( - None, - copy.deepcopy(volume_fakes.SNAPSHOT), - loaded=True - ) + self.volumes_mock.get.return_value = self.volume + self.snapshots_mock.create.return_value = self.new_snapshot # Get the command object to test self.cmd = snapshot.CreateSnapshot(self.app, None) def test_snapshot_create(self): arglist = [ - volume_fakes.volume_id, - "--name", volume_fakes.snapshot_name, - "--description", volume_fakes.snapshot_description, + "--name", self.new_snapshot.name, + "--description", self.new_snapshot.description, + "--force", + self.new_snapshot.volume_id, + ] + verifylist = [ + ("name", self.new_snapshot.name), + ("description", self.new_snapshot.description), + ("force", True), + ("volume", self.new_snapshot.volume_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.snapshots_mock.create.assert_called_with( + self.new_snapshot.volume_id, + force=True, + name=self.new_snapshot.name, + description=self.new_snapshot.description + ) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + def test_snapshot_create_without_name(self): + arglist = [ + self.new_snapshot.volume_id, + "--description", self.new_snapshot.description, "--force" ] verifylist = [ - ("volume", volume_fakes.volume_id), - ("name", volume_fakes.snapshot_name), - ("description", volume_fakes.snapshot_description), + ("volume", self.new_snapshot.volume_id), + ("description", self.new_snapshot.description), ("force", True) ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -67,24 +106,23 @@ class TestSnapshotCreate(TestSnapshot): columns, data = self.cmd.take_action(parsed_args) self.snapshots_mock.create.assert_called_with( - volume_fakes.volume_id, + self.new_snapshot.volume_id, force=True, - name=volume_fakes.snapshot_name, - description=volume_fakes.snapshot_description + name=None, + description=self.new_snapshot.description ) - self.assertEqual(columns, volume_fakes.SNAPSHOT_columns) - self.assertEqual(data, volume_fakes.SNAPSHOT_data) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) class TestSnapshotDelete(TestSnapshot): + snapshot = volume_fakes.FakeSnapshot.create_one_snapshot() + def setUp(self): super(TestSnapshotDelete, self).setUp() - self.snapshots_mock.get.return_value = fakes.FakeResource( - None, - copy.deepcopy(volume_fakes.SNAPSHOT), - loaded=True) + self.snapshots_mock.get.return_value = self.snapshot self.snapshots_mock.delete.return_value = None # Get the command object to mock @@ -92,20 +130,25 @@ class TestSnapshotDelete(TestSnapshot): def test_snapshot_delete(self): arglist = [ - volume_fakes.snapshot_id + self.snapshot.id ] verifylist = [ - ("snapshots", [volume_fakes.snapshot_id]) + ("snapshots", [self.snapshot.id]) ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.cmd.take_action(parsed_args) - self.snapshots_mock.delete.assert_called_with(volume_fakes.snapshot_id) + result = self.cmd.take_action(parsed_args) + + self.snapshots_mock.delete.assert_called_with(self.snapshot.id) + self.assertIsNone(result) class TestSnapshotList(TestSnapshot): + volume = volume_fakes.FakeVolume.create_one_volume() + snapshots = volume_fakes.FakeSnapshot.create_snapshots( + attrs={'volume_id': volume.name}, count=3) + columns = [ "ID", "Name", @@ -113,24 +156,39 @@ class TestSnapshotList(TestSnapshot): "Status", "Size" ] + columns_long = columns + [ + "Created At", + "Volume", + "Properties" + ] + + data = [] + for s in snapshots: + data.append(( + s.id, + s.name, + s.description, + s.status, + s.size, + )) + data_long = [] + for s in snapshots: + data_long.append(( + s.id, + s.name, + s.description, + s.status, + s.size, + s.created_at, + s.volume_id, + utils.format_dict(s.metadata), + )) def setUp(self): super(TestSnapshotList, self).setUp() - self.volumes_mock.list.return_value = [ - fakes.FakeResource( - None, - copy.deepcopy(volume_fakes.VOLUME), - loaded=True - ) - ] - self.snapshots_mock.list.return_value = [ - fakes.FakeResource( - None, - copy.deepcopy(volume_fakes.SNAPSHOT), - loaded=True - ) - ] + self.volumes_mock.list.return_value = [self.volume] + self.snapshots_mock.list.return_value = self.snapshots # Get the command to test self.cmd = snapshot.ListSnapshot(self.app, None) @@ -144,14 +202,7 @@ class TestSnapshotList(TestSnapshot): columns, data = self.cmd.take_action(parsed_args) self.assertEqual(self.columns, columns) - datalist = (( - volume_fakes.snapshot_id, - volume_fakes.snapshot_name, - volume_fakes.snapshot_description, - "available", - volume_fakes.snapshot_size - ),) - self.assertEqual(datalist, tuple(data)) + self.assertEqual(self.data, list(data)) def test_snapshot_list_with_options(self): arglist = ["--long"] @@ -160,24 +211,8 @@ class TestSnapshotList(TestSnapshot): columns, data = self.cmd.take_action(parsed_args) - columns = self.columns + [ - "Created At", - "Volume", - "Properties" - ] - self.assertEqual(columns, columns) - - datalist = (( - volume_fakes.snapshot_id, - volume_fakes.snapshot_name, - volume_fakes.snapshot_description, - "available", - volume_fakes.snapshot_size, - "2015-06-03T18:49:19.000000", - volume_fakes.volume_name, - volume_fakes.EXPECTED_SNAPSHOT.get("properties") - ),) - self.assertEqual(datalist, tuple(data)) + self.assertEqual(self.columns_long, columns) + self.assertEqual(self.data_long, list(data)) def test_snapshot_list_all_projects(self): arglist = [ @@ -192,27 +227,17 @@ class TestSnapshotList(TestSnapshot): columns, data = self.cmd.take_action(parsed_args) self.assertEqual(self.columns, columns) - - datalist = (( - volume_fakes.snapshot_id, - volume_fakes.snapshot_name, - volume_fakes.snapshot_description, - "available", - volume_fakes.snapshot_size - ), ) - self.assertEqual(datalist, tuple(data)) + self.assertEqual(self.data, list(data)) class TestSnapshotSet(TestSnapshot): + snapshot = volume_fakes.FakeSnapshot.create_one_snapshot() + def setUp(self): super(TestSnapshotSet, self).setUp() - self.snapshots_mock.get.return_value = fakes.FakeResource( - None, - copy.deepcopy(volume_fakes.SNAPSHOT), - loaded=True - ) + self.snapshots_mock.get.return_value = self.snapshot self.snapshots_mock.set_metadata.return_value = None self.snapshots_mock.update.return_value = None # Get the command object to mock @@ -220,86 +245,125 @@ class TestSnapshotSet(TestSnapshot): def test_snapshot_set(self): arglist = [ - volume_fakes.snapshot_id, "--name", "new_snapshot", "--property", "x=y", - "--property", "foo=foo" + "--property", "foo=foo", + self.snapshot.id, ] new_property = {"x": "y", "foo": "foo"} verifylist = [ - ("snapshot", volume_fakes.snapshot_id), ("name", "new_snapshot"), - ("property", new_property) + ("property", new_property), + ("snapshot", self.snapshot.id), ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) kwargs = { "name": "new_snapshot", } - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.cmd.take_action(parsed_args) - self.snapshots_mock.update.assert_called_with( - volume_fakes.snapshot_id, **kwargs) + self.snapshot.id, **kwargs) self.snapshots_mock.set_metadata.assert_called_with( - volume_fakes.snapshot_id, new_property + self.snapshot.id, new_property ) + self.assertIsNone(result) + + def test_snapshot_set_state_to_error(self): + arglist = [ + "--state", "error", + self.snapshot.id + ] + verifylist = [ + ("state", "error"), + ("snapshot", self.snapshot.id) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.snapshots_mock.reset_state.assert_called_with( + self.snapshot.id, "error") + self.assertIsNone(result) class TestSnapshotShow(TestSnapshot): + columns = ( + 'created_at', + 'description', + 'id', + 'name', + 'properties', + 'size', + 'status', + 'volume_id', + ) + def setUp(self): super(TestSnapshotShow, self).setUp() - self.snapshots_mock.get.return_value = fakes.FakeResource( - None, - copy.deepcopy(volume_fakes.SNAPSHOT), - loaded=True) + self.snapshot = volume_fakes.FakeSnapshot.create_one_snapshot() + + self.data = ( + self.snapshot.created_at, + self.snapshot.description, + self.snapshot.id, + self.snapshot.name, + utils.format_dict(self.snapshot.metadata), + self.snapshot.size, + self.snapshot.status, + self.snapshot.volume_id, + ) + + self.snapshots_mock.get.return_value = self.snapshot # Get the command object to test self.cmd = snapshot.ShowSnapshot(self.app, None) def test_snapshot_show(self): arglist = [ - volume_fakes.snapshot_id + self.snapshot.id ] verifylist = [ - ("snapshot", volume_fakes.snapshot_id) + ("snapshot", self.snapshot.id) ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.snapshots_mock.get.assert_called_with(volume_fakes.snapshot_id) + self.snapshots_mock.get.assert_called_with(self.snapshot.id) - self.assertEqual(volume_fakes.SNAPSHOT_columns, columns) - self.assertEqual(volume_fakes.SNAPSHOT_data, data) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) class TestSnapshotUnset(TestSnapshot): + snapshot = volume_fakes.FakeSnapshot.create_one_snapshot() + def setUp(self): super(TestSnapshotUnset, self).setUp() - self.snapshots_mock.get.return_value = fakes.FakeResource( - None, - copy.deepcopy(volume_fakes.SNAPSHOT), - loaded=True - ) + self.snapshots_mock.get.return_value = self.snapshot self.snapshots_mock.delete_metadata.return_value = None # Get the command object to mock self.cmd = snapshot.UnsetSnapshot(self.app, None) def test_snapshot_unset(self): arglist = [ - volume_fakes.snapshot_id, - "--property", "foo" + "--property", "foo", + self.snapshot.id, ] verifylist = [ - ("snapshot", volume_fakes.snapshot_id), - ("property", ["foo"]) + ("property", ["foo"]), + ("snapshot", self.snapshot.id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.cmd.take_action(parsed_args) + + result = self.cmd.take_action(parsed_args) self.snapshots_mock.delete_metadata.assert_called_with( - volume_fakes.snapshot_id, ["foo"] + self.snapshot.id, ["foo"] ) + self.assertIsNone(result) diff --git a/openstackclient/tests/volume/v2/test_type.py b/openstackclient/tests/volume/v2/test_type.py index 1408b9d..872b4ae 100644 --- a/openstackclient/tests/volume/v2/test_type.py +++ b/openstackclient/tests/volume/v2/test_type.py @@ -14,25 +14,14 @@ import copy +from openstackclient.common import utils from openstackclient.tests import fakes +from openstackclient.tests.identity.v3 import fakes as identity_fakes +from openstackclient.tests import utils as tests_utils from openstackclient.tests.volume.v2 import fakes as volume_fakes from openstackclient.volume.v2 import volume_type -class FakeTypeResource(fakes.FakeResource): - - _keys = {'property': 'value'} - - def set_keys(self, args): - self._keys.update(args) - - def unset_keys(self, key): - self._keys.pop(key, None) - - def get_keys(self): - return self._keys - - class TestType(volume_fakes.TestVolume): def setUp(self): @@ -41,6 +30,13 @@ class TestType(volume_fakes.TestVolume): self.types_mock = self.app.client_manager.volume.volume_types self.types_mock.reset_mock() + self.types_access_mock = ( + self.app.client_manager.volume.volume_type_access) + self.types_access_mock.reset_mock() + + self.projects_mock = self.app.client_manager.identity.projects + self.projects_mock.reset_mock() + class TestTypeCreate(TestType): @@ -49,82 +45,78 @@ class TestTypeCreate(TestType): 'id', 'name', ) - datalist = ( - volume_fakes.type_description, - volume_fakes.type_id, - volume_fakes.type_name, - ) def setUp(self): super(TestTypeCreate, self).setUp() - self.types_mock.create.return_value = fakes.FakeResource( - None, - copy.deepcopy(volume_fakes.TYPE), - loaded=True + self.new_volume_type = volume_fakes.FakeType.create_one_type() + self.data = ( + self.new_volume_type.description, + self.new_volume_type.id, + self.new_volume_type.name, ) + + self.types_mock.create.return_value = self.new_volume_type # Get the command object to test self.cmd = volume_type.CreateVolumeType(self.app, None) def test_type_create_public(self): arglist = [ - volume_fakes.type_name, - "--description", volume_fakes.type_description, - "--public" + "--description", self.new_volume_type.description, + "--public", + self.new_volume_type.name, ] verifylist = [ - ("name", volume_fakes.type_name), - ("description", volume_fakes.type_description), + ("description", self.new_volume_type.description), ("public", True), ("private", False), + ("name", self.new_volume_type.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) self.types_mock.create.assert_called_with( - volume_fakes.type_name, - description=volume_fakes.type_description, + self.new_volume_type.name, + description=self.new_volume_type.description, is_public=True, ) self.assertEqual(self.columns, columns) - self.assertEqual(self.datalist, data) + self.assertEqual(self.data, data) def test_type_create_private(self): arglist = [ - volume_fakes.type_name, - "--description", volume_fakes.type_description, + "--description", self.new_volume_type.description, "--private", + self.new_volume_type.name, ] verifylist = [ - ("name", volume_fakes.type_name), - ("description", volume_fakes.type_description), + ("description", self.new_volume_type.description), ("public", False), ("private", True), + ("name", self.new_volume_type.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) self.types_mock.create.assert_called_with( - volume_fakes.type_name, - description=volume_fakes.type_description, + self.new_volume_type.name, + description=self.new_volume_type.description, is_public=False, ) self.assertEqual(self.columns, columns) - self.assertEqual(self.datalist, data) + self.assertEqual(self.data, data) class TestTypeDelete(TestType): + volume_type = volume_fakes.FakeType.create_one_type() + def setUp(self): super(TestTypeDelete, self).setUp() - self.types_mock.get.return_value = fakes.FakeResource( - None, - copy.deepcopy(volume_fakes.TYPE), - loaded=True - ) + self.types_mock.get.return_value = self.volume_type self.types_mock.delete.return_value = None # Get the command object to mock @@ -132,35 +124,51 @@ class TestTypeDelete(TestType): def test_type_delete(self): arglist = [ - volume_fakes.type_id + self.volume_type.id ] verifylist = [ - ("volume_type", volume_fakes.type_id) + ("volume_type", self.volume_type.id) ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.cmd.take_action(parsed_args) - self.types_mock.delete.assert_called_with(volume_fakes.type_id) + result = self.cmd.take_action(parsed_args) + + self.types_mock.delete.assert_called_with(self.volume_type.id) + self.assertIsNone(result) class TestTypeList(TestType): + volume_types = volume_fakes.FakeType.create_types() + columns = [ "ID", "Name" ] + columns_long = columns + [ + "Description", + "Properties" + ] + + data = [] + for t in volume_types: + data.append(( + t.id, + t.name, + )) + data_long = [] + for t in volume_types: + data_long.append(( + t.id, + t.name, + t.description, + utils.format_dict(t.extra_specs), + )) def setUp(self): super(TestTypeList, self).setUp() - self.types_mock.list.return_value = [ - fakes.FakeResource( - None, - copy.deepcopy(volume_fakes.TYPE), - loaded=True - ) - ] + self.types_mock.list.return_value = self.volume_types # get the command to test self.cmd = volume_type.ListVolumeType(self.app, None) @@ -173,11 +181,7 @@ class TestTypeList(TestType): columns, data = self.cmd.take_action(parsed_args) self.assertEqual(self.columns, columns) - datalist = (( - volume_fakes.type_id, - volume_fakes.type_name, - ),) - self.assertEqual(datalist, tuple(data)) + self.assertEqual(self.data, list(data)) def test_type_list_with_options(self): arglist = ["--long"] @@ -185,31 +189,26 @@ class TestTypeList(TestType): parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - columns = self.columns + [ - "Description", - "Properties" - ] - self.assertEqual(columns, columns) - datalist = (( - volume_fakes.type_id, - volume_fakes.type_name, - volume_fakes.type_description, - "foo='bar'" - ),) - self.assertEqual(datalist, tuple(data)) + self.assertEqual(self.columns_long, columns) + self.assertEqual(self.data_long, list(data)) class TestTypeSet(TestType): + volume_type = volume_fakes.FakeType.create_one_type( + methods={'set_keys': None}) + def setUp(self): super(TestTypeSet, self).setUp() - self.types_mock.get.return_value = FakeTypeResource( + self.types_mock.get.return_value = self.volume_type + + # Return a project + self.projects_mock.get.return_value = fakes.FakeResource( None, - copy.deepcopy(volume_fakes.TYPE), + copy.deepcopy(identity_fakes.PROJECT), loaded=True, ) - # Get the command object to test self.cmd = volume_type.SetVolumeType(self.app, None) @@ -217,129 +216,246 @@ class TestTypeSet(TestType): new_name = 'new_name' arglist = [ '--name', new_name, - volume_fakes.type_id, + self.volume_type.id, ] verifylist = [ ('name', new_name), ('description', None), ('property', None), - ('volume_type', volume_fakes.type_id), + ('volume_type', self.volume_type.id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.cmd.take_action(parsed_args) + result = self.cmd.take_action(parsed_args) # Set expected values kwargs = { 'name': new_name, } self.types_mock.update.assert_called_with( - volume_fakes.type_id, + self.volume_type.id, **kwargs ) + self.assertIsNone(result) def test_type_set_description(self): new_desc = 'new_desc' arglist = [ '--description', new_desc, - volume_fakes.type_id, + self.volume_type.id, ] verifylist = [ ('name', None), ('description', new_desc), ('property', None), - ('volume_type', volume_fakes.type_id), + ('volume_type', self.volume_type.id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.cmd.take_action(parsed_args) + result = self.cmd.take_action(parsed_args) # Set expected values kwargs = { 'description': new_desc, } self.types_mock.update.assert_called_with( - volume_fakes.type_id, + self.volume_type.id, **kwargs ) + self.assertIsNone(result) def test_type_set_property(self): arglist = [ '--property', 'myprop=myvalue', - volume_fakes.type_id, + self.volume_type.id, ] verifylist = [ ('name', None), ('description', None), ('property', {'myprop': 'myvalue'}), - ('volume_type', volume_fakes.type_id), + ('volume_type', self.volume_type.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + self.volume_type.set_keys.assert_called_once_with( + {'myprop': 'myvalue'}) + self.assertIsNone(result) + + def test_type_set_not_called_without_project_argument(self): + arglist = [ + '--project', '', + self.volume_type.id, + ] + verifylist = [ + ('project', ''), + ('volume_type', self.volume_type.id), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + self.assertIsNone(result) + + self.assertFalse(self.types_access_mock.add_project_access.called) + + def test_type_set_failed_with_missing_volume_type_argument(self): + arglist = [ + '--project', 'identity_fakes.project_id', + ] + verifylist = [ + ('project', 'identity_fakes.project_id'), + ] + + self.assertRaises(tests_utils.ParserException, + self.check_parser, + self.cmd, + arglist, + verifylist) + + def test_type_set_project_access(self): + arglist = [ + '--project', identity_fakes.project_id, + self.volume_type.id, + ] + verifylist = [ + ('project', identity_fakes.project_id), + ('volume_type', self.volume_type.id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.cmd.take_action(parsed_args) + result = self.cmd.take_action(parsed_args) + self.assertIsNone(result) - result = self.types_mock.get.return_value._keys - self.assertIn('myprop', result) - self.assertEqual('myvalue', result['myprop']) + self.types_access_mock.add_project_access.assert_called_with( + self.volume_type.id, + identity_fakes.project_id, + ) class TestTypeShow(TestType): + columns = ( + 'description', + 'id', + 'name', + 'properties', + ) + def setUp(self): super(TestTypeShow, self).setUp() - self.types_mock.get.return_value = fakes.FakeResource( - None, - copy.deepcopy(volume_fakes.TYPE), - loaded=True + self.volume_type = volume_fakes.FakeType.create_one_type() + self.data = ( + self.volume_type.description, + self.volume_type.id, + self.volume_type.name, + utils.format_dict(self.volume_type.extra_specs) ) + self.types_mock.get.return_value = self.volume_type + # Get the command object to test self.cmd = volume_type.ShowVolumeType(self.app, None) def test_type_show(self): arglist = [ - volume_fakes.type_id + self.volume_type.id ] verifylist = [ - ("volume_type", volume_fakes.type_id) + ("volume_type", self.volume_type.id) ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.types_mock.get.assert_called_with(volume_fakes.type_id) + self.types_mock.get.assert_called_with(self.volume_type.id) - self.assertEqual(volume_fakes.TYPE_FORMATTED_columns, columns) - self.assertEqual(volume_fakes.TYPE_FORMATTED_data, data) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) class TestTypeUnset(TestType): + volume_type = volume_fakes.FakeType.create_one_type( + methods={'unset_keys': None}) + def setUp(self): super(TestTypeUnset, self).setUp() - self.types_mock.get.return_value = FakeTypeResource( + self.types_mock.get.return_value = self.volume_type + + # Return a project + self.projects_mock.get.return_value = fakes.FakeResource( None, - copy.deepcopy(volume_fakes.TYPE), - loaded=True + copy.deepcopy(identity_fakes.PROJECT), + loaded=True, ) + # Get the command object to test self.cmd = volume_type.UnsetVolumeType(self.app, None) def test_type_unset(self): arglist = [ '--property', 'property', - volume_fakes.type_id, + self.volume_type.id, ] verifylist = [ ('property', 'property'), - ('volume_type', volume_fakes.type_id), + ('volume_type', self.volume_type.id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.cmd.take_action(parsed_args) + result = self.cmd.take_action(parsed_args) + self.volume_type.unset_keys.assert_called_once_with('property') + self.assertIsNone(result) - result = self.types_mock.get.return_value._keys + def test_type_unset_project_access(self): + arglist = [ + '--project', identity_fakes.project_id, + self.volume_type.id, + ] + verifylist = [ + ('project', identity_fakes.project_id), + ('volume_type', self.volume_type.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + self.assertIsNone(result) + + self.types_access_mock.remove_project_access.assert_called_with( + self.volume_type.id, + identity_fakes.project_id, + ) + + def test_type_unset_not_called_without_project_argument(self): + arglist = [ + '--project', '', + self.volume_type.id, + ] + verifylist = [ + ('project', ''), + ('volume_type', self.volume_type.id), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + self.assertIsNone(result) + + self.assertFalse(self.types_access_mock.remove_project_access.called) + + def test_type_unset_failed_with_missing_volume_type_argument(self): + arglist = [ + '--project', 'identity_fakes.project_id', + ] + verifylist = [ + ('project', 'identity_fakes.project_id'), + ] - self.assertNotIn('property', result) + self.assertRaises(tests_utils.ParserException, + self.check_parser, + self.cmd, + arglist, + verifylist) diff --git a/openstackclient/tests/volume/v2/test_volume.py b/openstackclient/tests/volume/v2/test_volume.py index cbca09b..85ff614 100644 --- a/openstackclient/tests/volume/v2/test_volume.py +++ b/openstackclient/tests/volume/v2/test_volume.py @@ -14,6 +14,7 @@ import copy +import mock from mock import call from openstackclient.common import utils @@ -40,6 +41,9 @@ class TestVolume(volume_fakes.TestVolume): self.images_mock = self.app.client_manager.image.images self.images_mock.reset_mock() + self.snapshots_mock = self.app.client_manager.volume.volume_snapshots + self.snapshots_mock.reset_mock() + def setup_volumes_mock(self, count): volumes = volume_fakes.FakeVolume.create_volumes(count=count) @@ -54,6 +58,7 @@ class TestVolumeCreate(TestVolume): columns = ( 'attachments', 'availability_zone', + 'bootable', 'description', 'id', 'name', @@ -73,6 +78,7 @@ class TestVolumeCreate(TestVolume): self.datalist = ( self.new_volume.attachments, self.new_volume.availability_zone, + self.new_volume.bootable, self.new_volume.description, self.new_volume.id, self.new_volume.name, @@ -376,6 +382,45 @@ class TestVolumeCreate(TestVolume): self.assertEqual(self.columns, columns) self.assertEqual(self.datalist, data) + def test_volume_create_with_snapshot(self): + arglist = [ + '--size', str(self.new_volume.size), + '--snapshot', volume_fakes.snapshot_id, + self.new_volume.name, + ] + verifylist = [ + ('size', self.new_volume.size), + ('snapshot', volume_fakes.snapshot_id), + ('name', self.new_volume.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + fake_snapshot = mock.Mock() + fake_snapshot.id = volume_fakes.snapshot_id + self.snapshots_mock.get.return_value = fake_snapshot + + # In base command class ShowOne in cliff, abstract method take_action() + # returns a two-part tuple with a tuple of column names and a tuple of + # data to be shown. + columns, data = self.cmd.take_action(parsed_args) + + self.volumes_mock.create.assert_called_once_with( + size=self.new_volume.size, + snapshot_id=fake_snapshot.id, + name=self.new_volume.name, + description=None, + volume_type=None, + user_id=None, + project_id=None, + availability_zone=None, + metadata=None, + imageRef=None, + source_volid=None + ) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.datalist, data) + class TestVolumeDelete(TestVolume): @@ -396,11 +441,12 @@ class TestVolumeDelete(TestVolume): verifylist = [ ("volumes", [volumes[0].id]) ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.cmd.take_action(parsed_args) + result = self.cmd.take_action(parsed_args) + self.volumes_mock.delete.assert_called_with(volumes[0].id) + self.assertIsNone(result) def test_volume_delete_multi_volumes(self): volumes = self.setup_volumes_mock(count=3) @@ -409,14 +455,13 @@ class TestVolumeDelete(TestVolume): verifylist = [ ('volumes', arglist), ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.cmd.take_action(parsed_args) + result = self.cmd.take_action(parsed_args) calls = [call(v.id) for v in volumes] - self.volumes_mock.delete.assert_has_calls(calls) + self.assertIsNone(result) class TestVolumeList(TestVolume): @@ -432,13 +477,8 @@ class TestVolumeList(TestVolume): def setUp(self): super(TestVolumeList, self).setUp() - self.volumes_mock.list.return_value = [ - fakes.FakeResource( - None, - copy.deepcopy(volume_fakes.VOLUME), - loaded=True, - ), - ] + self.mock_volume = volume_fakes.FakeVolume.create_one_volume() + self.volumes_mock.list.return_value = [self.mock_volume] self.users_mock.get.return_value = [ fakes.FakeResource( @@ -473,14 +513,14 @@ class TestVolumeList(TestVolume): self.assertEqual(self.columns, columns) - server = volume_fakes.volume_attachment_server['server_id'] - device = volume_fakes.volume_attachment_server['device'] + server = self.mock_volume.attachments[0]['server_id'] + device = self.mock_volume.attachments[0]['device'] msg = 'Attached to %s on %s ' % (server, device) datalist = (( - volume_fakes.volume_id, - volume_fakes.volume_name, - volume_fakes.volume_status, - volume_fakes.volume_size, + self.mock_volume.id, + self.mock_volume.name, + self.mock_volume.status, + self.mock_volume.size, msg, ), ) self.assertEqual(datalist, tuple(data)) @@ -501,14 +541,14 @@ class TestVolumeList(TestVolume): self.assertEqual(self.columns, columns) - server = volume_fakes.volume_attachment_server['server_id'] - device = volume_fakes.volume_attachment_server['device'] + server = self.mock_volume.attachments[0]['server_id'] + device = self.mock_volume.attachments[0]['device'] msg = 'Attached to %s on %s ' % (server, device) datalist = (( - volume_fakes.volume_id, - volume_fakes.volume_name, - volume_fakes.volume_status, - volume_fakes.volume_size, + self.mock_volume.id, + self.mock_volume.name, + self.mock_volume.status, + self.mock_volume.size, msg, ), ) self.assertEqual(datalist, tuple(data)) @@ -531,14 +571,14 @@ class TestVolumeList(TestVolume): self.assertEqual(self.columns, columns) - server = volume_fakes.volume_attachment_server['server_id'] - device = volume_fakes.volume_attachment_server['device'] + server = self.mock_volume.attachments[0]['server_id'] + device = self.mock_volume.attachments[0]['device'] msg = 'Attached to %s on %s ' % (server, device) datalist = (( - volume_fakes.volume_id, - volume_fakes.volume_name, - volume_fakes.volume_status, - volume_fakes.volume_size, + self.mock_volume.id, + self.mock_volume.name, + self.mock_volume.status, + self.mock_volume.size, msg, ), ) self.assertEqual(datalist, tuple(data)) @@ -558,14 +598,14 @@ class TestVolumeList(TestVolume): columns, data = self.cmd.take_action(parsed_args) self.assertEqual(self.columns, columns) - server = volume_fakes.volume_attachment_server['server_id'] - device = volume_fakes.volume_attachment_server['device'] + server = self.mock_volume.attachments[0]['server_id'] + device = self.mock_volume.attachments[0]['device'] msg = 'Attached to %s on %s ' % (server, device) datalist = (( - volume_fakes.volume_id, - volume_fakes.volume_name, - volume_fakes.volume_status, - volume_fakes.volume_size, + self.mock_volume.id, + self.mock_volume.name, + self.mock_volume.status, + self.mock_volume.size, msg, ), ) self.assertEqual(datalist, tuple(data)) @@ -588,14 +628,14 @@ class TestVolumeList(TestVolume): self.assertEqual(self.columns, columns) - server = volume_fakes.volume_attachment_server['server_id'] - device = volume_fakes.volume_attachment_server['device'] + server = self.mock_volume.attachments[0]['server_id'] + device = self.mock_volume.attachments[0]['device'] msg = 'Attached to %s on %s ' % (server, device) datalist = (( - volume_fakes.volume_id, - volume_fakes.volume_name, - volume_fakes.volume_status, - volume_fakes.volume_size, + self.mock_volume.id, + self.mock_volume.name, + self.mock_volume.status, + self.mock_volume.size, msg, ), ) self.assertEqual(datalist, tuple(data)) @@ -616,14 +656,14 @@ class TestVolumeList(TestVolume): self.assertEqual(self.columns, columns) - server = volume_fakes.volume_attachment_server['server_id'] - device = volume_fakes.volume_attachment_server['device'] + server = self.mock_volume.attachments[0]['server_id'] + device = self.mock_volume.attachments[0]['device'] msg = 'Attached to %s on %s ' % (server, device) datalist = (( - volume_fakes.volume_id, - volume_fakes.volume_name, - volume_fakes.volume_status, - volume_fakes.volume_size, + self.mock_volume.id, + self.mock_volume.name, + self.mock_volume.status, + self.mock_volume.size, msg, ), ) self.assertEqual(datalist, tuple(data)) @@ -644,14 +684,14 @@ class TestVolumeList(TestVolume): self.assertEqual(self.columns, columns) - server = volume_fakes.volume_attachment_server['server_id'] - device = volume_fakes.volume_attachment_server['device'] + server = self.mock_volume.attachments[0]['server_id'] + device = self.mock_volume.attachments[0]['device'] msg = 'Attached to %s on %s ' % (server, device) datalist = (( - volume_fakes.volume_id, - volume_fakes.volume_name, - volume_fakes.volume_status, - volume_fakes.volume_size, + self.mock_volume.id, + self.mock_volume.name, + self.mock_volume.status, + self.mock_volume.size, msg, ), ) self.assertEqual(datalist, tuple(data)) @@ -672,14 +712,14 @@ class TestVolumeList(TestVolume): self.assertEqual(self.columns, columns) - server = volume_fakes.volume_attachment_server['server_id'] - device = volume_fakes.volume_attachment_server['device'] + server = self.mock_volume.attachments[0]['server_id'] + device = self.mock_volume.attachments[0]['device'] msg = 'Attached to %s on %s ' % (server, device) datalist = (( - volume_fakes.volume_id, - volume_fakes.volume_name, - volume_fakes.volume_status, - volume_fakes.volume_size, + self.mock_volume.id, + self.mock_volume.name, + self.mock_volume.status, + self.mock_volume.size, msg, ), ) self.assertEqual(datalist, tuple(data)) @@ -711,18 +751,18 @@ class TestVolumeList(TestVolume): ] self.assertEqual(collist, columns) - server = volume_fakes.volume_attachment_server['server_id'] - device = volume_fakes.volume_attachment_server['device'] + server = self.mock_volume.attachments[0]['server_id'] + device = self.mock_volume.attachments[0]['device'] msg = 'Attached to %s on %s ' % (server, device) datalist = (( - volume_fakes.volume_id, - volume_fakes.volume_name, - volume_fakes.volume_status, - volume_fakes.volume_size, - volume_fakes.volume_type, - '', + self.mock_volume.id, + self.mock_volume.name, + self.mock_volume.status, + self.mock_volume.size, + self.mock_volume.volume_type, + self.mock_volume.bootable, msg, - "Alpha='a', Beta='b', Gamma='g'", + utils.format_dict(self.mock_volume.metadata), ), ) self.assertEqual(datalist, tuple(data)) @@ -732,24 +772,110 @@ class TestVolumeShow(TestVolume): def setUp(self): super(TestVolumeShow, self).setUp() - self.volumes_mock.get.return_value = fakes.FakeResource( - None, - copy.deepcopy(volume_fakes.VOLUME), - loaded=True) + self._volume = volume_fakes.FakeVolume.create_one_volume() + self.volumes_mock.get.return_value = self._volume # Get the command object to test self.cmd = volume.ShowVolume(self.app, None) def test_volume_show(self): arglist = [ - volume_fakes.volume_id + self._volume.id ] verifylist = [ - ("volume", volume_fakes.volume_id) + ("volume", self._volume.id) ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.volumes_mock.get.assert_called_with(volume_fakes.volume_id) + self.volumes_mock.get.assert_called_with(self._volume.id) + + self.assertEqual( + volume_fakes.FakeVolume.get_volume_columns(self._volume), + columns) + + self.assertEqual( + volume_fakes.FakeVolume.get_volume_data(self._volume), + data) + + +class TestVolumeSet(TestVolume): + + def setUp(self): + super(TestVolumeSet, self).setUp() + + self.new_volume = volume_fakes.FakeVolume.create_one_volume() + self.volumes_mock.create.return_value = self.new_volume + + # Get the command object to test + self.cmd = volume.SetVolume(self.app, None) + + def test_volume_set_image_property(self): + arglist = [ + '--image-property', 'Alpha=a', + '--image-property', 'Beta=b', + self.new_volume.id, + ] + verifylist = [ + ('image_property', {'Alpha': 'a', 'Beta': 'b'}), + ('volume', self.new_volume.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # In base command class ShowOne in cliff, abstract method take_action() + # returns nothing + self.cmd.take_action(parsed_args) + self.volumes_mock.set_image_metadata.assert_called_with( + self.volumes_mock.get().id, parsed_args.image_property) + + +class TestVolumeUnset(TestVolume): + + def setUp(self): + super(TestVolumeUnset, self).setUp() + + self.new_volume = volume_fakes.FakeVolume.create_one_volume() + self.volumes_mock.create.return_value = self.new_volume + + # Get the command object to set property + self.cmd_set = volume.SetVolume(self.app, None) + + # Get the command object to unset property + self.cmd_unset = volume.UnsetVolume(self.app, None) + + def test_volume_unset_image_property(self): + + # Arguments for setting image properties + arglist = [ + '--image-property', 'Alpha=a', + '--image-property', 'Beta=b', + self.new_volume.id, + ] + verifylist = [ + ('image_property', {'Alpha': 'a', 'Beta': 'b'}), + ('volume', self.new_volume.id), + ] + parsed_args = self.check_parser(self.cmd_set, arglist, verifylist) + + # In base command class ShowOne in cliff, abstract method take_action() + # returns nothing + self.cmd_set.take_action(parsed_args) + + # Arguments for unsetting image properties + arglist_unset = [ + '--image-property', 'Alpha', + self.new_volume.id, + ] + verifylist_unset = [ + ('image_property', ['Alpha']), + ('volume', self.new_volume.id), + ] + parsed_args_unset = self.check_parser(self.cmd_unset, + arglist_unset, + verifylist_unset) + + # In base command class ShowOne in cliff, abstract method take_action() + # returns nothing + self.cmd_unset.take_action(parsed_args_unset) - self.assertEqual(volume_fakes.VOLUME_columns, columns) - self.assertEqual(volume_fakes.VOLUME_data, data) + self.volumes_mock.delete_image_metadata.assert_called_with( + self.volumes_mock.get().id, parsed_args_unset.image_property) diff --git a/openstackclient/volume/client.py b/openstackclient/volume/client.py index 0973868..a60f4b0 100644 --- a/openstackclient/volume/client.py +++ b/openstackclient/volume/client.py @@ -16,6 +16,7 @@ import logging from openstackclient.common import utils +from openstackclient.i18n import _ LOG = logging.getLogger(__name__) @@ -73,7 +74,7 @@ def build_option_parser(parser): '--os-volume-api-version', metavar='<volume-api-version>', default=utils.env('OS_VOLUME_API_VERSION'), - help='Volume API version, default=' + - DEFAULT_API_VERSION + - ' (Env: OS_VOLUME_API_VERSION)') + help=_('Volume API version, default=%s ' + '(Env: OS_VOLUME_API_VERSION)') % DEFAULT_API_VERSION + ) return parser diff --git a/openstackclient/volume/v1/backup.py b/openstackclient/volume/v1/backup.py index 32f39fb..607b521 100644 --- a/openstackclient/volume/v1/backup.py +++ b/openstackclient/volume/v1/backup.py @@ -20,6 +20,7 @@ import six from openstackclient.common import command from openstackclient.common import utils +from openstackclient.i18n import _ class CreateBackup(command.ShowOne): @@ -30,24 +31,23 @@ class CreateBackup(command.ShowOne): parser.add_argument( 'volume', metavar='<volume>', - help='Volume to backup (name or ID)', + help=_('Volume to backup (name or ID)'), ) parser.add_argument( '--container', metavar='<container>', required=False, - help='Optional backup container name', + help=_('Optional backup container name'), ) parser.add_argument( '--name', metavar='<name>', - required=False, - help='Name of the backup', + help=_('Name of the backup'), ) parser.add_argument( '--description', metavar='<description>', - help='Description of the backup', + help=_('Description of the backup'), ) return parser @@ -75,7 +75,7 @@ class DeleteBackup(command.Command): 'backups', metavar='<backup>', nargs="+", - help='Backup(s) to delete (ID only)', + help=_('Backup(s) to delete (ID only)'), ) return parser @@ -96,7 +96,7 @@ class ListBackup(command.Lister): '--long', action='store_true', default=False, - help='List additional fields in output', + help=_('List additional fields in output'), ) return parser @@ -149,11 +149,13 @@ class RestoreBackup(command.Command): parser.add_argument( 'backup', metavar='<backup>', - help='Backup to restore (ID only)') + help=_('Backup to restore (ID only)') + ) parser.add_argument( 'volume', metavar='<volume>', - help='Volume to restore to (name or ID)') + help=_('Volume to restore to (name or ID)') + ) return parser def take_action(self, parsed_args): @@ -174,7 +176,8 @@ class ShowBackup(command.ShowOne): parser.add_argument( 'backup', metavar='<backup>', - help='Backup to display (ID only)') + help=_('Backup to display (ID only)') + ) return parser def take_action(self, parsed_args): diff --git a/openstackclient/volume/v1/qos_specs.py b/openstackclient/volume/v1/qos_specs.py index 826e5c4..b9eb875 100644 --- a/openstackclient/volume/v1/qos_specs.py +++ b/openstackclient/volume/v1/qos_specs.py @@ -20,6 +20,7 @@ import six from openstackclient.common import command from openstackclient.common import parseractions from openstackclient.common import utils +from openstackclient.i18n import _ class AssociateQos(command.Command): @@ -30,12 +31,12 @@ class AssociateQos(command.Command): parser.add_argument( 'qos_spec', metavar='<qos-spec>', - help='QoS specification to modify (name or ID)', + help=_('QoS specification to modify (name or ID)'), ) parser.add_argument( 'volume_type', metavar='<volume-type>', - help='Volume type to associate the QoS (name or ID)', + help=_('Volume type to associate the QoS (name or ID)'), ) return parser @@ -57,7 +58,7 @@ class CreateQos(command.ShowOne): parser.add_argument( 'name', metavar='<name>', - help='New QoS specification name', + help=_('New QoS specification name'), ) consumer_choices = ['front-end', 'back-end', 'both'] parser.add_argument( @@ -65,15 +66,16 @@ class CreateQos(command.ShowOne): metavar='<consumer>', choices=consumer_choices, default='both', - help='Consumer of the QoS. Valid consumers: %s ' - "(defaults to 'both')" % utils.format_list(consumer_choices) + help=(_('Consumer of the QoS. Valid consumers: %s ' + "(defaults to 'both')") % + utils.format_list(consumer_choices)) ) parser.add_argument( '--property', metavar='<key=value>', action=parseractions.KeyValueAction, - help='Set a QoS specification property ' - '(repeat option to set multiple properties)', + help=_('Set a QoS specification property ' + '(repeat option to set multiple properties)'), ) return parser @@ -99,7 +101,7 @@ class DeleteQos(command.Command): 'qos_specs', metavar='<qos-spec>', nargs="+", - help='QoS specification(s) to delete (name or ID)', + help=_('QoS specification(s) to delete (name or ID)'), ) return parser @@ -118,19 +120,19 @@ class DisassociateQos(command.Command): parser.add_argument( 'qos_spec', metavar='<qos-spec>', - help='QoS specification to modify (name or ID)', + help=_('QoS specification to modify (name or ID)'), ) volume_type_group = parser.add_mutually_exclusive_group() volume_type_group.add_argument( '--volume-type', metavar='<volume-type>', - help='Volume type to disassociate the QoS from (name or ID)', + help=_('Volume type to disassociate the QoS from (name or ID)'), ) volume_type_group.add_argument( '--all', action='store_true', default=False, - help='Disassociate the QoS from every volume type', + help=_('Disassociate the QoS from every volume type'), ) return parser @@ -181,14 +183,14 @@ class SetQos(command.Command): parser.add_argument( 'qos_spec', metavar='<qos-spec>', - help='QoS specification to modify (name or ID)', + help=_('QoS specification to modify (name or ID)'), ) parser.add_argument( '--property', metavar='<key=value>', action=parseractions.KeyValueAction, - help='Property to add or modify for this QoS specification ' - '(repeat option to set multiple properties)', + help=_('Property to add or modify for this QoS specification ' + '(repeat option to set multiple properties)'), ) return parser @@ -201,7 +203,7 @@ class SetQos(command.Command): volume_client.qos_specs.set_keys(qos_spec.id, parsed_args.property) else: - self.app.log.error("No changes requested\n") + self.app.log.error(_("No changes requested\n")) class ShowQos(command.ShowOne): @@ -212,7 +214,7 @@ class ShowQos(command.ShowOne): parser.add_argument( 'qos_spec', metavar='<qos-spec>', - help='QoS specification to display (name or ID)', + help=_('QoS specification to display (name or ID)'), ) return parser @@ -241,15 +243,15 @@ class UnsetQos(command.Command): parser.add_argument( 'qos_spec', metavar='<qos-spec>', - help='QoS specification to modify (name or ID)', + help=_('QoS specification to modify (name or ID)'), ) parser.add_argument( '--property', metavar='<key>', action='append', default=[], - help='Property to remove from the QoS specification. ' - '(repeat option to unset multiple properties)', + help=_('Property to remove from the QoS specification. ' + '(repeat option to unset multiple properties)'), ) return parser @@ -262,4 +264,4 @@ class UnsetQos(command.Command): volume_client.qos_specs.unset_keys(qos_spec.id, parsed_args.property) else: - self.app.log.error("No changes requested\n") + self.app.log.error(_("No changes requested\n")) diff --git a/openstackclient/volume/v1/service.py b/openstackclient/volume/v1/service.py new file mode 100644 index 0000000..023dda9 --- /dev/null +++ b/openstackclient/volume/v1/service.py @@ -0,0 +1,73 @@ +# +# 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. +# + +"""Service action implementations""" + +from openstackclient.common import command +from openstackclient.common import utils +from openstackclient.i18n import _ + + +class ListService(command.Lister): + """List service command""" + + def get_parser(self, prog_name): + parser = super(ListService, self).get_parser(prog_name) + parser.add_argument( + "--host", + metavar="<host>", + help=_("List services on specified host (name only)") + ) + parser.add_argument( + "--service", + metavar="<service>", + help=_("List only specified service (name only)") + ) + parser.add_argument( + "--long", + action="store_true", + default=False, + help=_("List additional fields in output") + ) + return parser + + def take_action(self, parsed_args): + service_client = self.app.client_manager.volume + + if parsed_args.long: + columns = [ + "Binary", + "Host", + "Zone", + "Status", + "State", + "Updated At", + "Disabled Reason" + ] + else: + columns = [ + "Binary", + "Host", + "Zone", + "Status", + "State", + "Updated At" + ] + + data = service_client.services.list(parsed_args.host, + parsed_args.service) + return (columns, + (utils.get_item_properties( + s, columns, + ) for s in data)) diff --git a/openstackclient/volume/v1/snapshot.py b/openstackclient/volume/v1/snapshot.py index 95200e4..bf5bf26 100644 --- a/openstackclient/volume/v1/snapshot.py +++ b/openstackclient/volume/v1/snapshot.py @@ -21,6 +21,7 @@ import six from openstackclient.common import command from openstackclient.common import parseractions from openstackclient.common import utils +from openstackclient.i18n import _ class CreateSnapshot(command.ShowOne): @@ -31,25 +32,25 @@ class CreateSnapshot(command.ShowOne): parser.add_argument( 'volume', metavar='<volume>', - help='Volume to snapshot (name or ID)', + help=_('Volume to snapshot (name or ID)'), ) parser.add_argument( '--name', metavar='<name>', - required=True, - help='Name of the snapshot', + help=_('Name of the snapshot'), ) parser.add_argument( '--description', metavar='<description>', - help='Description of the snapshot', + help=_('Description of the snapshot'), ) parser.add_argument( '--force', dest='force', action='store_true', default=False, - help='Create a snapshot attached to an instance. Default is False', + help=_('Create a snapshot attached to an instance. ' + 'Default is False'), ) return parser @@ -80,7 +81,7 @@ class DeleteSnapshot(command.Command): 'snapshots', metavar='<snapshot>', nargs="+", - help='Snapshot(s) to delete (name or ID)', + help=_('Snapshot(s) to delete (name or ID)'), ) return parser @@ -101,13 +102,13 @@ class ListSnapshot(command.Lister): '--all-projects', action='store_true', default=False, - help='Include all projects (admin only)', + help=_('Include all projects (admin only)'), ) parser.add_argument( '--long', action='store_true', default=False, - help='List additional fields in output', + help=_('List additional fields in output'), ) return parser @@ -171,21 +172,24 @@ class SetSnapshot(command.Command): parser.add_argument( 'snapshot', metavar='<snapshot>', - help='Snapshot to modify (name or ID)') + help=_('Snapshot to modify (name or ID)') + ) parser.add_argument( '--name', metavar='<name>', - help='New snapshot name') + help=_('New snapshot name') + ) parser.add_argument( '--description', metavar='<description>', - help='New snapshot description') + help=_('New snapshot description') + ) parser.add_argument( '--property', metavar='<key=value>', action=parseractions.KeyValueAction, - help='Property to add/change for this snapshot ' - '(repeat option to set multiple properties)', + help=_('Property to add/change for this snapshot ' + '(repeat option to set multiple properties)'), ) return parser @@ -205,7 +209,7 @@ class SetSnapshot(command.Command): kwargs['display_description'] = parsed_args.description if not kwargs and not parsed_args.property: - self.app.log.error("No changes requested\n") + self.app.log.error(_("No changes requested\n")) return snapshot.update(**kwargs) @@ -219,7 +223,8 @@ class ShowSnapshot(command.ShowOne): parser.add_argument( 'snapshot', metavar='<snapshot>', - help='Snapshot to display (name or ID)') + help=_('Snapshot to display (name or ID)') + ) return parser def take_action(self, parsed_args): @@ -242,16 +247,16 @@ class UnsetSnapshot(command.Command): parser.add_argument( 'snapshot', metavar='<snapshot>', - help='Snapshot to modify (name or ID)', + help=_('Snapshot to modify (name or ID)'), ) parser.add_argument( '--property', metavar='<key>', action='append', default=[], - help='Property to remove from snapshot ' - '(repeat to remove multiple values)', required=True, + help=_('Property to remove from snapshot ' + '(repeat option to remove multiple properties)'), ) return parser @@ -266,4 +271,4 @@ class UnsetSnapshot(command.Command): parsed_args.property, ) else: - self.app.log.error("No changes requested\n") + self.app.log.error(_("No changes requested\n")) diff --git a/openstackclient/volume/v1/volume.py b/openstackclient/volume/v1/volume.py index 90827d2..11e42f8 100644 --- a/openstackclient/volume/v1/volume.py +++ b/openstackclient/volume/v1/volume.py @@ -21,6 +21,7 @@ import six from openstackclient.common import command from openstackclient.common import parseractions from openstackclient.common import utils +from openstackclient.i18n import _ class CreateVolume(command.ShowOne): @@ -31,20 +32,30 @@ class CreateVolume(command.ShowOne): parser.add_argument( 'name', metavar='<name>', - help='New volume name', + help=_('Volume name'), ) parser.add_argument( '--size', metavar='<size>', required=True, type=int, - help='New volume size in GB', + help=_('Volume size in GB'), + ) + parser.add_argument( + '--type', + metavar='<volume-type>', + help=_("Set the type of volume"), + ) + parser.add_argument( + '--image', + metavar='<image>', + help=_('Use <image> as source of volume (name or ID)'), ) snapshot_group = parser.add_mutually_exclusive_group() snapshot_group.add_argument( '--snapshot', metavar='<snapshot>', - help='Use <snapshot> as source of new volume', + help=_('Use <snapshot> as source of volume (name or ID)'), ) snapshot_group.add_argument( '--snapshot-id', @@ -52,46 +63,36 @@ class CreateVolume(command.ShowOne): help=argparse.SUPPRESS, ) parser.add_argument( - '--description', - metavar='<description>', - help='New volume description', + '--source', + metavar='<volume>', + help=_('Volume to clone (name or ID)'), ) parser.add_argument( - '--type', - metavar='<volume-type>', - help='Use <volume-type> as the new volume type', + '--description', + metavar='<description>', + help=_('Volume description'), ) parser.add_argument( '--user', metavar='<user>', - help='Specify an alternate user (name or ID)', + help=_('Specify an alternate user (name or ID)'), ) parser.add_argument( '--project', metavar='<project>', - help='Specify an alternate project (name or ID)', + help=_('Specify an alternate project (name or ID)'), ) parser.add_argument( '--availability-zone', metavar='<availability-zone>', - help='Create new volume in <availability-zone>', - ) - parser.add_argument( - '--image', - metavar='<image>', - help='Use <image> as source of new volume (name or ID)', - ) - parser.add_argument( - '--source', - metavar='<volume>', - help='Volume to clone (name or ID)', + help=_('Create volume in <availability-zone>'), ) parser.add_argument( '--property', metavar='<key=value>', action=parseractions.KeyValueAction, - help='Set a property on this volume ' - '(repeat option to set multiple properties)', + help=_('Set a property on this volume ' + '(repeat option to set multiple properties)'), ) return parser @@ -165,15 +166,15 @@ class DeleteVolume(command.Command): 'volumes', metavar='<volume>', nargs="+", - help='Volume(s) to delete (name or ID)', + help=_('Volume(s) to delete (name or ID)'), ) parser.add_argument( '--force', dest='force', action='store_true', default=False, - help='Attempt forced removal of volume(s), regardless of state ' - '(defaults to False)', + help=_('Attempt forced removal of volume(s), regardless of state ' + '(defaults to False)'), ) return parser @@ -196,24 +197,24 @@ class ListVolume(command.Lister): parser.add_argument( '--name', metavar='<name>', - help='Filter results by volume name', + help=_('Filter results by volume name'), ) parser.add_argument( '--status', metavar='<status>', - help='Filter results by status', + help=_('Filter results by status'), ) parser.add_argument( '--all-projects', action='store_true', default=False, - help='Include all projects (admin only)', + help=_('Include all projects (admin only)'), ) parser.add_argument( '--long', action='store_true', default=False, - help='List additional fields in output', + help=_('List additional fields in output'), ) return parser @@ -308,30 +309,30 @@ class SetVolume(command.Command): parser.add_argument( 'volume', metavar='<volume>', - help='Volume to change (name or ID)', + help=_('Volume to modify (name or ID)'), ) parser.add_argument( '--name', metavar='<name>', - help='New volume name', + help=_('New volume name'), ) parser.add_argument( '--description', metavar='<description>', - help='New volume description', + help=_('New volume description'), ) parser.add_argument( '--size', metavar='<size>', type=int, - help='Extend volume size in GB', + help=_('Extend volume size in GB'), ) parser.add_argument( '--property', metavar='<key=value>', action=parseractions.KeyValueAction, - help='Property to add or modify for this volume ' - '(repeat option to set multiple properties)', + help=_('Set a property on this volume ' + '(repeat option to set multiple properties)'), ) return parser @@ -341,12 +342,12 @@ class SetVolume(command.Command): if parsed_args.size: if volume.status != 'available': - self.app.log.error("Volume is in %s state, it must be " - "available before size can be extended" % + self.app.log.error(_("Volume is in %s state, it must be " + "available before size can be extended") % volume.status) return if parsed_args.size <= volume.size: - self.app.log.error("New size must be greater than %s GB" % + self.app.log.error(_("New size must be greater than %s GB") % volume.size) return volume_client.volumes.extend(volume.id, parsed_args.size) @@ -363,7 +364,7 @@ class SetVolume(command.Command): volume_client.volumes.update(volume.id, **kwargs) if not kwargs and not parsed_args.property and not parsed_args.size: - self.app.log.error("No changes requested\n") + self.app.log.error(_("No changes requested\n")) class ShowVolume(command.ShowOne): @@ -374,7 +375,7 @@ class ShowVolume(command.ShowOne): parser.add_argument( 'volume', metavar='<volume>', - help='Volume to display (name or ID)', + help=_('Volume to display (name or ID)'), ) return parser @@ -404,15 +405,15 @@ class UnsetVolume(command.Command): parser.add_argument( 'volume', metavar='<volume>', - help='Volume to modify (name or ID)', + help=_('Volume to modify (name or ID)'), ) parser.add_argument( '--property', metavar='<key>', action='append', default=[], - help='Property to remove from volume ' - '(repeat option to remove multiple properties)', + help=_('Remove a property from volume ' + '(repeat option to remove multiple properties)'), required=True, ) return parser @@ -428,4 +429,4 @@ class UnsetVolume(command.Command): parsed_args.property, ) else: - self.app.log.error("No changes requested\n") + self.app.log.error(_("No changes requested\n")) diff --git a/openstackclient/volume/v1/volume_type.py b/openstackclient/volume/v1/volume_type.py index 24d0b23..7392702 100644 --- a/openstackclient/volume/v1/volume_type.py +++ b/openstackclient/volume/v1/volume_type.py @@ -20,6 +20,7 @@ import six from openstackclient.common import command from openstackclient.common import parseractions from openstackclient.common import utils +from openstackclient.i18n import _ class CreateVolumeType(command.ShowOne): @@ -30,14 +31,14 @@ class CreateVolumeType(command.ShowOne): parser.add_argument( 'name', metavar='<name>', - help='New volume type name', + help=_('Volume type name'), ) parser.add_argument( '--property', metavar='<key=value>', action=parseractions.KeyValueAction, - help='Property to add for this volume type ' - '(repeat option to set multiple properties)', + help=_('Set a property on this volume type ' + '(repeat option to set multiple properties)'), ) return parser @@ -62,7 +63,7 @@ class DeleteVolumeType(command.Command): parser.add_argument( 'volume_type', metavar='<volume-type>', - help='Volume type to delete (name or ID)', + help=_('Volume type to delete (name or ID)'), ) return parser @@ -82,7 +83,8 @@ class ListVolumeType(command.Lister): '--long', action='store_true', default=False, - help='List additional fields in output') + help=_('List additional fields in output') + ) return parser def take_action(self, parsed_args): @@ -108,14 +110,14 @@ class SetVolumeType(command.Command): parser.add_argument( 'volume_type', metavar='<volume-type>', - help='Volume type to modify (name or ID)', + help=_('Volume type to modify (name or ID)'), ) parser.add_argument( '--property', metavar='<key=value>', action=parseractions.KeyValueAction, - help='Property to add or modify for this volume type ' - '(repeat option to set multiple properties)', + help=_('Set a property on this volume type ' + '(repeat option to set multiple properties)'), ) return parser @@ -128,6 +130,27 @@ class SetVolumeType(command.Command): volume_type.set_keys(parsed_args.property) +class ShowVolumeType(command.ShowOne): + """Display volume type details""" + + def get_parser(self, prog_name): + parser = super(ShowVolumeType, self).get_parser(prog_name) + parser.add_argument( + "volume_type", + metavar="<volume-type>", + help=_("Volume type to display (name or ID)") + ) + return parser + + def take_action(self, parsed_args): + volume_client = self.app.client_manager.volume + volume_type = utils.find_resource( + volume_client.volume_types, parsed_args.volume_type) + properties = utils.format_dict(volume_type._info.pop('extra_specs')) + volume_type._info.update({'properties': properties}) + return zip(*sorted(six.iteritems(volume_type._info))) + + class UnsetVolumeType(command.Command): """Unset volume type properties""" @@ -136,15 +159,15 @@ class UnsetVolumeType(command.Command): parser.add_argument( 'volume_type', metavar='<volume-type>', - help='Volume type to modify (name or ID)', + help=_('Volume type to modify (name or ID)'), ) parser.add_argument( '--property', metavar='<key>', action='append', default=[], - help='Property to remove from volume type ' - '(repeat option to remove multiple properties)', + help=_('Remove a property from this volume type ' + '(repeat option to remove multiple properties)'), required=True, ) return parser @@ -159,25 +182,4 @@ class UnsetVolumeType(command.Command): if parsed_args.property: volume_type.unset_keys(parsed_args.property) else: - self.app.log.error("No changes requested\n") - - -class ShowVolumeType(command.ShowOne): - """Display volume type details""" - - def get_parser(self, prog_name): - parser = super(ShowVolumeType, self).get_parser(prog_name) - parser.add_argument( - "volume_type", - metavar="<volume-type>", - help="Volume type to display (name or ID)" - ) - return parser - - def take_action(self, parsed_args): - volume_client = self.app.client_manager.volume - volume_type = utils.find_resource( - volume_client.volume_types, parsed_args.volume_type) - properties = utils.format_dict(volume_type._info.pop('extra_specs')) - volume_type._info.update({'properties': properties}) - return zip(*sorted(six.iteritems(volume_type._info))) + self.app.log.error(_("No changes requested\n")) diff --git a/openstackclient/volume/v2/backup.py b/openstackclient/volume/v2/backup.py index 64ca97a..e6fbe78 100644 --- a/openstackclient/volume/v2/backup.py +++ b/openstackclient/volume/v2/backup.py @@ -20,6 +20,7 @@ import six from openstackclient.common import command from openstackclient.common import utils +from openstackclient.i18n import _ class CreateBackup(command.ShowOne): @@ -30,23 +31,22 @@ class CreateBackup(command.ShowOne): parser.add_argument( "volume", metavar="<volume>", - help="Volume to backup (name or ID)" + help=_("Volume to backup (name or ID)") ) parser.add_argument( "--name", metavar="<name>", - required=True, - help="Name of the backup" + help=_("Name of the backup") ) parser.add_argument( "--description", metavar="<description>", - help="Description of the backup" + help=_("Description of the backup") ) parser.add_argument( "--container", metavar="<container>", - help="Optional backup container name" + help=_("Optional backup container name") ) return parser @@ -73,7 +73,7 @@ class DeleteBackup(command.Command): "backups", metavar="<backup>", nargs="+", - help="Backup(s) to delete (name or ID)" + help=_("Backup(s) to delete (name or ID)") ) return parser @@ -94,7 +94,7 @@ class ListBackup(command.Lister): "--long", action="store_true", default=False, - help="List additional fields in output" + help=_("List additional fields in output") ) return parser @@ -147,12 +147,12 @@ class RestoreBackup(command.ShowOne): parser.add_argument( "backup", metavar="<backup>", - help="Backup to restore (ID only)" + help=_("Backup to restore (ID only)") ) parser.add_argument( "volume", metavar="<volume>", - help="Volume to restore to (name or ID)" + help=_("Volume to restore to (name or ID)") ) return parser @@ -172,7 +172,8 @@ class ShowBackup(command.ShowOne): parser.add_argument( "backup", metavar="<backup>", - help="Backup to display (name or ID)") + help=_("Backup to display (name or ID)") + ) return parser def take_action(self, parsed_args): diff --git a/openstackclient/volume/v2/qos_specs.py b/openstackclient/volume/v2/qos_specs.py index 961cc27..aa2059d 100644 --- a/openstackclient/volume/v2/qos_specs.py +++ b/openstackclient/volume/v2/qos_specs.py @@ -20,6 +20,7 @@ import six from openstackclient.common import command from openstackclient.common import parseractions from openstackclient.common import utils +from openstackclient.i18n import _ class AssociateQos(command.Command): @@ -30,12 +31,12 @@ class AssociateQos(command.Command): parser.add_argument( 'qos_spec', metavar='<qos-spec>', - help='QoS specification to modify (name or ID)', + help=_('QoS specification to modify (name or ID)'), ) parser.add_argument( 'volume_type', metavar='<volume-type>', - help='Volume type to associate the QoS (name or ID)', + help=_('Volume type to associate the QoS (name or ID)'), ) return parser @@ -57,7 +58,7 @@ class CreateQos(command.ShowOne): parser.add_argument( 'name', metavar='<name>', - help='New QoS specification name', + help=_('New QoS specification name'), ) consumer_choices = ['front-end', 'back-end', 'both'] parser.add_argument( @@ -65,15 +66,16 @@ class CreateQos(command.ShowOne): metavar='<consumer>', choices=consumer_choices, default='both', - help='Consumer of the QoS. Valid consumers: %s ' - "(defaults to 'both')" % utils.format_list(consumer_choices) + help=(_('Consumer of the QoS. Valid consumers: %s ' + "(defaults to 'both')") % + utils.format_list(consumer_choices)) ) parser.add_argument( '--property', metavar='<key=value>', action=parseractions.KeyValueAction, - help='Set a QoS specification property ' - '(repeat option to set multiple properties)', + help=_('Set a QoS specification property ' + '(repeat option to set multiple properties)'), ) return parser @@ -99,7 +101,7 @@ class DeleteQos(command.Command): 'qos_specs', metavar='<qos-spec>', nargs="+", - help='QoS specification(s) to delete (name or ID)', + help=_('QoS specification(s) to delete (name or ID)'), ) return parser @@ -118,19 +120,19 @@ class DisassociateQos(command.Command): parser.add_argument( 'qos_spec', metavar='<qos-spec>', - help='QoS specification to modify (name or ID)', + help=_('QoS specification to modify (name or ID)'), ) volume_type_group = parser.add_mutually_exclusive_group() volume_type_group.add_argument( '--volume-type', metavar='<volume-type>', - help='Volume type to disassociate the QoS from (name or ID)', + help=_('Volume type to disassociate the QoS from (name or ID)'), ) volume_type_group.add_argument( '--all', action='store_true', default=False, - help='Disassociate the QoS from every volume type', + help=_('Disassociate the QoS from every volume type'), ) return parser @@ -181,14 +183,14 @@ class SetQos(command.Command): parser.add_argument( 'qos_spec', metavar='<qos-spec>', - help='QoS specification to modify (name or ID)', + help=_('QoS specification to modify (name or ID)'), ) parser.add_argument( '--property', metavar='<key=value>', action=parseractions.KeyValueAction, - help='Property to add or modify for this QoS specification ' - '(repeat option to set multiple properties)', + help=_('Property to add or modify for this QoS specification ' + '(repeat option to set multiple properties)'), ) return parser @@ -201,7 +203,7 @@ class SetQos(command.Command): volume_client.qos_specs.set_keys(qos_spec.id, parsed_args.property) else: - self.app.log.error("No changes requested\n") + self.app.log.error(_("No changes requested\n")) class ShowQos(command.ShowOne): @@ -212,7 +214,7 @@ class ShowQos(command.ShowOne): parser.add_argument( 'qos_spec', metavar='<qos-spec>', - help='QoS specification to display (name or ID)', + help=_('QoS specification to display (name or ID)'), ) return parser @@ -241,15 +243,15 @@ class UnsetQos(command.Command): parser.add_argument( 'qos_spec', metavar='<qos-spec>', - help='QoS specification to modify (name or ID)', + help=_('QoS specification to modify (name or ID)'), ) parser.add_argument( '--property', metavar='<key>', action='append', default=[], - help='Property to remove from the QoS specification. ' - '(repeat option to unset multiple properties)', + help=('Property to remove from the QoS specification. ' + '(repeat option to unset multiple properties)'), ) return parser @@ -262,4 +264,4 @@ class UnsetQos(command.Command): volume_client.qos_specs.unset_keys(qos_spec.id, parsed_args.property) else: - self.app.log.error("No changes requested\n") + self.app.log.error(_("No changes requested\n")) diff --git a/openstackclient/volume/v2/service.py b/openstackclient/volume/v2/service.py new file mode 100644 index 0000000..023dda9 --- /dev/null +++ b/openstackclient/volume/v2/service.py @@ -0,0 +1,73 @@ +# +# 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. +# + +"""Service action implementations""" + +from openstackclient.common import command +from openstackclient.common import utils +from openstackclient.i18n import _ + + +class ListService(command.Lister): + """List service command""" + + def get_parser(self, prog_name): + parser = super(ListService, self).get_parser(prog_name) + parser.add_argument( + "--host", + metavar="<host>", + help=_("List services on specified host (name only)") + ) + parser.add_argument( + "--service", + metavar="<service>", + help=_("List only specified service (name only)") + ) + parser.add_argument( + "--long", + action="store_true", + default=False, + help=_("List additional fields in output") + ) + return parser + + def take_action(self, parsed_args): + service_client = self.app.client_manager.volume + + if parsed_args.long: + columns = [ + "Binary", + "Host", + "Zone", + "Status", + "State", + "Updated At", + "Disabled Reason" + ] + else: + columns = [ + "Binary", + "Host", + "Zone", + "Status", + "State", + "Updated At" + ] + + data = service_client.services.list(parsed_args.host, + parsed_args.service) + return (columns, + (utils.get_item_properties( + s, columns, + ) for s in data)) diff --git a/openstackclient/volume/v2/snapshot.py b/openstackclient/volume/v2/snapshot.py index 4d00b72..db4ce6c 100644 --- a/openstackclient/volume/v2/snapshot.py +++ b/openstackclient/volume/v2/snapshot.py @@ -21,6 +21,7 @@ import six from openstackclient.common import command from openstackclient.common import parseractions from openstackclient.common import utils +from openstackclient.i18n import _ class CreateSnapshot(command.ShowOne): @@ -31,25 +32,25 @@ class CreateSnapshot(command.ShowOne): parser.add_argument( "volume", metavar="<volume>", - help="Volume to snapshot (name or ID)" + help=_("Volume to snapshot (name or ID)") ) parser.add_argument( "--name", metavar="<name>", - required=True, - help="Name of the snapshot" + help=("Name of the snapshot") ) parser.add_argument( "--description", metavar="<description>", - help="Description of the snapshot" + help=_("Description of the snapshot") ) parser.add_argument( "--force", dest="force", action="store_true", default=False, - help="Create a snapshot attached to an instance. Default is False" + help=_("Create a snapshot attached to an instance. " + "Default is False") ) return parser @@ -78,7 +79,7 @@ class DeleteSnapshot(command.Command): "snapshots", metavar="<snapshot>", nargs="+", - help="Snapshot(s) to delete (name or ID)" + help=_("Snapshot(s) to delete (name or ID)") ) return parser @@ -99,13 +100,13 @@ class ListSnapshot(command.Lister): '--all-projects', action='store_true', default=False, - help='Include all projects (admin only)', + help=_('Include all projects (admin only)'), ) parser.add_argument( '--long', action='store_true', default=False, - help='List additional fields in output', + help=_('List additional fields in output'), ) return parser @@ -164,21 +165,32 @@ class SetSnapshot(command.Command): parser.add_argument( 'snapshot', metavar='<snapshot>', - help='Snapshot to modify (name or ID)') + help=_('Snapshot to modify (name or ID)') + ) parser.add_argument( '--name', metavar='<name>', - help='New snapshot name') + help=_('New snapshot name') + ) parser.add_argument( '--description', metavar='<description>', - help='New snapshot description') + help=_('New snapshot description') + ) parser.add_argument( '--property', metavar='<key=value>', action=parseractions.KeyValueAction, - help='Property to add/change for this snapshot ' - '(repeat option to set multiple properties)', + help=_('Property to add/change for this snapshot ' + '(repeat option to set multiple properties)'), + ) + parser.add_argument( + '--state', + metavar='<state>', + choices=['available', 'error', 'creating', 'deleting', + 'error-deleting'], + help=_('New snapshot state. Valid values are available, ' + 'error, creating, deleting, and error-deleting.'), ) return parser @@ -193,13 +205,17 @@ class SetSnapshot(command.Command): if parsed_args.description: kwargs['description'] = parsed_args.description - if not kwargs and not parsed_args.property: - self.app.log.error("No changes requested\n") + if (not kwargs and not parsed_args.property and not + parsed_args.state): + self.app.log.error(_("No changes requested\n")) return if parsed_args.property: volume_client.volume_snapshots.set_metadata(snapshot.id, parsed_args.property) + if parsed_args.state: + volume_client.volume_snapshots.reset_state(snapshot.id, + parsed_args.state) volume_client.volume_snapshots.update(snapshot.id, **kwargs) @@ -211,7 +227,7 @@ class ShowSnapshot(command.ShowOne): parser.add_argument( "snapshot", metavar="<snapshot>", - help="Snapshot to display (name or ID)" + help=_("Snapshot to display (name or ID)") ) return parser @@ -233,15 +249,15 @@ class UnsetSnapshot(command.Command): parser.add_argument( 'snapshot', metavar='<snapshot>', - help='Snapshot to modify (name or ID)', + help=_('Snapshot to modify (name or ID)'), ) parser.add_argument( '--property', metavar='<key>', action='append', default=[], - help='Property to remove from snapshot ' - '(repeat to remove multiple values)', + help=_('Property to remove from snapshot ' + '(repeat option to remove multiple properties)'), ) return parser @@ -256,4 +272,4 @@ class UnsetSnapshot(command.Command): parsed_args.property, ) else: - self.app.log.error("No changes requested\n") + self.app.log.error(_("No changes requested\n")) diff --git a/openstackclient/volume/v2/volume.py b/openstackclient/volume/v2/volume.py index 8f2122e..0e07aa7 100644 --- a/openstackclient/volume/v2/volume.py +++ b/openstackclient/volume/v2/volume.py @@ -21,6 +21,7 @@ import six from openstackclient.common import command from openstackclient.common import parseractions from openstackclient.common import utils +from openstackclient.i18n import _ from openstackclient.identity import common as identity_common @@ -32,61 +33,61 @@ class CreateVolume(command.ShowOne): parser.add_argument( "name", metavar="<name>", - help="New volume name" + help=_("Volume name"), ) parser.add_argument( "--size", metavar="<size>", type=int, required=True, - help="New volume size in GB" + help=_("Volume size in GB"), + ) + parser.add_argument( + "--type", + metavar="<volume-type>", + help=_("Set the type of volume"), + ) + parser.add_argument( + "--image", + metavar="<image>", + help=_("Use <image> as source of volume (name or ID)"), ) parser.add_argument( "--snapshot", metavar="<snapshot>", - help="Use <snapshot> as source of new volume (name or ID)" + help=_("Use <snapshot> as source of volume (name or ID)"), ) parser.add_argument( - "--description", - metavar="<description>", - help="New volume description" + "--source", + metavar="<volume>", + help=_("Volume to clone (name or ID)"), ) parser.add_argument( - "--type", - metavar="<volume-type>", - help="Use <volume-type> as the new volume type", + "--description", + metavar="<description>", + help=_("Volume description"), ) parser.add_argument( '--user', metavar='<user>', - help='Specify an alternate user (name or ID)', + help=_('Specify an alternate user (name or ID)'), ) parser.add_argument( '--project', metavar='<project>', - help='Specify an alternate project (name or ID)', + help=_('Specify an alternate project (name or ID)'), ) parser.add_argument( "--availability-zone", metavar="<availability-zone>", - help="Create new volume in <availability_zone>" - ) - parser.add_argument( - "--image", - metavar="<image>", - help="Use <image> as source of new volume (name or ID)" - ) - parser.add_argument( - "--source", - metavar="<volume>", - help="Volume to clone (name or ID)" + help=_("Create volume in <availability-zone>"), ) parser.add_argument( "--property", metavar="<key=value>", action=parseractions.KeyValueAction, - help="Set a property to this volume " - "(repeat option to set multiple properties)" + help=_("Set a property to this volume " + "(repeat option to set multiple properties)"), ) return parser @@ -110,7 +111,7 @@ class CreateVolume(command.ShowOne): snapshot = None if parsed_args.snapshot: snapshot = utils.find_resource( - volume_client.snapshots, + volume_client.volume_snapshots, parsed_args.snapshot).id project = None @@ -158,15 +159,15 @@ class DeleteVolume(command.Command): "volumes", metavar="<volume>", nargs="+", - help="Volume(s) to delete (name or ID)" + help=_("Volume(s) to delete (name or ID)") ) parser.add_argument( "--force", dest="force", action="store_true", default=False, - help="Attempt forced removal of volume(s), regardless of state " - "(defaults to False)" + help=_("Attempt forced removal of volume(s), regardless of state " + "(defaults to False)") ) return parser @@ -188,37 +189,37 @@ class ListVolume(command.Lister): parser = super(ListVolume, self).get_parser(prog_name) parser.add_argument( '--project', - metavar='<project-id>', - help='Filter results by project (name or ID) (admin only)' + metavar='<project>', + help=_('Filter results by project (name or ID) (admin only)') ) identity_common.add_project_domain_option_to_parser(parser) parser.add_argument( '--user', - metavar='<user-id>', - help='Filter results by user (name or ID) (admin only)' + metavar='<user>', + help=_('Filter results by user (name or ID) (admin only)') ) identity_common.add_user_domain_option_to_parser(parser) parser.add_argument( '--name', metavar='<name>', - help='Filter results by volume name', + help=_('Filter results by volume name'), ) parser.add_argument( '--status', metavar='<status>', - help='Filter results by status', + help=_('Filter results by status'), ) parser.add_argument( '--all-projects', action='store_true', default=False, - help='Include all projects (admin only)', + help=_('Include all projects (admin only)'), ) parser.add_argument( '--long', action='store_true', default=False, - help='List additional fields in output', + help=_('List additional fields in output'), ) return parser @@ -320,30 +321,37 @@ class SetVolume(command.Command): parser.add_argument( 'volume', metavar='<volume>', - help='Volume to change (name or ID)', + help=_('Volume to modify (name or ID)'), ) parser.add_argument( '--name', metavar='<name>', - help='New volume name', - ) - parser.add_argument( - '--description', - metavar='<description>', - help='New volume description', + help=_('New volume name'), ) parser.add_argument( '--size', metavar='<size>', type=int, - help='Extend volume size in GB', + help=_('Extend volume size in GB'), + ) + parser.add_argument( + '--description', + metavar='<description>', + help=_('New volume description'), ) parser.add_argument( '--property', metavar='<key=value>', action=parseractions.KeyValueAction, - help='Property to add or modify for this volume ' - '(repeat option to set multiple properties)', + help=_('Set a property on this volume ' + '(repeat option to set multiple properties)'), + ) + parser.add_argument( + '--image-property', + metavar='<key=value>', + action=parseractions.KeyValueAction, + help=_('Set an image property on this volume ' + '(repeat option to set multiple image properties)'), ) return parser @@ -353,18 +361,21 @@ class SetVolume(command.Command): if parsed_args.size: if volume.status != 'available': - self.app.log.error("Volume is in %s state, it must be " - "available before size can be extended" % + self.app.log.error(_("Volume is in %s state, it must be " + "available before size can be extended") % volume.status) return if parsed_args.size <= volume.size: - self.app.log.error("New size must be greater than %s GB" % + self.app.log.error(_("New size must be greater than %s GB") % volume.size) return volume_client.volumes.extend(volume.id, parsed_args.size) if parsed_args.property: volume_client.volumes.set_metadata(volume.id, parsed_args.property) + if parsed_args.image_property: + volume_client.volumes.set_image_metadata( + volume.id, parsed_args.image_property) kwargs = {} if parsed_args.name: @@ -374,8 +385,9 @@ class SetVolume(command.Command): if kwargs: volume_client.volumes.update(volume.id, **kwargs) - if not kwargs and not parsed_args.property and not parsed_args.size: - self.app.log.error("No changes requested\n") + if (not kwargs and not parsed_args.property + and not parsed_args.image_property and not parsed_args.size): + self.app.log.error(_("No changes requested\n")) class ShowVolume(command.ShowOne): @@ -386,7 +398,7 @@ class ShowVolume(command.ShowOne): parser.add_argument( 'volume', metavar="<volume-id>", - help="Volume to display (name or ID)" + help=_("Volume to display (name or ID)") ) return parser @@ -417,16 +429,21 @@ class UnsetVolume(command.Command): parser.add_argument( 'volume', metavar='<volume>', - help='Volume to modify (name or ID)', + help=_('Volume to modify (name or ID)'), ) parser.add_argument( '--property', metavar='<key>', - required=True, action='append', - default=[], - help='Property to remove from volume ' - '(repeat option to remove multiple properties)', + help=_('Remove a property from volume ' + '(repeat option to remove multiple properties)'), + ) + parser.add_argument( + '--image-property', + metavar='<key>', + action='append', + help=_('Remove an image property from volume ' + '(repeat option to remove multiple image properties)'), ) return parser @@ -435,5 +452,12 @@ class UnsetVolume(command.Command): volume = utils.find_resource( volume_client.volumes, parsed_args.volume) - volume_client.volumes.delete_metadata( - volume.id, parsed_args.property) + if parsed_args.property: + volume_client.volumes.delete_metadata( + volume.id, parsed_args.property) + if parsed_args.image_property: + volume_client.volumes.delete_image_metadata( + volume.id, parsed_args.image_property) + + if (not parsed_args.image_property and not parsed_args.property): + self.app.log.error(_("No changes requested\n")) diff --git a/openstackclient/volume/v2/volume_type.py b/openstackclient/volume/v2/volume_type.py index d2b3ed6..adaccb0 100644 --- a/openstackclient/volume/v2/volume_type.py +++ b/openstackclient/volume/v2/volume_type.py @@ -17,8 +17,11 @@ import six from openstackclient.common import command +from openstackclient.common import exceptions from openstackclient.common import parseractions from openstackclient.common import utils +from openstackclient.i18n import _ +from openstackclient.identity import common as identity_common class CreateVolumeType(command.ShowOne): @@ -29,12 +32,12 @@ class CreateVolumeType(command.ShowOne): parser.add_argument( "name", metavar="<name>", - help="New volume type name" + help=_("Volume type name"), ) parser.add_argument( "--description", metavar="<description>", - help="New volume type description", + help=_("Volume type description"), ) public_group = parser.add_mutually_exclusive_group() public_group.add_argument( @@ -42,21 +45,21 @@ class CreateVolumeType(command.ShowOne): dest="public", action="store_true", default=False, - help="Volume type is accessible to the public", + help=_("Volume type is accessible to the public"), ) public_group.add_argument( "--private", dest="private", action="store_true", default=False, - help="Volume type is not accessible to the public", + help=_("Volume type is not accessible to the public"), ) parser.add_argument( '--property', metavar='<key=value>', action=parseractions.KeyValueAction, - help='Property to add for this volume type' - '(repeat option to set multiple properties)', + help=_('Set a property on this volume type ' + '(repeat option to set multiple properties)'), ) return parser @@ -91,7 +94,7 @@ class DeleteVolumeType(command.Command): parser.add_argument( "volume_type", metavar="<volume-type>", - help="Volume type to delete (name or ID)" + help=_("Volume type to delete (name or ID)") ) return parser @@ -111,7 +114,7 @@ class ListVolumeType(command.Lister): '--long', action='store_true', default=False, - help='List additional fields in output') + help=_('List additional fields in output')) return parser def take_action(self, parsed_args): @@ -137,38 +140,50 @@ class SetVolumeType(command.Command): parser.add_argument( 'volume_type', metavar='<volume-type>', - help='Volume type to modify (name or ID)', + help=_('Volume type to modify (name or ID)'), ) parser.add_argument( '--name', metavar='<name>', - help='Set volume type name', + help=_('Set volume type name'), ) parser.add_argument( '--description', metavar='<name>', - help='Set volume type description', + help=_('Set volume type description'), ) parser.add_argument( '--property', metavar='<key=value>', action=parseractions.KeyValueAction, - help='Property to add or modify for this volume type ' - '(repeat option to set multiple properties)', + help=_('Set a property on this volume type ' + '(repeat option to set multiple properties)'), ) + parser.add_argument( + '--project', + metavar='<project>', + help=_('Set volume type access to project (name or ID) ' + '(admin only)'), + ) + identity_common.add_project_domain_option_to_parser(parser) + return parser def take_action(self, parsed_args): volume_client = self.app.client_manager.volume + identity_client = self.app.client_manager.identity + volume_type = utils.find_resource( volume_client.volume_types, parsed_args.volume_type) if (not parsed_args.name and not parsed_args.description - and not parsed_args.property): - self.app.log.error("No changes requested\n") + and not parsed_args.property + and not parsed_args.project): + self.app.log.error(_("No changes requested\n")) return + result = 0 kwargs = {} if parsed_args.name: kwargs['name'] = parsed_args.name @@ -176,13 +191,42 @@ class SetVolumeType(command.Command): kwargs['description'] = parsed_args.description if kwargs: - volume_client.volume_types.update( - volume_type.id, - **kwargs - ) + try: + volume_client.volume_types.update( + volume_type.id, + **kwargs + ) + except Exception as e: + self.app.log.error(_("Failed to update volume type name or" + " description: %s") % str(e)) + result += 1 if parsed_args.property: - volume_type.set_keys(parsed_args.property) + try: + volume_type.set_keys(parsed_args.property) + except Exception as e: + self.app.log.error(_("Failed to set volume type property: ") + + str(e)) + result += 1 + + if parsed_args.project: + project_info = None + try: + project_info = identity_common.find_project( + identity_client, + parsed_args.project, + parsed_args.project_domain) + + volume_client.volume_type_access.add_project_access( + volume_type.id, project_info.id) + except Exception as e: + self.app.log.error(_("Failed to set volume type access to" + " project: %s") % str(e)) + result += 1 + + if result > 0: + raise exceptions.CommandError("Command Failed: One or more of the" + " operations failed") class ShowVolumeType(command.ShowOne): @@ -193,7 +237,7 @@ class ShowVolumeType(command.ShowOne): parser.add_argument( "volume_type", metavar="<volume-type>", - help="Volume type to display (name or ID)" + help=_("Volume type to display (name or ID)") ) return parser @@ -214,22 +258,62 @@ class UnsetVolumeType(command.Command): parser.add_argument( 'volume_type', metavar='<volume-type>', - help='Volume type to modify (name or ID)', + help=_('Volume type to modify (name or ID)'), ) parser.add_argument( '--property', metavar='<key>', - default=[], - required=True, - help='Property to remove from volume type ' - '(repeat option to remove multiple properties)', + help=_('Remove a property from this volume type ' + '(repeat option to remove multiple properties)'), + ) + parser.add_argument( + '--project', + metavar='<project>', + help=_('Removes volume type access to project (name or ID) ' + ' (admin only)'), ) + identity_common.add_project_domain_option_to_parser(parser) + return parser def take_action(self, parsed_args): volume_client = self.app.client_manager.volume + identity_client = self.app.client_manager.identity + volume_type = utils.find_resource( volume_client.volume_types, parsed_args.volume_type, ) - volume_type.unset_keys(parsed_args.property) + + if (not parsed_args.property + and not parsed_args.project): + self.app.log.error(_("No changes requested\n")) + return + + result = 0 + if parsed_args.property: + try: + volume_type.unset_keys(parsed_args.property) + except Exception as e: + self.app.log.error(_("Failed to unset volume type property: %s" + ) % str(e)) + result += 1 + + if parsed_args.project: + project_info = None + try: + project_info = identity_common.find_project( + identity_client, + parsed_args.project, + parsed_args.project_domain) + + volume_client.volume_type_access.remove_project_access( + volume_type.id, project_info.id) + except Exception as e: + self.app.log.error(_("Failed to remove volume type access from" + " project: ") + str(e)) + result += 1 + + if result > 0: + raise exceptions.CommandError("Command Failed: One or more of the" + " operations failed") diff --git a/releasenotes/notes/add-disable-reason-6e0f28459a09a60d.yaml b/releasenotes/notes/add-disable-reason-6e0f28459a09a60d.yaml new file mode 100644 index 0000000..7f9e441 --- /dev/null +++ b/releasenotes/notes/add-disable-reason-6e0f28459a09a60d.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Add ``--disable-reason`` option to the ``service set`` command diff --git a/releasenotes/notes/add-port-commands-a3580662721a6312.yaml b/releasenotes/notes/add-port-commands-a3580662721a6312.yaml new file mode 100644 index 0000000..4a6ea9b --- /dev/null +++ b/releasenotes/notes/add-port-commands-a3580662721a6312.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Add ``port create``, ``port list`` and ``port set`` commands + [Bug `1519909 <https://bugs.launchpad.net/python-openstackclient/+bug/1519909>`_] diff --git a/releasenotes/notes/add-quota-set-for-network-11fcd7b9e08624b5.yaml b/releasenotes/notes/add-quota-set-for-network-11fcd7b9e08624b5.yaml new file mode 100644 index 0000000..0f7dd55 --- /dev/null +++ b/releasenotes/notes/add-quota-set-for-network-11fcd7b9e08624b5.yaml @@ -0,0 +1,9 @@ +--- +features: + - | + Add network support for ``quota set`` command. Options added includes + ``--networks --subnets --subnetpools --ports --routers --rbac-policies`` + ``--vips --members --health-monitors``. + Options ``--floating-ips --secgroup-rules --secgroups`` now support + both network and compute API. + [Bug `1489441 <https://bugs.launchpad.net/bugs/1489441>`_] diff --git a/releasenotes/notes/add-restore-server-d8c73e0e83df17dd.yaml b/releasenotes/notes/add-restore-server-d8c73e0e83df17dd.yaml new file mode 100644 index 0000000..e6d2211 --- /dev/null +++ b/releasenotes/notes/add-restore-server-d8c73e0e83df17dd.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Add ``server restore`` command diff --git a/releasenotes/notes/bug-1519502-d534db6c18adef20.yaml b/releasenotes/notes/bug-1519502-d534db6c18adef20.yaml new file mode 100644 index 0000000..43317a0 --- /dev/null +++ b/releasenotes/notes/bug-1519502-d534db6c18adef20.yaml @@ -0,0 +1,4 @@ +--- +upgrade: + - The ``ip floating create`` command now uses Network v2 when enabled + [Bug `1519502 <https://bugs.launchpad.net/python-openstackclient/+bug/1519502>`_] diff --git a/releasenotes/notes/bug-1519502-f72236598d14d350.yaml b/releasenotes/notes/bug-1519502-f72236598d14d350.yaml index 73ead7f..1ace59b 100644 --- a/releasenotes/notes/bug-1519502-f72236598d14d350.yaml +++ b/releasenotes/notes/bug-1519502-f72236598d14d350.yaml @@ -6,3 +6,7 @@ features: [Bug `1519502 <https://bugs.launchpad.net/python-openstackclient/+bug/1519502>`_] - Add command ``ip floating show`` for neutron and nova network. [Bug `1519502 <https://bugs.launchpad.net/python-openstackclient/+bug/1519502>`_] +upgrade: + - Output of command ``ip floating list`` for nova network has been changed. + And it is different from the output of neutron network. + [Ref `<http://docs.openstack.org/developer/python-openstackclient/backwards-incompatible.html>`_] diff --git a/releasenotes/notes/bug-1519511-65b8901ae6ea2e63.yaml b/releasenotes/notes/bug-1519511-65b8901ae6ea2e63.yaml new file mode 100644 index 0000000..3122f83 --- /dev/null +++ b/releasenotes/notes/bug-1519511-65b8901ae6ea2e63.yaml @@ -0,0 +1,14 @@ +--- +features: + - The ``security group create``, ``security group set`` and + ``security group show`` commands now uses Network v2 when + enabled which results in a more detailed output for network + security group rules. + [Bug `1519511 <https://bugs.launchpad.net/bugs/1519511>`_] +fixes: + - The ``security group create`` command now uses Network v2 when + enabled which allows the security group description to be created + with an empty value. In addition, the ``tenant_id`` field changed + to ``project_id`` to match the ``security group show`` command + output. + [Bug `1519511 <https://bugs.launchpad.net/bugs/1519511>`_] diff --git a/releasenotes/notes/bug-1519511-65d8d21dde31e5e2.yaml b/releasenotes/notes/bug-1519511-65d8d21dde31e5e2.yaml new file mode 100644 index 0000000..8800db8 --- /dev/null +++ b/releasenotes/notes/bug-1519511-65d8d21dde31e5e2.yaml @@ -0,0 +1,5 @@ +--- +features: + - Add ``--project`` and ``--project-domain`` options to the + ``security group create`` command for Network v2. + [Bug `1519511 <https://bugs.launchpad.net/bugs/1519511>`_] diff --git a/releasenotes/notes/bug-1519511-74bab0e0d32db043.yaml b/releasenotes/notes/bug-1519511-74bab0e0d32db043.yaml index 1a70c79..ca66b64 100644 --- a/releasenotes/notes/bug-1519511-74bab0e0d32db043.yaml +++ b/releasenotes/notes/bug-1519511-74bab0e0d32db043.yaml @@ -1,7 +1,10 @@ --- fixes: - - | - Ignore the ``security group list`` command ``--all-projects`` option + - Ignore the ``security group list`` command ``--all-projects`` option for Network v2 since security groups will be displayed for all projects by default (admin only). [Bug `1519511 <https://bugs.launchpad.net/bugs/1519511>`_] + - The ``security group set`` command now uses Network v2 when + enabled which allows the security group name and description + to be set to an empty value. + [Bug `1519511 <https://bugs.launchpad.net/bugs/1519511>`_] diff --git a/releasenotes/notes/bug-1519512-4231ac6014109142.yaml b/releasenotes/notes/bug-1519512-4231ac6014109142.yaml new file mode 100644 index 0000000..c9beda4 --- /dev/null +++ b/releasenotes/notes/bug-1519512-4231ac6014109142.yaml @@ -0,0 +1,7 @@ +--- +upgrade: + - The ``security group rule create`` command now uses Network v2 + when enabled which results in a more detailed output for network + security group rules that matches the ``security group rule show`` + command. + [Bug `1519512 <https://bugs.launchpad.net/bugs/1519512>`_] diff --git a/releasenotes/notes/bug-1519512-48d98f09e44220a3.yaml b/releasenotes/notes/bug-1519512-48d98f09e44220a3.yaml new file mode 100644 index 0000000..0161b5c --- /dev/null +++ b/releasenotes/notes/bug-1519512-48d98f09e44220a3.yaml @@ -0,0 +1,7 @@ +--- +features: + - Add ``--ingress``, ``--egress``, ``--ethertype``, ``--project`` + and ``--project-domain`` options to the ``security group rule create`` + command for Network v2 only. These options enable ``egress`` and + ``IPv6`` security group rules along with setting the project. + [Bug `1519512 <https://bugs.launchpad.net/bugs/1519512>`_] diff --git a/releasenotes/notes/bug-1519512-65df002102b7fb99.yaml b/releasenotes/notes/bug-1519512-65df002102b7fb99.yaml new file mode 100644 index 0000000..87b14d6 --- /dev/null +++ b/releasenotes/notes/bug-1519512-65df002102b7fb99.yaml @@ -0,0 +1,16 @@ +--- +features: + - The ``security group rule list`` command now uses Network v2 + when enabled which results in ``egress`` security group rules + being displayed. The ``--long`` option was also added for + Network v2 to display direction and ethertype information. + In addition, security group rules for all projects will be + displayed when the ``group`` argument is not specified + (admin only). This is done by default when using Network v2, + but requires the new ``--all-projects`` option when using + Compute v2. + [Bug `1519512 <https://bugs.launchpad.net/bugs/1519512>`_] +fixes: + - The ``security group rule list`` command no longer ignores + the ``group`` argument when it is set to an empty value. + [Bug `1519512 <https://bugs.launchpad.net/bugs/1519512>`_] diff --git a/releasenotes/notes/bug-1519512-dbf4368fe10dc495.yaml b/releasenotes/notes/bug-1519512-dbf4368fe10dc495.yaml new file mode 100644 index 0000000..f8f2387 --- /dev/null +++ b/releasenotes/notes/bug-1519512-dbf4368fe10dc495.yaml @@ -0,0 +1,24 @@ +--- +features: + - Add ``--icmp-type`` and ``--icmp-code`` options to the + ``security group rule create`` command for Network v2 only. + These options can be used to set ICMP type and code for + ICMP IP protocols. + [Bug `1519512 <https://bugs.launchpad.net/bugs/1519512>`_] + - The following Network v2 IP protocols are supported by the + ``security group rule create`` command ``--protocol`` option, + ``ah``, ``dccp``, ``egp``, ``esp``, ``gre``, ``igmp``, + ``ipv6-encap``, ``ipv6-frag``, ``ipv6-icmp``, ``ipv6-nonxt``, + ``ipv6-opts``, ``ipv6-route``, ``ospf``, ``pgm``, ``rsvp``, ``sctp``, + ``udplite``, ``vrrp`` and integer representations [0-255]. + [Bug `1519512 <https://bugs.launchpad.net/bugs/1519512>`_] + - The ``security group rule list`` command supports displaying + the ICMP type and code for security group rules with the + ICMP IP protocols. + [Bug `1519512 <https://bugs.launchpad.net/bugs/1519512>`_] +upgrade: + - Changed the ``security group rule create`` command ``--proto`` + option to ``--protocol``. Using the ``--proto`` option is still + supported, but is no longer documented and may be deprecated in + a future release. + [Bug `1519512 <https://bugs.launchpad.net/bugs/1519512>`_] diff --git a/releasenotes/notes/bug-1535239-767e6cf1990eda01.yaml b/releasenotes/notes/bug-1535239-767e6cf1990eda01.yaml new file mode 100644 index 0000000..36f8e68 --- /dev/null +++ b/releasenotes/notes/bug-1535239-767e6cf1990eda01.yaml @@ -0,0 +1,6 @@ +--- +fixes: + - | + Support a new ``--state`` option for ``snapshot set`` command that + changes the state of a snapshot. + [Bug `1535239 <https://bugs.launchpad.net/bugs/1535239>`_] diff --git a/releasenotes/notes/bug-1536479-d1f03ed2177d06ed.yaml b/releasenotes/notes/bug-1536479-d1f03ed2177d06ed.yaml new file mode 100644 index 0000000..b9932ad --- /dev/null +++ b/releasenotes/notes/bug-1536479-d1f03ed2177d06ed.yaml @@ -0,0 +1,5 @@ +--- +fixes: + - | + ``--pool-prefix`` option made required for ``subnet pool create`` + [Bug `1536479 <https://bugs.launchpad.net/bugs/1536479>`_] diff --git a/releasenotes/notes/bug-1540656-f7b7b7e3feef2440.yaml b/releasenotes/notes/bug-1540656-f7b7b7e3feef2440.yaml new file mode 100644 index 0000000..4abb284 --- /dev/null +++ b/releasenotes/notes/bug-1540656-f7b7b7e3feef2440.yaml @@ -0,0 +1,5 @@ +--- +features: + - The ``security group rule create`` command now supports a security + group name for the ``--src-group`` option. + [Bug `1540656 <https://bugs.launchpad.net/bugs/1540656>`_] diff --git a/releasenotes/notes/bug-1542171-fde165df53216726.yaml b/releasenotes/notes/bug-1542171-fde165df53216726.yaml new file mode 100644 index 0000000..b0f3a25 --- /dev/null +++ b/releasenotes/notes/bug-1542171-fde165df53216726.yaml @@ -0,0 +1,7 @@ +--- +features: + - | + Add ``server group create``, ``server group delete``, + ``server group list``, ``server group show`` commands. + [Bug `1542171 <https://bugs.launchpad.net/python-openstackclient/+bug/1542171>`_] + [Blueprint `nova-server-group-support <https://blueprints.launchpad.net/python-openstackclient/+spec/nova-server-group-support>`_] diff --git a/releasenotes/notes/bug-1542364-5d1e93cfd24f0b65.yaml b/releasenotes/notes/bug-1542364-5d1e93cfd24f0b65.yaml new file mode 100644 index 0000000..0d61ba3 --- /dev/null +++ b/releasenotes/notes/bug-1542364-5d1e93cfd24f0b65.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Add ``subnet create`` command. + [Bug `1542364 <https://bugs.launchpad.net/bugs/1542364>`_] diff --git a/releasenotes/notes/bug-1544586-0e6ca9a09dac0726.yaml b/releasenotes/notes/bug-1544586-0e6ca9a09dac0726.yaml new file mode 100644 index 0000000..d1fc20c --- /dev/null +++ b/releasenotes/notes/bug-1544586-0e6ca9a09dac0726.yaml @@ -0,0 +1,5 @@ +--- +features: + - Add ``subnet pool create`` and ``subnet pool set`` commands. + [Bug `1544586 <https://bugs.launchpad.net/python-openstackclient/+bug/1544586>`_] + [Bug `1544591 <https://bugs.launchpad.net/python-openstackclient/+bug/1544591>`_]
\ No newline at end of file diff --git a/releasenotes/notes/bug-1544586-0fe19a567d3e31fc.yaml b/releasenotes/notes/bug-1544586-0fe19a567d3e31fc.yaml new file mode 100644 index 0000000..fb73325 --- /dev/null +++ b/releasenotes/notes/bug-1544586-0fe19a567d3e31fc.yaml @@ -0,0 +1,6 @@ +--- +features: + - Add ``--share`` and ``--default`` options to ``subnet pool create`` + and ``--default`` option to ``subnet pool set`` + [Bug `1544586 <https://bugs.launchpad.net/python-openstackclient/+bug/1544586>`_] + [Bug `1544591 <https://bugs.launchpad.net/python-openstackclient/+bug/1544591>`_] diff --git a/releasenotes/notes/bug-1545537-12bbf01d2280dd2f.yaml b/releasenotes/notes/bug-1545537-12bbf01d2280dd2f.yaml new file mode 100644 index 0000000..fa205a6 --- /dev/null +++ b/releasenotes/notes/bug-1545537-12bbf01d2280dd2f.yaml @@ -0,0 +1,8 @@ +--- +features: + - | + Add provider network options ``--provider-network-type``, + ``--provider-physical-network`` and ``--provider-segment`` + to the ``network create`` and ``network set`` commands. + These options are available for NetworkV2 only. + [Bug `1545537 <https://bugs.launchpad.net/bugs/1545537>`_]
\ No newline at end of file diff --git a/releasenotes/notes/bug-1545537-4fa72fbfbbe3f31e.yaml b/releasenotes/notes/bug-1545537-4fa72fbfbbe3f31e.yaml new file mode 100644 index 0000000..c38eac9 --- /dev/null +++ b/releasenotes/notes/bug-1545537-4fa72fbfbbe3f31e.yaml @@ -0,0 +1,8 @@ +--- +features: + - | + Add ``--transparent-vlan`` and ``--no-transparent-vlan`` options to + ``network create`` and ``network set`` commands to add/remove VLAN + transparency attributes from networks. + This option is available in Network V2 only. + [Bug `1545537 <https://bugs.launchpad.net/bugs/1545537>`_]
\ No newline at end of file diff --git a/releasenotes/notes/bug-1545537-7a66219d263bb1e5.yaml b/releasenotes/notes/bug-1545537-7a66219d263bb1e5.yaml new file mode 100644 index 0000000..30056eb --- /dev/null +++ b/releasenotes/notes/bug-1545537-7a66219d263bb1e5.yaml @@ -0,0 +1,8 @@ +--- +features: + - | + Add external network options ``--external|--internal`` and ``--external`` + suboptions ``--default|--no-default`` to the ``network create`` and + ``network set`` commands. + These options are available for Network version 2 only. + [Bug `1545537 <https://bugs.launchpad.net/bugs/1545537>`_]
\ No newline at end of file diff --git a/releasenotes/notes/bug-1545609-bdc1efc17214463b.yaml b/releasenotes/notes/bug-1545609-bdc1efc17214463b.yaml new file mode 100644 index 0000000..73ce8db --- /dev/null +++ b/releasenotes/notes/bug-1545609-bdc1efc17214463b.yaml @@ -0,0 +1,5 @@ +--- +fixes: + - | + Fixed ``openstack command list`` to display properly + [Bug `1545609 <https://bugs.launchpad.net/python-openstackclient/+bug/1545609>`_] diff --git a/releasenotes/notes/bug-1550999-5e352a71dfbc828d.yaml b/releasenotes/notes/bug-1550999-5e352a71dfbc828d.yaml new file mode 100644 index 0000000..168da1a --- /dev/null +++ b/releasenotes/notes/bug-1550999-5e352a71dfbc828d.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Adds ``volume service list`` command. + [Bug `1550999 <https://bugs.launchpad.net/python-openstackclient/+bug/1550999>`_] diff --git a/releasenotes/notes/bug-1554877-7f8479791eab45b7.yaml b/releasenotes/notes/bug-1554877-7f8479791eab45b7.yaml new file mode 100644 index 0000000..b8120fb --- /dev/null +++ b/releasenotes/notes/bug-1554877-7f8479791eab45b7.yaml @@ -0,0 +1,11 @@ +--- +features: + - | + Add ``--image-property`` option to ``volume set`` and ``volume unset`` commands + + Image properties are copied when a volume is created from an image. + The properties are immutable on the image itself but may be updated + or removed from the volume created from that image. + + [Bug `1554877 <https://bugs.launchpad.net/python-openstackclient/+bug/1554877>`_] + [Bug `1554879 <https://bugs.launchpad.net/python-openstackclient/+bug/1554879>`_] diff --git a/releasenotes/notes/bug-1554889-32ba8d4bfb0f5f3d.yaml b/releasenotes/notes/bug-1554889-32ba8d4bfb0f5f3d.yaml new file mode 100644 index 0000000..ab9ae81 --- /dev/null +++ b/releasenotes/notes/bug-1554889-32ba8d4bfb0f5f3d.yaml @@ -0,0 +1,12 @@ +--- +features: + - | + Add ``--project`` and ``--project-domain`` options to ``volume type set`` + and ``volume type unset`` commands + + Use the ``--project`` option to restrict a volume type to a specific project. + Volume types are public by default, restricted volume types should be made + private with the ``--private`` option to the ``volume create`` command. + + [Bug `1554889 <https://bugs.launchpad.net/python-openstackclient/+bug/1554889>`_] + [Bug `1554890 <https://bugs.launchpad.net/python-openstackclient/+bug/1554890>`_] diff --git a/releasenotes/notes/bug-1556719-d2dcf61acf87e856.yaml b/releasenotes/notes/bug-1556719-d2dcf61acf87e856.yaml new file mode 100644 index 0000000..7c8e5c0 --- /dev/null +++ b/releasenotes/notes/bug-1556719-d2dcf61acf87e856.yaml @@ -0,0 +1,6 @@ +--- +fixes: + - Command ``network delete`` will delete as many networks as possible, log + and report failures in the end. + [Bug `1556719 <https://bugs.launchpad.net/python-openstackclient/+bug/1556719>`_] + [Bug `1537856 <https://bugs.launchpad.net/python-openstackclient/+bug/1537856>`_] diff --git a/releasenotes/notes/bug-1556929-edd78cded88ecdc9.yaml b/releasenotes/notes/bug-1556929-edd78cded88ecdc9.yaml new file mode 100644 index 0000000..9cfdd48 --- /dev/null +++ b/releasenotes/notes/bug-1556929-edd78cded88ecdc9.yaml @@ -0,0 +1,4 @@ +--- +features: + - Add ``host set`` command + [Bug `1556929 <https://bugs.launchpad.net/python-openstackclient/+bug/1556929>`_] diff --git a/releasenotes/notes/bug-1559866-733988f5dd5b07bb.yaml b/releasenotes/notes/bug-1559866-733988f5dd5b07bb.yaml new file mode 100644 index 0000000..db30ed7 --- /dev/null +++ b/releasenotes/notes/bug-1559866-733988f5dd5b07bb.yaml @@ -0,0 +1,4 @@ +--- +features: + - Add ``aggregate unset`` command + [Bug `1559866 <https://bugs.launchpad.net/python-openstackclient/+bug/1559866>`_] diff --git a/releasenotes/notes/bug-1560157-bce572f58b43efa1.yaml b/releasenotes/notes/bug-1560157-bce572f58b43efa1.yaml new file mode 100644 index 0000000..e5c394b --- /dev/null +++ b/releasenotes/notes/bug-1560157-bce572f58b43efa1.yaml @@ -0,0 +1,7 @@ +--- +fixes: + - Fixed SSL/TLS verification for Network v2 commands. The commands + were ignoring the ``--insecure`` and ``--os-cacert`` options and + the ``OS_CACERT`` environment variable which caused them to fail + with ``An SSL error occurred.`` when authenticating using SSL/TLS. + [Bug `1560157 <https://bugs.launchpad.net/python-openstackclient/+bug/1560157>`_] diff --git a/releasenotes/notes/bug-1561838-3a006a8263d7536d.yaml b/releasenotes/notes/bug-1561838-3a006a8263d7536d.yaml new file mode 100644 index 0000000..71e5ba2 --- /dev/null +++ b/releasenotes/notes/bug-1561838-3a006a8263d7536d.yaml @@ -0,0 +1,6 @@ +--- +features: + - Support X.latest format for OS_COMPUTE_API_VERSION in order to talk with + the latest nova microversion API, that is very helpful shortcut usage to + use new nova side features. + [Bug `1561838 <https://bugs.launchpad.net/python-openstackclient/+bug/1561838>`_] diff --git a/releasenotes/notes/bug-1564460-ab7ad35c02392cb4.yaml b/releasenotes/notes/bug-1564460-ab7ad35c02392cb4.yaml new file mode 100644 index 0000000..54b9bdd --- /dev/null +++ b/releasenotes/notes/bug-1564460-ab7ad35c02392cb4.yaml @@ -0,0 +1,9 @@ +--- +fixes: + - Fixed the ``--route`` option on the ``router set`` command + which did not properly format the new routes to set resulting + in a ``Bad Request`` error. In addition, the ``router create``, + ``router list`` and ``router show`` command output for routes + was fixed to improve readability and to align with the + ``--route`` option on the ``router set`` command. + [Bug `1564460 <https://bugs.launchpad.net/bugs/1564460>`_] diff --git a/releasenotes/notes/bug-1565034-dd404bfb42d7778d.yaml b/releasenotes/notes/bug-1565034-dd404bfb42d7778d.yaml new file mode 100644 index 0000000..e5ff38e --- /dev/null +++ b/releasenotes/notes/bug-1565034-dd404bfb42d7778d.yaml @@ -0,0 +1,5 @@ +--- +fixes: + - Added ``--no-route`` to the ``router set`` command. + Deprecated ``--clear-routes``. + [Bug `1565034 <https://bugs.launchpad.net/bugs/1565034>`_] diff --git a/releasenotes/notes/bug-1565112-e0cea9bfbcab954f.yaml b/releasenotes/notes/bug-1565112-e0cea9bfbcab954f.yaml new file mode 100644 index 0000000..e813427 --- /dev/null +++ b/releasenotes/notes/bug-1565112-e0cea9bfbcab954f.yaml @@ -0,0 +1,6 @@ +--- +features: + - Add global options ``os-cert`` and ``--os-key`` to support client + certificate/key. Environment variables ``OS_CERT`` and ``OS_KEY``, as well + as the ``cert`` and ``key`` values in clouds.yaml may also be used + [Bug `1565112 <https://bugs.launchpad.net/bugs/1565112>`_] diff --git a/releasenotes/notes/bug-1566269-2572bca9157ca107.yaml b/releasenotes/notes/bug-1566269-2572bca9157ca107.yaml new file mode 100644 index 0000000..6354bbf --- /dev/null +++ b/releasenotes/notes/bug-1566269-2572bca9157ca107.yaml @@ -0,0 +1,5 @@ +--- +features: + - Add ``address scope create``, ``address scope delete``, ``address scope list``, + ``address scope set`` and ``address scope show`` commands. + [Bug `1566269 <https://bugs.launchpad.net/python-openstackclient/+bug/1566269>`_] diff --git a/releasenotes/notes/bug-1569480-c52e330548bfbd78.yaml b/releasenotes/notes/bug-1569480-c52e330548bfbd78.yaml new file mode 100644 index 0000000..ccec658 --- /dev/null +++ b/releasenotes/notes/bug-1569480-c52e330548bfbd78.yaml @@ -0,0 +1,7 @@ +--- +fixes: + - Fixed ``subnet pool list`` command to properly disply the + list of subnet pool prefixes in the ``Prefixes`` column. + This fix is consistent with the ``subnet pool create`` and + ``subnet pool show`` command output. + [Bug `1569480 <https://bugs.launchpad.net/bugs/1569480>`_] diff --git a/releasenotes/notes/bug-1571812-49cdce4df5f3d481.yaml b/releasenotes/notes/bug-1571812-49cdce4df5f3d481.yaml new file mode 100644 index 0000000..f331b4b --- /dev/null +++ b/releasenotes/notes/bug-1571812-49cdce4df5f3d481.yaml @@ -0,0 +1,10 @@ +--- +upgrade: + - | + Deprecate global option ``--profile`` in favor of ``--os-profile``. + + ``--profile`` interferes with existing command options with the same name. + Unfortunately it appeared in a release so we must follow the deprecation + process and wait one year (April 2017) before removing it. + + [Bug `1571812 <https://bugs.launchpad.net/python-openstackclient/+bug/1571812>`_] diff --git a/releasenotes/notes/bug-1572228-03638a7adec5da8b.yaml b/releasenotes/notes/bug-1572228-03638a7adec5da8b.yaml new file mode 100644 index 0000000..9db0ac0 --- /dev/null +++ b/releasenotes/notes/bug-1572228-03638a7adec5da8b.yaml @@ -0,0 +1,6 @@ +--- +fixes: + - Fixed ``network create``, ``network show`` and ``network list`` + commands to correctly display the router type in the + ``router:external`` and ``Router Type`` columns. + [Bug `1572228 <https://bugs.launchpad.net/bugs/1572228>`_] diff --git a/releasenotes/notes/bug-1572733-874b37a7fa8292d0.yaml b/releasenotes/notes/bug-1572733-874b37a7fa8292d0.yaml new file mode 100644 index 0000000..3ddf493 --- /dev/null +++ b/releasenotes/notes/bug-1572733-874b37a7fa8292d0.yaml @@ -0,0 +1,6 @@ +--- +fixes: + - The ``quota show`` command ``<project/class>`` argument is now + optional. If not specified, the user's current project is used. + This allows non-admin users to show quotas for their current project. + [Bug `1572733 <https://bugs.launchpad.net/bugs/1572733>`_] diff --git a/releasenotes/notes/bug-1575478-5a0a923c3a32f96a.yaml b/releasenotes/notes/bug-1575478-5a0a923c3a32f96a.yaml new file mode 100644 index 0000000..b043a0e --- /dev/null +++ b/releasenotes/notes/bug-1575478-5a0a923c3a32f96a.yaml @@ -0,0 +1,5 @@ +--- +fixes: + - Fixed ``flavor show/delete/set/unset`` command to properly + find a private flavor by flavor name. + [Bug `1575478 <https://bugs.launchpad.net/bugs/1575478>`_] diff --git a/releasenotes/notes/bug-1575624-87957ff60ad661a6.yaml b/releasenotes/notes/bug-1575624-87957ff60ad661a6.yaml new file mode 100644 index 0000000..9500220 --- /dev/null +++ b/releasenotes/notes/bug-1575624-87957ff60ad661a6.yaml @@ -0,0 +1,5 @@ +--- +fixes: + - Fixed ``flavor set/unset`` command to properly find a + flavor to be set/unset by flavor id. + [Bug `1575624 <https://bugs.launchpad.net/bugs/1575624>`_] diff --git a/releasenotes/notes/bug-1581179-4d15dc504777f9e7.yaml b/releasenotes/notes/bug-1581179-4d15dc504777f9e7.yaml new file mode 100644 index 0000000..53a5284 --- /dev/null +++ b/releasenotes/notes/bug-1581179-4d15dc504777f9e7.yaml @@ -0,0 +1,6 @@ +--- +features: + - + Add the ``--ip-version`` option to the ``subnet list`` command. This + will output subnets based on IP version filter. + [`Bug 1581179 <https://bugs.launchpad.net/python-openstackclient/+bug/1581179>`_] diff --git a/releasenotes/notes/make-snapshot-and-backup-name-optional-01971d33640ef1c8.yaml b/releasenotes/notes/make-snapshot-and-backup-name-optional-01971d33640ef1c8.yaml new file mode 100644 index 0000000..8ecbe6b --- /dev/null +++ b/releasenotes/notes/make-snapshot-and-backup-name-optional-01971d33640ef1c8.yaml @@ -0,0 +1,4 @@ +--- +fixes: + - Make ``--name`` optional in ``volume snapshot create`` and + ``volume backup create`` commands. diff --git a/releasenotes/notes/router-port-add-0afe7392c080bcb8.yaml b/releasenotes/notes/router-port-add-0afe7392c080bcb8.yaml new file mode 100644 index 0000000..1e2ee7e --- /dev/null +++ b/releasenotes/notes/router-port-add-0afe7392c080bcb8.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Add ``router add port`` command + [Bug `1546849 <https://bugs.launchpad.net/bugs/1546849>`_] diff --git a/releasenotes/notes/router-remove-port-058078c93819b0f4.yaml b/releasenotes/notes/router-remove-port-058078c93819b0f4.yaml new file mode 100644 index 0000000..187026a --- /dev/null +++ b/releasenotes/notes/router-remove-port-058078c93819b0f4.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Add ``router remove port`` command + [Bug `1546849 <https://bugs.launchpad.net/bugs/1546849>`_] diff --git a/releasenotes/notes/router-subnet-469d095ae0bac884.yaml b/releasenotes/notes/router-subnet-469d095ae0bac884.yaml new file mode 100644 index 0000000..db94b64 --- /dev/null +++ b/releasenotes/notes/router-subnet-469d095ae0bac884.yaml @@ -0,0 +1,8 @@ +--- +features: + - | + Add ``router add subnet`` command + [Bug `1546849 <https://bugs.launchpad.net/bugs/1546849>`_] + - | + Add ``router remove subnet`` command + [Bug `1546849 <https://bugs.launchpad.net/bugs/1546849>`_] diff --git a/releasenotes/notes/subnet-set-bbc26ecc16929302.yaml b/releasenotes/notes/subnet-set-bbc26ecc16929302.yaml new file mode 100644 index 0000000..8fb0c32 --- /dev/null +++ b/releasenotes/notes/subnet-set-bbc26ecc16929302.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Add ``subnet set`` command. + [Bug `1542363 <https://bugs.launchpad.net/bugs/1542363>`_] diff --git a/releasenotes/source/index.rst b/releasenotes/source/index.rst index c89ed74..e3af0e6 100644 --- a/releasenotes/source/index.rst +++ b/releasenotes/source/index.rst @@ -6,3 +6,4 @@ OpenStackClient Release Notes :maxdepth: 1 unreleased + mitaka diff --git a/releasenotes/source/mitaka.rst b/releasenotes/source/mitaka.rst new file mode 100644 index 0000000..e545609 --- /dev/null +++ b/releasenotes/source/mitaka.rst @@ -0,0 +1,6 @@ +=================================== + Mitaka Series Release Notes +=================================== + +.. release-notes:: + :branch: origin/stable/mitaka diff --git a/requirements.txt b/requirements.txt index 7349d9f..62bcfe3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,17 +4,17 @@ pbr>=1.6 # Apache-2.0 six>=1.9.0 # MIT -Babel>=1.3 # BSD +Babel>=2.3.4 # BSD cliff!=1.16.0,!=1.17.0,>=1.15.0 # Apache-2.0 keystoneauth1>=2.1.0 # Apache-2.0 -openstacksdk>=0.8.1 # Apache-2.0 +openstacksdk>=0.8.6 # Apache-2.0 os-client-config>=1.13.1 # Apache-2.0 -oslo.config>=3.7.0 # Apache-2.0 +oslo.config>=3.9.0 # Apache-2.0 oslo.i18n>=2.1.0 # Apache-2.0 oslo.utils>=3.5.0 # Apache-2.0 python-glanceclient>=2.0.0 # Apache-2.0 -python-keystoneclient!=1.8.0,!=2.1.0,>=1.6.0 # Apache-2.0 +python-keystoneclient!=1.8.0,!=2.1.0,>=1.7.0 # Apache-2.0 python-novaclient!=2.33.0,>=2.29.0 # Apache-2.0 -python-cinderclient>=1.3.1 # Apache-2.0 -requests!=2.9.0,>=2.8.1 # Apache-2.0 -stevedore>=1.5.0 # Apache-2.0 +python-cinderclient!=1.7.0,>=1.6.0 # Apache-2.0 +requests>=2.10.0 # Apache-2.0 +stevedore>=1.10.0 # Apache-2.0 @@ -63,6 +63,7 @@ openstack.compute.v2 = aggregate_remove_host = openstackclient.compute.v2.aggregate:RemoveAggregateHost aggregate_set = openstackclient.compute.v2.aggregate:SetAggregate aggregate_show = openstackclient.compute.v2.aggregate:ShowAggregate + aggregate_unset = openstackclient.compute.v2.aggregate:UnsetAggregate compute_service_delete = openstackclient.compute.v2.service:DeleteService compute_service_list = openstackclient.compute.v2.service:ListService @@ -79,6 +80,7 @@ openstack.compute.v2 = flavor_unset = openstackclient.compute.v2.flavor:UnsetFlavor host_list = openstackclient.compute.v2.host:ListHost + host_set = openstackclient.compute.v2.host:SetHost host_show = openstackclient.compute.v2.host:ShowHost hypervisor_list = openstackclient.compute.v2.hypervisor:ListHypervisor @@ -90,7 +92,6 @@ openstack.compute.v2 = ip_fixed_remove = openstackclient.compute.v2.fixedip:RemoveFixedIP ip_floating_add = openstackclient.compute.v2.floatingip:AddFloatingIP - ip_floating_create = openstackclient.compute.v2.floatingip:CreateFloatingIP ip_floating_remove = openstackclient.compute.v2.floatingip:RemoveFloatingIP ip_floating_pool_list = openstackclient.compute.v2.floatingippool:ListFloatingIPPool @@ -99,12 +100,6 @@ openstack.compute.v2 = keypair_list = openstackclient.compute.v2.keypair:ListKeypair keypair_show = openstackclient.compute.v2.keypair:ShowKeypair - security_group_create = openstackclient.compute.v2.security_group:CreateSecurityGroup - security_group_set = openstackclient.compute.v2.security_group:SetSecurityGroup - security_group_show = openstackclient.compute.v2.security_group:ShowSecurityGroup - security_group_rule_create = openstackclient.compute.v2.security_group:CreateSecurityGroupRule - security_group_rule_list = openstackclient.compute.v2.security_group:ListSecurityGroupRule - server_add_security_group = openstackclient.compute.v2.server:AddServerSecurityGroup server_add_volume = openstackclient.compute.v2.server:AddServerVolume server_create = openstackclient.compute.v2.server:CreateServer @@ -120,6 +115,7 @@ openstack.compute.v2 = server_remove_volume = openstackclient.compute.v2.server:RemoveServerVolume server_rescue = openstackclient.compute.v2.server:RescueServer server_resize = openstackclient.compute.v2.server:ResizeServer + server_restore = openstackclient.compute.v2.server:RestoreServer server_resume = openstackclient.compute.v2.server:ResumeServer server_set = openstackclient.compute.v2.server:SetServer server_shelve = openstackclient.compute.v2.server:ShelveServer @@ -135,6 +131,11 @@ openstack.compute.v2 = server_unset = openstackclient.compute.v2.server:UnsetServer server_unshelve = openstackclient.compute.v2.server:UnshelveServer + server_group_create = openstackclient.compute.v2.server_group:CreateServerGroup + server_group_delete = openstackclient.compute.v2.server_group:DeleteServerGroup + server_group_list = openstackclient.compute.v2.server_group:ListServerGroup + server_group_show = openstackclient.compute.v2.server_group:ShowServerGroup + usage_list = openstackclient.compute.v2.usage:ListUsage usage_show = openstackclient.compute.v2.usage:ShowUsage @@ -323,30 +324,60 @@ openstack.image.v2 = image_set = openstackclient.image.v2.image:SetImage openstack.network.v2 = + address_scope_create = openstackclient.network.v2.address_scope:CreateAddressScope + address_scope_delete = openstackclient.network.v2.address_scope:DeleteAddressScope + address_scope_list = openstackclient.network.v2.address_scope:ListAddressScope + address_scope_set = openstackclient.network.v2.address_scope:SetAddressScope + address_scope_show = openstackclient.network.v2.address_scope:ShowAddressScope + + ip_floating_create = openstackclient.network.v2.floating_ip:CreateFloatingIP ip_floating_delete = openstackclient.network.v2.floating_ip:DeleteFloatingIP ip_floating_list = openstackclient.network.v2.floating_ip:ListFloatingIP ip_floating_show = openstackclient.network.v2.floating_ip:ShowFloatingIP + network_create = openstackclient.network.v2.network:CreateNetwork network_delete = openstackclient.network.v2.network:DeleteNetwork network_list = openstackclient.network.v2.network:ListNetwork network_set = openstackclient.network.v2.network:SetNetwork network_show = openstackclient.network.v2.network:ShowNetwork + + port_create = openstackclient.network.v2.port:CreatePort port_delete = openstackclient.network.v2.port:DeletePort + port_list = openstackclient.network.v2.port:ListPort + port_set = openstackclient.network.v2.port:SetPort port_show = openstackclient.network.v2.port:ShowPort + + router_add_port = openstackclient.network.v2.router:AddPortToRouter + router_add_subnet = openstackclient.network.v2.router:AddSubnetToRouter router_create = openstackclient.network.v2.router:CreateRouter router_delete = openstackclient.network.v2.router:DeleteRouter router_list = openstackclient.network.v2.router:ListRouter + router_remove_port = openstackclient.network.v2.router:RemovePortFromRouter + router_remove_subnet = openstackclient.network.v2.router:RemoveSubnetFromRouter router_set = openstackclient.network.v2.router:SetRouter router_show = openstackclient.network.v2.router:ShowRouter + + security_group_create = openstackclient.network.v2.security_group:CreateSecurityGroup security_group_delete = openstackclient.network.v2.security_group:DeleteSecurityGroup security_group_list = openstackclient.network.v2.security_group:ListSecurityGroup + security_group_set = openstackclient.network.v2.security_group:SetSecurityGroup + security_group_show = openstackclient.network.v2.security_group:ShowSecurityGroup + + security_group_rule_create = openstackclient.network.v2.security_group_rule:CreateSecurityGroupRule security_group_rule_delete = openstackclient.network.v2.security_group_rule:DeleteSecurityGroupRule + security_group_rule_list = openstackclient.network.v2.security_group_rule:ListSecurityGroupRule security_group_rule_show = openstackclient.network.v2.security_group_rule:ShowSecurityGroupRule + + subnet_create = openstackclient.network.v2.subnet:CreateSubnet subnet_delete = openstackclient.network.v2.subnet:DeleteSubnet subnet_list = openstackclient.network.v2.subnet:ListSubnet + subnet_set = openstackclient.network.v2.subnet:SetSubnet subnet_show = openstackclient.network.v2.subnet:ShowSubnet + + subnet_pool_create = openstackclient.network.v2.subnet_pool:CreateSubnetPool subnet_pool_delete = openstackclient.network.v2.subnet_pool:DeleteSubnetPool subnet_pool_list = openstackclient.network.v2.subnet_pool:ListSubnetPool + subnet_pool_set = openstackclient.network.v2.subnet_pool:SetSubnetPool subnet_pool_show = openstackclient.network.v2.subnet_pool:ShowSubnetPool openstack.object_store.v1 = @@ -405,6 +436,8 @@ openstack.volume.v1 = volume_qos_show = openstackclient.volume.v1.qos_specs:ShowQos volume_qos_unset = openstackclient.volume.v1.qos_specs:UnsetQos + volume_service_list = openstackclient.volume.v1.service:ListService + openstack.volume.v2 = backup_create = openstackclient.volume.v2.backup:CreateBackup backup_delete = openstackclient.volume.v2.backup:DeleteBackup @@ -442,6 +475,8 @@ openstack.volume.v2 = volume_qos_show = openstackclient.volume.v2.qos_specs:ShowQos volume_qos_unset = openstackclient.volume.v2.qos_specs:UnsetQos + volume_service_list = openstackclient.volume.v2.service:ListService + [build_sphinx] source-dir = doc/source build-dir = doc/build diff --git a/test-requirements.txt b/test-requirements.txt index 20a2d5a..ffc8326 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -5,25 +5,30 @@ hacking<0.11,>=0.10.0 coverage>=3.6 # Apache-2.0 discover # BSD -fixtures>=1.3.1 # Apache-2.0/BSD +fixtures<2.0,>=1.3.1 # Apache-2.0/BSD mock>=1.2 # BSD oslosphinx!=3.4.0,>=2.5.0 # Apache-2.0 oslotest>=1.10.0 # Apache-2.0 -reno>=0.1.1 # Apache2 +reno>=1.6.2 # Apache2 requests-mock>=0.7.0 # Apache-2.0 sphinx!=1.2.0,!=1.3b1,<1.3,>=1.1.2 # BSD os-testr>=0.4.1 # Apache-2.0 testrepository>=0.0.18 # Apache-2.0/BSD testtools>=1.4.0 # MIT -tempest-lib>=0.14.0 # Apache-2.0 -osprofiler>=1.1.0 # Apache-2.0 +tempest>=11.0.0 # Apache-2.0 +osprofiler>=1.3.0 # Apache-2.0 +bandit>=1.0.1 # Apache-2.0 # Install these to generate sphinx autodocs -python-barbicanclient>=3.3.0 # Apache-2.0 +python-barbicanclient>=4.0.0 # Apache-2.0 python-congressclient<2000,>=1.0.0 # Apache-2.0 python-designateclient>=1.5.0 # Apache-2.0 -python-heatclient>=0.6.0 # Apache-2.0 +python-heatclient>=1.1.0 # Apache-2.0 python-ironicclient>=1.1.0 # Apache-2.0 +python-ironic-inspector-client>=1.5.0 # Apache-2.0 python-mistralclient>=1.0.0 # Apache-2.0 +python-muranoclient>=0.8.2 # Apache-2.0 python-saharaclient>=0.13.0 # Apache-2.0 -python-zaqarclient>=0.3.0 # Apache-2.0 +python-searchlightclient>=0.2.0 #Apache-2.0 +python-senlinclient>=0.3.0 # Apache-2.0 +python-zaqarclient>=1.0.0 # Apache-2.0 diff --git a/tools/fast8.sh b/tools/fast8.sh new file mode 100755 index 0000000..2b3e22a --- /dev/null +++ b/tools/fast8.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +cd $(dirname "$0")/.. +CHANGED=$(git diff --name-only HEAD~1 | tr '\n' ' ') + +# Skip files that don't exist +# (have been git rm'd) +CHECK="" +for FILE in $CHANGED; do + if [ -f "$FILE" ]; then + CHECK="$CHECK $FILE" + fi +done + +diff -u --from-file /dev/null $CHECK | flake8 --diff @@ -11,8 +11,43 @@ deps = -r{toxinidir}/test-requirements.txt commands = ostestr {posargs} whitelist_externals = ostestr +[testenv:fast8] +# Use same environment directory as pep8 env to save space and install time +envdir = {toxworkdir}/pep8 +commands = + {toxinidir}/tools/fast8.sh + [testenv:pep8] -commands = flake8 +commands = + flake8 + bandit -r openstackclient -x tests -s B105,B106,B107,B401,B404,B603,B606,B607,B110,B605,B101 + +[testenv:bandit] +# This command runs the bandit security linter against the openstackclient +# codebase minus the tests directory. Some tests are being excluded to +# reduce the number of positives before a team inspection, and to ensure a +# passing gate job for initial addition. The excluded tests are: +# B105-B107: hardcoded password checks - likely to generate false positives +# in a gate environment +# B401: import subprocess - not necessarily a security issue; this plugin is +# mainly used for penetration testing workflow +# B603,B606: process without shell - not necessarily a security issue; this +# plugin is mainly used for penetration testing workflow +# B607: start process with a partial path - this should be a project level +# decision +# NOTE(elmiko): The following tests are being excluded specifically for +# python-openstackclient, they are being excluded to ensure that voting jobs +# in the project and in bandit integration tests continue to pass. These +# tests have generated issue within the project and should be investigated +# by the project. +# B110: try, except, pass detected - possible security issue; this should be +# investigated by the project for possible exploitation +# B605: process with a shell - possible security issue; this should be +# investigated by the project for possible exploitation +# B101: use of assert - this code will be removed when compiling to optimized +# byte code +commands = + bandit -r openstackclient -x tests -s B105,B106,B107,B401,B404,B603,B606,B607,B110,B605,B101 [testenv:functional] setenv = OS_TEST_PATH=./functional/tests @@ -40,4 +75,4 @@ show-source = True exclude = .git,.tox,dist,doc,*openstack/common*,*lib/python*,*egg,build,tools # If 'ignore' is not set there are default errors and warnings that are set # Doc: http://flake8.readthedocs.org/en/latest/config.html#default -ignore = __
\ No newline at end of file +ignore = __ |
