Compare commits

...

343 commits

Author SHA1 Message Date
Andriy Kurilin
0a93733293 Fix quota usage and reservation display
Fix `openstack quota show --usage` to correctly display resource usage
and reservations by applying proper name normalization for corresponding
sections of data.
Previously, name normalization was applied only for "limits" which is
the root section, leaving 'usage' and 'reservation' sections untouched.

Change-Id: Id14fe894b30a74b9b8d78b00c3d4ff151f8b4210
Closes-bug: #2137636
Signed-off-by: Andriy Kurilin <andr.kurilin@gmail.com>
2026-01-07 13:29:49 +01:00
Zuul
d1a0ede7db Merge "zuul: Make openstackclient-check-plugins voting" 2025-12-19 19:38:24 +00:00
Abhishek Kekane
ed2dc692dd Fix image owner change when accepting membership with --project
When using 'openstack image set --project <project> --accept <image>',
the command incorrectly changed the image owner. The --project parameter
when used with membership flags should only identify which member's
status to update, not change ownership.

Closes-Bug: #2136795
Change-Id: I1044b51f38000fb5339740bc40c7f8645c794402
Signed-off-by: Abhishek Kekane <akekane@redhat.com>
2025-12-18 14:01:45 +00:00
Zuul
85fccc7a91 Merge "typing: Fixups for typed osc-lib" 2025-12-16 18:13:51 +00:00
Zuul
08b1bb70dd Merge "taas: Use custom command classes" 2025-12-16 18:13:48 +00:00
Zuul
58210a141a Merge "fix(keystone): correct the args submitted on user creation" 2025-12-15 19:56:34 +00:00
Doug Goldstein
0b05fd8968
fix(keystone): correct the args submitted on user creation
When a user is created without a password then no parameter called
'password' should be submitted to the keystone API. This removes the
incorrect 'password' with null being supplied.

Closes-Bug: 2136148
Change-Id: If1c2eb5db360764a5f7660ce4e5353da85b6d3da
Signed-off-by: Doug Goldstein <cardoe@cardoe.com>
2025-12-15 10:14:41 -06:00
Stephen Finucane
e8ae075c38 typing: Fixups for typed osc-lib
Change-Id: I436983a13e8812d704af2f1eb3f600277ef8a531
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
2025-12-15 11:38:36 +00:00
Stephen Finucane
7246a07834 taas: Use custom command classes
In change I53d9058273748ecd4d4eecec5f7291d5f38ce5ab we added custom
Command classes for typing purposes. However, the Tap-as-a-Service code
merged around the same time and was missed. Correct this.

Change-Id: I3a9fe20b5b8eb54708644527538f27396f29b476
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
2025-12-15 11:27:56 +00:00
Stephen Finucane
8dbb7126c6 identity: Use plural dest for append opts
Change-Id: I73a263a309e022b7606ced43a814a1d1914bc751
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
2025-12-15 11:25:43 +00:00
Zuul
43ffea5c4d Merge "Remove functional testenv for py38/py39" 2025-12-12 17:59:10 +00:00
Koya Watanabe
f2f0f92d41 Remove functional testenv for py38/py39
Python 3.8 and 3.9 are no longer supported.
Refer to pyproject.toml for the current supported versions.

Change-Id: Ie7f917c26299509050294037cc27e1fd9c20e78b
Signed-off-by: Koya Watanabe <koywatan@lycorp.co.jp>
2025-12-12 14:49:33 +00:00
Stephen Finucane
748cff5914 zuul: Make openstackclient-check-plugins voting
This will ensure we do not forget to ignore a module when migrating
plugins in-tree.

Change-Id: Id4dd657746f7c5f8ebf5ef55964593123303b996
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
Depends-on: https://review.opendev.org/c/openstack/openstackclient/+/970618
2025-12-12 11:39:24 +00:00
Stephen Finucane
841d95b095 common: Remove references to pkg_resources
Even though the comment here attributed this to stevedore, it was in
fact the use of pkg_resources that changed things.

Change-Id: I35377dd7d773024aa6423b72b1412e11b1b6f2e4
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
2025-12-12 10:32:12 +00:00
Stephen Finucane
a7e2f31ecc volume: Remove negotiation for v1 API
Change Ibe1cd6461d2cb78826467078aa17272f171746aa removed support for the
v1 volume API. We should have removed this check at the same time.

We also remove some god-awful monkey patching that references v1
cinderclient but in practice modified all clients.

Change-Id: I3727fd9238df966b3bc59812c5efcf3398da5c72
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
2025-12-12 10:32:12 +00:00
Zuul
de4e119272 Merge "typing: Add types to custom formatters" 2025-12-11 21:56:06 +00:00
Zuul
30f3192b8d Merge "Add custom command classes" 2025-12-11 21:42:13 +00:00
Zuul
cc36d929b3 Merge "clientmanager: Remove legacy cruft" 2025-12-11 21:32:52 +00:00
Stephen Finucane
e799a4a676 typing: Add types to custom formatters
We make a lot of use of typing.Any just to get this over the line. We
can come back to this later.

Change-Id: I03c18b0b44f210b2ad3e4012344d521fb85cae97
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
2025-12-11 18:55:15 +00:00
Stephen Finucane
3cd544df53 Add custom command classes
These are effectively identical to the osc-lib variants except they
include the attributes that the OSC shell implementation will set on
this during shell init. This helps from a typing perspective.

Change-Id: I53d9058273748ecd4d4eecec5f7291d5f38ce5ab
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
2025-12-11 18:55:13 +00:00
Zuul
c59bf71fb7 Merge "Use openstacksdk test generate_fake_resources factory" 2025-12-11 15:50:51 +00:00
Zuul
924822507a Merge "Implement conflict resolution" 2025-12-11 15:50:49 +00:00
Zuul
65801e7e58 Merge "Improve help strings for tap services" 2025-12-11 15:03:09 +00:00
Zuul
f2810a83b0 Merge "Moving tapas osc client code from neutronclient" 2025-12-11 15:03:06 +00:00
Stephen Finucane
3fbe41cd52 clientmanager: Remove legacy cruft
No has used Initialize functions in years, while the _auth_required
attribute has long since been handled by the base class in osc-lib.

Change-Id: I3af9a6d8c339b2170a13346b009392d51e044443
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
2025-12-11 13:31:26 +00:00
Miro Tomaska
dedc1a342c Use openstacksdk test generate_fake_resources factory
Instead of building fake test objects in the local fakes.py file,
use existing generate_fake_resource(s) factory methods to automatically
populate class attributes. Doing this ensures that fake objects are always
build with actual attributes of the class.

Change-Id: If424b87c79e7dab102cbd8a7938df85411c9465d
Signed-off-by: Miro Tomaska <mtomaska@redhat.com>
2025-12-11 13:28:38 +00:00
Stephen Finucane
060299c749 Implement conflict resolution
Take advantage of functionality recently introduced in cliff to allow us
to prefer commands that are in-tree over those provided via plugin
packages. This will allow us to move the extensions themselves in-tree.

Change-Id: I5dd9bc9743bea779ea1b4a71264c9a77c80033b3
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
2025-12-11 13:17:24 +00:00
Takashi Kajinami
92a277ff4c ruff: Enable E5 check
... to enforce maximum line length, to keep consistent code format.

Note that E501 check is disabled in test code now, until we decide how
to update ~50 lines violating the limit due to too long names.

Change-Id: I122c8b9035d6381dafb34438517c26b01e5201f5
Signed-off-by: Takashi Kajinami <kajinamit@oss.nttdata.com>
2025-12-11 02:43:20 +09:00
Miro Tomaska
9e49047ed1 Improve help strings for tap services
This is a follow up patch to feedback from[1]

[1] https://review.opendev.org/c/openstack/python-openstackclient/+/963445/comment/8f9576d4_938391ea/

Change-Id: I1c1ee68b37ef4c87c13d18e773c19b4ca5814ead
Signed-off-by: Miro Tomaska <mtomaska@redhat.com>
2025-12-08 19:00:45 +00:00
Miro Tomaska
97c2238df1 Moving tapas osc client code from neutronclient
Proposal to move all stadium projects from neutronclient
to openstackclient repo. Tap-as-a-service is the first example.
The tapas osc client code was recently moved to neutronclient see
https://review.opendev.org/c/openstack/tap-as-a-service/+/960849
but proposal is to make openstackclient its final destination.

This change also includes automatic lint fixes required in
this repo.

Change-Id: Ied47f40c6947600d40bf675ec06f0bf88fd15f1f
Signed-off-by: Miro Tomaska <mtomaska@redhat.com>
2025-12-08 13:22:12 -05:00
Zuul
f8effe997e Merge "Change metavar name for registered limit delete" 2025-11-26 12:29:54 +00:00
Zuul
79db64f264 Merge "Remove duplicate test utilities" 2025-11-25 14:47:22 +00:00
Stephen Finucane
fb6dad48db Remove duplicate test utilities
We cannot remove them fully, but we can remove a lot of them. Further
cleanup is needed here to remove the references but that will be done
once a version of osc_lib with fixes is included.

Change-Id: Ifd200bd3d3e5c02c239a8ad0e6cee6d823e26544
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
2025-11-25 12:06:49 +00:00
Luan Utimura
4132ca1818 volume: Add missing backup_id field in tests
This change also reverts commit:
  * 5f1ffe742c

Depends-On: https://review.opendev.org/c/openstack/openstacksdk/+/958801
Change-Id: Icac78179bc324e6fbe762f8095f2cba490ef6aea
Signed-off-by: Luan Utimura <luan.utimura@luizalabs.com>
2025-11-24 13:50:38 -03:00
0weng
0411805608 Change metavar name for registered limit delete
Change registered limit argument to plural and remove `id` suffix for
`registered limit delete` command. Also, note that service can be
specified by name or ID in help description.

Change-Id: I16950a5ac1a197761592304dcb71dcb09d608d78
Signed-off-by: 0weng <oweng@osuosl.org>
2025-11-19 14:29:31 -08:00
Brian Haley
c17c5f0df6 Try to make help text of network code consistent
Just change all text to be as consistent as possible.

TrivialFix

Change-Id: I959cda9b0688f0fcec0f55ce4c8cadf209d3537f
Signed-off-by: Brian Haley <haleyb.dev@gmail.com>
2025-11-18 19:39:15 -05:00
Zuul
d6b2f42cfb Merge "hacking: Check for missing ignore_missing calls" 2025-11-17 10:58:12 +00:00
Zuul
e9b720e1c7 Merge "trivial: Add missing ignore_missing arguments" 2025-11-17 10:57:14 +00:00
Zuul
0b88ea4fab Merge "identity: Remove duplicated _find_sdk_id method" 2025-11-17 10:28:13 +00:00
Zuul
d7e71480d4 Merge "identity: Fix filtering endpoints by project with domain" 2025-11-17 10:28:11 +00:00
Zuul
351d537cb0 Merge "trivial: Normalize some client usage" 2025-11-17 10:26:43 +00:00
Stephen Finucane
db6c34c2c7 hacking: Check for missing ignore_missing calls
This comes up in reviews frequently. Let's automate it.

Change-Id: Ia7ebd7cf29fe4550b22921e898bebaaa5f7bb4f6
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
2025-11-14 11:54:41 +00:00
Stephen Finucane
73021165ff trivial: Add missing ignore_missing arguments
This prevents a class of bugs.

Change-Id: I96e1cd8ed4a682ef5c95f67f3d1246f7026eada9
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
2025-11-14 11:54:40 +00:00
Stephen Finucane
55fd501657 identity: Remove duplicated _find_sdk_id method
We have a few instances of this. Settle on one.

Change-Id: Id115fea1c59ad75ec8e00d665e587020f7177a55
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
2025-11-14 11:51:02 +00:00
Stephen Finucane
a5e4d5f0fa identity: Fix filtering endpoints by project with domain
We were incorrectly passing domain_id as a positional argument, causing
it to get picked up as the ignore_missing argument instead. Correct
this, fixing another bug where the look of projects or domains could be
forbidden by policy, in the process. The latter is unlikely to happen,
given endpoint lookup is typically an admin-only operation, but it's
better to be safe.

Change-Id: Idd3300040967d781b7743accd62298cb24c62872
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
2025-11-14 11:47:18 +00:00
Stephen Finucane
db2c1a5e2b trivial: Normalize some client usage
Ahead of rework in this area.

Change-Id: I1b1c2370967381903970870da8cbe0868b1e23e1
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
2025-11-13 15:20:35 +00:00
Zuul
0e8aa79f9f Merge "Add functional tests for image metadef resource type list" 2025-11-13 15:17:36 +00:00
Zuul
3b6f9ee7ba Merge "Add functional test for cached image command" 2025-11-13 13:34:57 +00:00
Zuul
7505186a41 Merge "ruff: Use more specific name to enable pyupgrade rule" 2025-11-11 20:10:45 +00:00
Takashi Kajinami
188737c69c ruff: Use more specific name to enable pyupgrade rule
UP is the exact name of the rule, instead of U. Use the exact name to
avoid potential problems caused by any UX rules which can be added
in the future.

Change-Id: I5fa59181fcd3e28bf3c87ce2a5e610561b2ee8a8
Signed-off-by: Takashi Kajinami <kajinamit@oss.nttdata.com>
2025-11-12 01:41:37 +09:00
Zuul
1b6df0b5bb Merge "Add new hacking rules" 2025-11-07 18:17:29 +00:00
Zuul
232a0ab68d Merge "tests: Avoid more unnecessary mocks" 2025-11-07 17:18:53 +00:00
Zuul
23f47e0dab Merge "tests: Remove duplicated fake network client" 2025-11-07 17:18:50 +00:00
Stephen Finucane
eb7c4c61a9 Add new hacking rules
To catch some obvious issues.

Change-Id: Ic0ddc95100811e7b313b519aad7d687a1415020b
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
2025-11-07 15:45:51 +00:00
Stephen Finucane
33d34bdfe8 Remove tests for other osc-lib
These are already found in osc-lib itself.

Change-Id: I51114a5a79d6cd6ea46f60284066132b2e54a1a5
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
2025-11-07 14:14:05 +00:00
Stephen Finucane
7116449190 tests: Avoid more unnecessary mocks
Change-Id: I04672d46595e93b19f873a54d5be9363d262370b
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
2025-11-07 14:06:36 +00:00
Stephen Finucane
44dfa157e4 tests: Remove duplicated fake network client
This must have crept in some time after [1] merged.

[1] Ic203964c7dede7dd80ae2d93b8fa1b7e6634a758

Change-Id: Ic0603db8b1a59b7704c51b0e0ffceb7db2e781d3
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
2025-11-07 12:43:11 +00:00
OpenStack Release Bot
3cc6b24bb5 reno: Update master for unmaintained/2024.1
Update the 2024.1 release notes configuration to build from
unmaintained/2024.1.

Change-Id: Ia4fff2a8e0f9bb083423c2e5c7339a46aaccd271
Signed-off-by: OpenStack Release Bot <infra-root@openstack.org>
Generated-By: openstack/project-config:roles/copy-release-tools-scripts/files/release-tools/change_reno_branch_to_unmaintained.sh
2025-10-31 12:05:25 +00:00
Zuul
492a184add Merge "Add option to filter for projects when listing volume backups" 2025-10-22 17:51:49 +00:00
Jan Ueberacker
305e037df2 Add option to filter for projects when listing volume backups
Change-Id: Idb07c1be90a98b65b6c1b8f888d0ca5309f8cbc4
Signed-off-by: Jan Ueberacker <jan.ueberacker@inovex.de>
2025-10-22 16:33:09 +02:00
Zuul
b0763f9f9a Merge "Extend project delete command description" 2025-10-14 18:46:12 +00:00
Stephen Finucane
20ad83bf84 pre-commit: Bump versions
We need to rename two hooks.

Change-Id: I15582a23da6ea6babf2b277ff443b7cdb764c9f9
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
2025-10-10 10:40:55 +01:00
Zuul
b28b3249de Merge "Fix openstack quota show without cinder" 2025-10-02 13:40:36 +00:00
Zuul
4282a512c0 Merge "identity: Fix 'user list --project' option" 2025-10-02 12:29:02 +00:00
Stephen Finucane
0ed122094a identity: Fix 'user list --project' option
The 'role_assignments_filter' identity proxy method requires either a
user or group, which defeats the entire purpose of the command when used
with this option. Use 'role_assignments' instead.

Change-Id: I8fb705c55fb4e81fa82d4a7dbe4c5bf7e1edd98a
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
Closes-bug: #1616104
2025-10-02 09:58:46 +00:00
Matt Anson
fb8cdd4441 Ensure show on absent appcreds raises exception
Currently, running ``application credential show`` on a
non-existent appcred will exit normally and display a
formatted application credential with no data, despite
the Keystone API returning a 404.

Ensure that querying a non-existent application
credential raises an exception message and an exit-code
1 to the user.

Closes-Bug: #2126565
Change-Id: I597d2d4064f1020c5ac40862ecc556f3c94b53eb
Signed-off-by: Matt Anson <matta@stackhpc.com>
2025-10-01 14:36:17 +01:00
Thomas Goirand
de88853de2 Fix openstack quota show without cinder
Per this Debian bug [1], 'openstack quota show --default' fails when
cinder is NOT installed. This is also true of other services.

[1] https://bugs.debian.org/1109288

Change-Id: I361da44b9f1d09ba3a454632d41e2110a3815395
Signed-off-by: Svein-Erik Skjelbred <svein-erik@skjelbred.com>
Signed-off-by: Thomas Goirand <zigo@debian.org>
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
2025-09-30 10:32:44 +01:00
wonjun0120
4cf70113d2 Add functional test for cached image command
Implements tests for cache clear operations including queue,
cache, and combined clearing functionality.

Change-Id: I71056bb5db6c3de4f9294ac1b661ab927f59c867
Signed-off-by: wonjun0120 <wonjundero@gmail.com>
2025-09-25 23:44:09 +09:00
jiwonjang
6b6a9bafd8 Add functional tests for image metadef resource type list
Implements functional tests for 'image metadef resource type list' command.

Change-Id: If645a04d4b8800da44041769f08b1e81332af33c
Signed-off-by: jiwonjang <kooriangman@gmail.com>
2025-09-25 21:12:10 +09:00
Alexey Stupnikov
c0ada2d6ab Extend project delete command description
"openstack project delete" command doesn't try to figure out if
other services are using specified project somehow before trying
to delete it. This patch extends command description to ensure
that this is clearly communicated to users.

Related-bug: #2118900
Change-Id: I3ae0b2a8f04d4f791cab46ccd89f400549d24ecd
Signed-off-by: Alexey Stupnikov <aleksey.stupnikov@gmail.com>
2025-09-19 19:37:28 +02:00
OpenStack Release Bot
3c3ea30bd3 Update master for stable/2025.2
Add file to the reno documentation build to show release notes for
stable/2025.2.

Use pbr instruction to increment the minor version number
automatically so that master versions are higher than the versions on
stable/2025.2.

Sem-Ver: feature
Change-Id: I6aec2d1f91ed7fc2dba466574b4efb92b4bd7c88
Signed-off-by: OpenStack Release Bot <infra-root@openstack.org>
Generated-By: openstack/project-config:roles/copy-release-tools-scripts/files/release-tools/add_release_note_page.sh
2025-09-16 08:11:27 +00:00
Zuul
8f3277326c Merge "Validation: Cannot create network with segmentation id alone" 2025-09-11 17:49:10 +00:00
dlawton
b808b82dfb Validation: Cannot create network with segmentation id alone
Change-Id: I7d98921fe6f2819a6427bc826d640a6685a00da7
Signed-off-by: Dan lawton <dlawton@redhat.com>
Closes-bug: #1693106
2025-09-11 15:28:51 +00:00
Zuul
afcce59c8d Merge "Add functional tests for role assignment list" 2025-09-10 11:02:58 +00:00
doburn
a5a6ec27e5 Add functional tests for role assignment list
Implements tests for `role assignment list` domain options.

The options covered are:
- `--user-domain`
- `--group-domain`
- `--project-domain`
- `--role-domain`

Change-Id: Ia42dcc337df0de7d5a93250696b807038a2d9d0e
Signed-off-by: doburn <kwon2852@gmail.com>
2025-09-10 16:07:25 +09:00
Zuul
bc98e21559 Merge "tests: Simplify catalog functional tests" 2025-09-09 22:48:09 +00:00
Zuul
475d69efad Merge "Bug Fix: Skip invalid server ID during multi-server delete" 2025-09-08 19:43:25 +00:00
dlawton
3dfeb5ed08 Bug Fix: Skip invalid server ID during multi-server delete
Change-Id: I8e5339f07b43dd0a9422eaf33346bbfdf2c9b328
Signed-off-by: Dan Lawton <dlawton@redhat.com>
Closes-Bug: #2122056
2025-09-08 17:13:47 +01:00
Rajesh Tailor
e7554603ac Fix microversion 2.100
This change fixes missing conditional logic for microversion
2.100 which adds support for showing `scheduler_hints` field
to `openstack server list --long` output.

Change-Id: I2820e02a91deb73850f37dc737dbec79dea99e8d
Signed-off-by: Rajesh Tailor <ratailor@redhat.com>
2025-09-03 16:53:35 +05:30
Stephen Finucane
5f1ffe742c volume: Temporarily ignore Volume.backup_id column
We really need a better way to do this.

Change-Id: I631748e2dfe3c136156d7987eab952370a88d35b
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
Related: https://review.opendev.org/c/openstack/openstacksdk/+/958801
2025-08-29 13:31:58 +01:00
minkyukim
68d1d01b2a tests: Simplify catalog functional tests
Combine multiple test cases into a single test case, in order to
speed up execution.

Change-Id: Idcfd0c8c5b7418046601d222248c0cd16886e079
Signed-off-by: minkyukim <mj110602@gmail.com>
2025-08-24 04:29:44 +09:00
Stephen Finucane
94e447af80 tests: Remove use of namedtuple
Change-Id: I19a272ffd260bab263dd63cb920802b792e192eb
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
2025-08-20 17:41:21 +01:00
Rajesh Tailor
dbddbf9760 Fix microversion 2.96
This change fixes missing conditional logic for
microversion 2.96 which adds `pinned_availability_zone`
field to `openstack server list` output.

Change-Id: I1e398bb3379fa6443b0a44db76baaf6241a945e7
Signed-off-by: Rajesh Tailor <ratailor@redhat.com>
2025-08-20 18:19:13 +05:30
Zuul
34f431bade Merge "compute: Fix flavor create --id auto" 2025-08-18 16:59:33 +00:00
Artem Goncharov
a312e9cdad Adopt sdk_fakes for compute.test_flavor
Use sdk_fakes inside test_flavor. The only left fake is for
flavor_access, for which there is no resource in SDK.

Change-Id: I8fcfb734eb45308b80aa1478c2935c9881fee928
Signed-off-by: Artem Goncharov <artem.goncharov@gmail.com>
2025-08-18 13:57:56 +01:00
Stephen Finucane
5feaa952ad compute: Fix flavor create --id auto
This was inadvertently broken during the switch from novaclient to SDK.
Fix it for now but also deprecate it since it is an unnecessary alias.

Change-Id: Iaf136d82e00defc86e57ae4ea7e848246f2ade2c
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
Closes-bug: #2120833
2025-08-18 12:20:44 +01:00
Zuul
d90e18b08c Merge "Support image save with chunk-size option" 2025-08-15 13:37:16 +00:00
Zuul
6ba9473cdb Merge "Adds CLI support for `glance md-namespace-properties-delete`" 2025-08-15 13:37:13 +00:00
Zuul
ae7e8448ba Merge "image: Add hashing-related fields" 2025-08-15 13:18:10 +00:00
Mridula Joshi
2177f07dfb Adds CLI support for `glance md-namespace-properties-delete`
This patch modifies the command to delete all metadef properties
inside a namespace. This operation can be called by `image metadef
property delete`

Change-Id: Iff9bda0dddfa157be0438a66d1d05da7b0b437c3
Signed-off-by: Mridula Joshi <mrjoshi@redhat.com>
2025-08-15 11:16:46 +00:00
Artem Goncharov
a73698490a image: Add hashing-related fields
Add support for the 'os_hash_algo' and 'os_hash_value' image attributes
added with Image API 2.7.

Change-Id: Id8fe6f3fecf77f537587e9088b207ef2077a9def
Signed-off-by: Artem Goncharov <artem.goncharov@gmail.com>
2025-08-15 12:01:58 +01:00
Hang Yang
13fe801968 Support image save with chunk-size option
Add '--chunk-size' option to 'image save' command to control the size of
bytes to read at one time.

Change-Id: I0a02323384433010b8c6804a4337040acb13da8f
Signed-off-by: Hang Yang <hangyang@verizonmedia.com>
2025-08-15 11:59:30 +01:00
Stephen Finucane
81db99b32b doc: Indicate md-namespace-import as WONTFIX
This is a meta command that can be easily achieved via some shell
scripting. We don't need it in OSC.

Change-Id: Ia3fc8d0458cb0c0dc4695347ef953028112a9c49
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
2025-08-15 11:47:12 +01:00
Zuul
37228ae2d3 Merge "tests: Avoid unnecessary mocks" 2025-08-15 04:48:40 +00:00
Zuul
87fe04ae08 Merge "tests: Simplify mocking in server tests" 2025-08-15 04:48:26 +00:00
Zuul
7bb5857081 Merge "Remap custom named Image attributes when unsetting" 2025-08-15 04:48:23 +00:00
Zuul
a8751b00fe Merge "Add device ID and device owner to port unset" 2025-08-15 03:29:28 +00:00
Zuul
2a492fb58a Merge "Remove leading empty line from server create with --wait" 2025-08-15 03:29:22 +00:00
Zuul
ad2a511434 Merge "Replace deprecated assertItemsEqual" 2025-08-14 13:34:42 +00:00
Zuul
8a88d65a67 Merge "docs: Add note about scoping on tokens" 2025-08-14 11:10:19 +00:00
Stephen Finucane
a99ae364fc tests: Avoid unnecessary mocks
Change-Id: Ibb1bf5c29bf37d3f31889b091a055d0308e8cd85
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
2025-08-14 12:03:16 +01:00
dna
e8a7db5858 tests: Simplify mocking in server tests
Replace assignments of `Mock` objects to methods that are already
mocked in the class functions within test_server.py

Change-Id: I446632301c1b9f94545a0b8314e54e761d5d296f
Signed-off-by: dna <la18byeol@gmail.com>
Story: 2011459
Task: 52211
2025-08-14 12:03:00 +01:00
Mridula Joshi
9f55b253a3 Adds CLI support for `glance md-namespace-objects-delete`
This patch adds operation which delete all metadef object inside a
namespace.
This can be implemented by `image metadef object delete`

Change-Id: Ib196e295aad1921d8bc0c451522e0ad530389134
Depends-on: https://review.opendev.org/c/openstack/openstacksdk/+/901671
Signed-off-by: Cyril Roelandt <cyril@redhat.com>
2025-08-07 20:25:50 +02:00
Alexey Stupnikov
e37148484c Remap custom named Image attributes when unsetting
Some Image attributes defined in openstacksdk are named differently
from actual properties managed by Glance. Because openstackclient
checked property names to be unset against Image object properties,
it was impossible to unset such properties.

This patch introduces a IMAGE_ATTRIBUTES_CUSTOM_NAMES dictionary
mapping real property names with custom attribute names.

Closes-bug: #2115732
Change-Id: I7296fc293dff9208464c9a07f58ce3e9ffabd3e9
Signed-off-by: Alexey Stupnikov <aleksey.stupnikov@gmail.com>
2025-07-29 18:24:57 +02:00
Alexey Stupnikov
46b25c7c0b network: Add '--project' to SG rule list command
Include '--project' and '--project-domain' filtering options to the
'security group rule list'.

Closes-Bug: #1648317
Change-Id: I19e423906846073cfa1e45b4a295b3a8f5d11970
Signed-off-by: Alexey Stupnikov <aleksey.stupnikov@gmail.com>
2025-07-24 18:06:03 +02:00
Stephen Finucane
c7d465a221 volume: Migrate 'volume show' to SDK
Change-Id: Ibd9d7a62c2500a1f31aa2d3d13ac7e8bad4e6964
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
2025-07-18 11:38:26 +01:00
Stephen Finucane
5e5f12ba40 volume: Migrate 'volume migrate' to SDK
Change-Id: I99af5fce0c2c184e300dfbf5624b91eeed38c94b
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
2025-07-18 11:20:42 +01:00
Stephen Finucane
504cbd24e2 volume: Migrate 'volume create' to SDK
We need a shim for consistency group support, which we may eventually
port to SDK but have not yet. Otherwise, this is rather straightforward.

Change-Id: Ic880b7a64cde2148c266d549c4768c669ba3f501
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
Depends-on: https://review.opendev.org/c/openstack/openstacksdk/+/943800
2025-07-17 16:41:46 +01:00
Doug Goldstein
51ecb5f984
volume: fix volume service set call
The command line operation could never work due to the incorrect call of
the openstacksdk API. This is updated to make it work and report errors
back to the user.

Closes-Bug: #2116969
Change-Id: I87cc410853c03b00dd1549d67cb1b9a8145bcfaa
Signed-off-by: Doug Goldstein <cardoe@cardoe.com>
2025-07-16 08:48:45 -05:00
Zuul
ffa37bab12 Merge "Add is_shared to security_groups" 2025-07-15 15:32:51 +00:00
elajkat
1b2dfeacf4 Add is_shared to security_groups
Change-Id: I1ff4aa9c6579699e6b479fdb429668de894cd012
Related-Bug: #1999774
Signed-off-by: elajkat <lajos.katona@est.tech>
2025-07-15 14:27:46 +01:00
Zuul
9d3a956a54 Merge "Add metadata as a filter condition when listing volumes" 2025-07-04 11:51:51 +00:00
ohjiwoo
572eeb6d38 Add metadata as a filter condition when listing volumes
When listing volumes, the API provides metadata as a query parameter, but this feature is not implemented in the client code. So add parameter.

story: 2011487
task: 52446
Change-Id: I4bf66d4e073c86296fa96ee29c2b33d771e18293
Signed-off-by: ohjiwoo <jiwooo.oh@samsung.com>
2025-07-04 09:28:40 +00:00
Takashi Kajinami
b6af7883b7 Replace deprecated assertItemsEqual
It has been provided by testtools to ease migration from python 2, but
was deprecated in 2.7.2[1] and will be removed in 2.8.0[2].

[1] https://github.com/testing-cabal/testtools/commit/e0d56b7ce65ae5b3d
[2] https://github.com/testing-cabal/testtools/commit/f01e86084e6a858d1

Change-Id: I8b68212a88553aff5c3b4182c246b3c0f7365cf6
2025-06-27 23:46:22 +09:00
Stephen Finucane
4ea3deda61 Remove duplicate Python version declarations
ruff can automatically detect this [1] now that it is defined in
pyproject.toml. mypy defaults to the version of Python used to run mypy
[2][3] so we need to keep its configuration around a while longer.

[1] https://docs.astral.sh/ruff/settings/#target-version
[2] https://mypy.readthedocs.io/en/stable/config_file.html#confval-python_version
[3] https://github.com/python/mypy/issues/19349

Change-Id: If5b3e6ff2483d73d5ff54cc28c1558cb9852b464
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
2025-06-27 09:23:06 +01:00
Stephen Finucane
1b4fe6fac1 Migrate setup configuration to pyproject.toml
Change-Id: If7a6252ce751e875881654965762f6f514a15342
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
2025-06-27 09:22:45 +01:00
Takashi Kajinami
ea85c7aa4d Replace deprecated datetime.utcfromtimestamp
It was deprecated in Python 3.12 in favor of datetime.fromtimestamp[1].

[1] https://docs.python.org/3/library/datetime.html#datetime.datetime.utcfromtimestamp

Change-Id: Ia805157eaecac0c61d4c5f88daa430ec6d69a9d4
2025-06-18 18:15:52 +09:00
psnew14
edb17881d0 Remove leading empty line from server create with --wait
The "openstack server create" with "--wait" args was priting an extra empty line character before the server ID.
This commit removes the extra empty line.

story: 2010947
task: 48984
Change-Id: Ib5ba1c9f23e7655ddfae0e5b644ed167ecd6485e
2025-06-01 22:50:55 +09:00
Zuul
f4c4a7343c Merge "Fix openstack image import --method web-download --uri 'invalid value'" 2025-05-30 17:15:43 +00:00
Zuul
d5e6392454 Merge "identity: Remove unnecessary helper" 2025-05-30 14:37:41 +00:00
djp
9ad18c4967
Fix openstack image import --method web-download --uri 'invalid value'
although python-openstackclient run command(image import) with invalid uri,
but the request succeeds. Fixed it to throw an exception
when requesting with an invalid URI.

unit test added. the test cover --uri 'invalid value'

Task: 52251
Story: 2011468
Closes-Bug: 2111777
Change-Id: I62cd8cdf054b6a5e07d664a543b0923ce5f20f83
2025-05-30 22:47:29 +09:00
Stephen Finucane
2e301857af docs: Add note about scoping on tokens
Change-Id: I4df74eaa1aa82fb8666bc1e6728b55a3e81bc76a
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
2025-05-29 19:13:21 +01:00
Zuul
c923ed5893 Merge "Security-groups: Temporarily ignore is_shared" 2025-05-29 15:59:21 +00:00
elajkat
5f602475cd Security-groups: Temporarily ignore is_shared
[1] introduces is_shared field to SDK security-groups.
Till that is merged and released temporary skip the new field.

[1]: https://review.opendev.org/c/openstack/openstacksdk/+/950305
Related-Bug: #1999774

Change-Id: I71cdf96460bbb21ee61105ef9ccc23170b0b5460
2025-05-29 07:53:58 +00:00
Zuul
a8d2c56337 Merge "bug fix volume group show command." 2025-05-26 16:28:16 +00:00
Zuul
606fd132bc Merge "identity: Normalise output of application credentials commands" 2025-05-26 16:03:37 +00:00
Stephen Finucane
2f03c3ea3c identity: Remove unnecessary helper
Make better use of argparse and eliminate the need for a helper in the
process.

Change-Id: Ibdc9b4bfbb4d532ddb05bce9b49bcf0580cce76d
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
2025-05-26 16:12:28 +01:00
hongp
1c70e264a4 bug fix volume group show command.
There is no "show" function in the "group" class of python-cinderclient. Instead, there is a "get" function, so I fixed it.
0125495f92/cinderclient/v3/groups.py (L134)

Closes-Bug: #2111539
Change-Id: If61d8cf115c9ff04d172d7344565e693774e7a21
2025-05-23 10:08:39 +09:00
Zuul
88b59d8975 Merge "evacuate: respect original SHUTOFF state in --wait completion" 2025-05-21 10:29:33 +00:00
waf
3909e93301 evacuate: respect original SHUTOFF state in --wait completion
When running `openstack server evacuate --wait`, the command would hang
indefinitely if the instance was originally in SHUTOFF state, because
only “ACTIVE” was treated as a successful completion. We now capture
the server’s status before evacuation and dynamically include
“SHUTOFF” in the `success_status` list if the instance was already
shut off. This ensures that a shutoff instance is accepted as a valid
completion without requiring manual intervention.

Unit tests have been added and updated to cover both:
- pre-evacuation ACTIVE → success_status=['active']
- pre-evacuation SHUTOFF → success_status=['active','shutoff']

Closes-Bug: #2103426
Change-Id: I86ad1cd173a144b16fde1dbac87819fab2d7a50a
2025-05-21 16:52:24 +09:00
Zuul
5a565ca1ce Merge "Fix missing 'options' field in 'user show' command" 2025-05-20 18:08:26 +00:00
Chaemin-Lim
9bcb1c5c00 Fix missing 'options' field in 'user show' command
This patch fixes a bug where the 'options' field was missing from
the output of the 'openstack user show' command since v7.0.0.

The issue was caused by the 'options' field not being included in
the column list in the _format_user function. This field is important
as it contains various user settings such as multi-factor authentication
configurations and password policy exemptions.

This patch:
1. Adds 'options' field to the column list in _format_user function
2. Updates all affected unit tests to include this field
3. Uses getattr() to safely handle cases where the options field may be absent

Without this fix, users cannot see important options like multi-factor
authentication settings through the CLI, which could lead to security
configuration issues being overlooked.

Closes-Bug: #2084946
Change-Id: I4319268ad4310e6164eb8e65664d73f9b32cdd78
2025-05-20 23:46:39 +09:00
0weng
a2be1b014e Identity: Migrate 'group' commands to SDK
Change-Id: I5a477426318d77021c0430efa1d1f9a7b1ee2633
2025-05-19 15:39:18 -07:00
Zuul
5fb4559a1f Merge "tests: Simplify mocking in image tests" 2025-05-19 16:25:49 +00:00
Zuul
50dd542519 Merge "network: Allow multiple FIP filter opts" 2025-05-19 16:05:20 +00:00
Zuul
de7762f9b8 Merge "volume: Migrate 'volume delete' to SDK" 2025-05-19 15:54:41 +00:00
Zuul
bea4d834ce Merge "volume: Temporarily ignore new volume columns" 2025-05-19 15:54:38 +00:00
Zuul
47a2596b39 Merge "volume: Split v2, v3 create, delete commands" 2025-05-19 15:54:36 +00:00
Zuul
07909a6a6c Merge "volume: Migrate 'snapshot show', 'snapshot list' to SDK" 2025-05-19 15:47:53 +00:00
Zuul
803c3c6d6a Merge "volume: Migrate 'snapshot set', 'snapshot unset' to SDK" 2025-05-19 15:47:50 +00:00
Zuul
110cd9faae Merge "volume: Migrate 'snapshot create' to SDK" 2025-05-19 15:45:04 +00:00
Zuul
2e0318629d Merge "volume: Migrate 'snapshot delete' to SDK" 2025-05-19 15:45:01 +00:00
Zuul
0e47fc4e45 Merge "volume: Add v3-specific volume snapshot module" 2025-05-19 15:37:26 +00:00
Zuul
5c9c4e3dd7 Merge "tests: Use SDK mocks for SDK-based commands" 2025-05-19 15:15:38 +00:00
Zuul
ff7497689c Merge "volume: Migrate 'backup set', 'backup unset' to SDK" 2025-05-19 15:15:35 +00:00
Zuul
d27ff5a650 Merge "volume: Migrate 'service *' to SDK" 2025-05-19 15:15:32 +00:00
Zuul
82170be8d6 Merge "volume: Add v3-specific volume service module" 2025-05-19 15:15:29 +00:00
Zuul
e3bd9a4c01 Merge "volume: Migrate 'block storage log level *' to SDK" 2025-05-19 15:14:29 +00:00
Zuul
d5238d1dab Merge "Add a column to all_projects tag of server list cmd" 2025-05-19 14:17:25 +00:00
Zuul
5f2145d102 Merge "Add filters to search for enabled/disabled users and projects" 2025-05-19 14:17:22 +00:00
Zuul
51305d06f7 Merge "identity: Migrate 'domain' commands to SDK" 2025-05-19 14:17:19 +00:00
djp
444a1df705 tests: Simplify mocking in image tests
Replace assignment of `Mock` objects to methods that are already mocked
in tests/unit/v1/test_image.py, tests/unit/v2/test_image.py

story: 2011459
task: 52210
Change-Id: I0a4644c27066c26cf0ee0f3613c174f141fe94ed
2025-05-19 13:46:27 +00:00
Stephen Finucane
f1cd38aabf identity: Normalise output of application credentials commands
cliff is now smarter (I9155763eee15e19eab23b48989dfcc19ea2c5d34), so we
can effectively revert change I6b4f1b793dc383856bfdf9a01514381be3cd2bf1.
We bump the dependencies to ensure this.

Change-Id: I2af19043fd66b5be0826a774baeabeac7110a4aa
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
2025-05-19 14:14:14 +01:00
Stephen Finucane
01c1b1e36f network: Allow multiple FIP filter opts
This is allowed by the neutron API. Allow it in OSC.

Change-Id: I7642ecd686d11c5af9e11cc80896243e853e33f3
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
2025-05-19 14:07:37 +01:00
Zuul
5e1fc3db05 Merge "Fix incorrect warning with --password-prompt option" 2025-05-19 12:48:49 +00:00
Stephen Finucane
082aca89d6 volume: Migrate 'volume delete' to SDK
Change-Id: Ia7d2bfb14945cb5c185daa820f699a4cfe4a3e7f
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
2025-05-19 13:20:22 +01:00
Stephen Finucane
125133d056 volume: Temporarily ignore new volume columns
Change-Id: I4296766a1576c64eb3927cb0557aef5346b2d6f6
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
2025-05-19 13:20:22 +01:00
Stephen Finucane
03aa172fd0 volume: Split v2, v3 create, delete commands
Change-Id: I42616b9586fede3b775bc0fdbba73df90b555d46
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
2025-05-19 13:20:21 +01:00
Stephen Finucane
267a29d594 volume: Migrate 'snapshot show', 'snapshot list' to SDK
Change-Id: I40de24012363f496e46c3dddc31a3e2563ccf443
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
2025-05-19 13:20:20 +01:00
Stephen Finucane
3c6fa42642 volume: Migrate 'snapshot set', 'snapshot unset' to SDK
Change-Id: Id34d460c8c5656bf43f48717b13a002508562e4e
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
2025-05-19 13:20:19 +01:00
Stephen Finucane
e0020aec6a volume: Migrate 'snapshot create' to SDK
Change-Id: I0c2811b8518c41658803a7b2053f0f5d5114ed67
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
2025-05-19 13:20:18 +01:00
Stephen Finucane
fc42f12eb2 volume: Migrate 'snapshot delete' to SDK
Change-Id: Iba89d521ec17a642c5905b0cff908b5a4a9dafd0
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
2025-05-19 13:20:16 +01:00
Stephen Finucane
e1ff450e34 volume: Add v3-specific volume snapshot module
Change-Id: I23026abbb909c082fbc0fe0c9b2efcc89f4d464a
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
2025-05-19 13:20:15 +01:00
Stephen Finucane
a9b9984973 tests: Use SDK mocks for SDK-based commands
Change-Id: I7e781875d4467ed097196771144e25cee38ce678
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
2025-05-19 13:20:13 +01:00
Stephen Finucane
8eb1a183fe volume: Migrate 'backup set', 'backup unset' to SDK
Change-Id: Iced346df828faab1ff08a2645ff64f4cfea25cb1
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
2025-05-19 13:20:12 +01:00
Stephen Finucane
b933330d55 volume: Migrate 'service *' to SDK
Change-Id: I81254c6cde8783be371ccdcface5027eb247b1ce
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
2025-05-19 13:20:10 +01:00
Stephen Finucane
1ee3ef33d7 volume: Add v3-specific volume service module
Ease migration.

Change-Id: Ibcdb157ba1bf370c63320d3a1afcf3c400370624
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
2025-05-19 13:20:09 +01:00
Stephen Finucane
00f4cf9c17 volume: Migrate 'block storage log level *' to SDK
Change-Id: Ic03f65fee197a85518df448c18a0fd2c11d51993
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
2025-05-19 13:20:07 +01:00
xfrnk2
a74850d2c4 Add a column to all_projects tag of server list cmd
Add a Project ID column to the --all-projects tag of server list cmd
Differentiate from the basic command, add a column for Project ID
to facilitate easier identification.
Add test code for the Project ID column of --all-projects tag.

Change-Id: I12af2c91f934e7cd268d21cf76dda78646ed2ff4
2025-05-19 12:38:20 +01:00
Chaemin-Lim
eea369e73c Fix incorrect warning with --password-prompt option
When creating a user with the --password-prompt option, a warning is
incorrectly displayed stating that no password was supplied, even though
a password was entered. This occurs because the code checks parsed_args.password
instead of the password variable that actually stores the prompted password.

This patch fixes the issue by checking the 'password' variable
instead of 'parsed_args.password' in the warning condition. A test
case has been added to verify that no warning is displayed when
using --password-prompt and entering a password.

Closes-Bug: #2091836
Change-Id: Ib3ddc7e400ee7988f605c00db534bccc3617d982
2025-05-19 16:12:06 +09:00
Stephen Finucane
32762bcda6 compute: Fix key used for NIC fixed IP field
Change-Id: If099ac0e2663228681e87e2f4821b746c8113ffc
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
Closes-bug: #2106221
2025-05-18 15:12:17 +01:00
Zuul
79de137152 Merge "identity: Add missing user argument" 2025-05-16 02:50:39 +00:00
Zuul
f380d029df Merge "identity: Fix listing of applications credentials by user" 2025-05-16 02:50:36 +00:00
Zuul
efce69b47f Merge "Don't warn about unsupported version with SDK-based commands" 2025-05-15 16:19:20 +00:00
Stephen Finucane
e26b447925 identity: Add missing user argument
Change-Id: Ifd2b32e97d1f5fd426f333da13852a8bb6821f1b
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
Closes-bug: #2110765
2025-05-15 16:05:56 +01:00
Stephen Finucane
94d17b8762 identity: Fix listing of applications credentials by user
Change-Id: I71f1c4f338694e2b50e71b6907c415bbb6a768fa
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
Closes-bug: #2107354
2025-05-15 15:59:34 +01:00
Zuul
9de5e58bc8 Merge "Bump Python version used for linters to 3.10" 2025-05-09 16:13:11 +00:00
Zuul
05accdde9a Merge "Drop support for Python 3.9" 2025-05-09 16:13:08 +00:00
Stephen Finucane
7c7c066096 Bump Python version used for linters to 3.10
Change-Id: I693516fc2a08218c50d83a3ab121b51254f97958
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
2025-05-07 17:55:39 +01:00
Stephen Finucane
ce2a253d5a Drop support for Python 3.9
Change-Id: If7d8ce2be7081fdcd609c54a211c91439cddce6b
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
2025-05-07 17:55:31 +01:00
Michael Still
5d730f374b Add support for spice-direct console types.
This patch adds support for Nova microversion 2.99 which exposes the new
spice-direct console type and the pre-existing /os-console-auth-token/ API.

+----------+----------------------------------------------------------+
| Field    | Value                                                    |
+----------+----------------------------------------------------------+
| protocol | spice                                                    |
| type     | spice-direct                                             |
| url      | http://127.0.0.1:13002/nova?token=f78009fb-41ad-...      |
+----------+----------------------------------------------------------+

+----------------------+--------------------------------------+
| Field                | Value                                |
+----------------------+--------------------------------------+
| host                 | 127.0.0.1                            |
| instance_uuid        | f2477018-aa93-...                    |
| internal_access_path | None                                 |
| port                 | 5900                                 |
| tls_port             | 5901                                 |
+----------------------+--------------------------------------+

Change-Id: I2d33646d6ac9b25076d69be76dcef8f5c465cd1b
Depends-On: https://review.opendev.org/c/openstack/openstacksdk/+/940479
2025-05-07 19:34:17 +10:00
Zuul
a49a290a2b Merge "Update README" 2025-04-17 11:04:23 +00:00
Zuul
871b2a4d80 Merge "Add labels to Dockerfile" 2025-04-17 11:04:21 +00:00
Zuul
6e4c2879d4 Merge "Update the docker image to python3.12" 2025-04-17 11:04:20 +00:00
Zuul
0208a24235 Merge "zuul: Remove osc-upload-image, osc-promote-image jobs" 2025-04-17 09:47:42 +00:00
Stephen Finucane
6cb5d8cd14 Update README
Restructure things to be a little more helpful. Also add a reference to
the Dockerfile we provide and remove an errant header that should have
been removed in change Ife108e6ae191641b56e872e4616a3f4ec78281e8.

Change-Id: I5f562a99ccee7db485b5d40ef4ea6f2e2e362c13
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
2025-04-17 08:53:35 +00:00
Stephen Finucane
22eecc54f8 Add labels to Dockerfile
Change-Id: Ic2c774c4fea263c7b04d20182e3354d9ae93788b
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
2025-04-17 08:53:27 +00:00
Stephen Finucane
f4e97d9733 Update the docker image to python3.12
Change-Id: I2120ac8d27bcefffa0b414cb74871922ccd2ad80
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
2025-04-17 08:53:19 +00:00
Stephen Finucane
e4d621d24f zuul: Remove osc-upload-image, osc-promote-image jobs
We are no longer going to publish these images to Dockerhub, given
the recent changes to quotas there coupled with the fact that no one
appears to be using them [1]. The osc-build-image job is retained to
ensure our Dockerfile keeps working.

[1] https://lists.openstack.org/archives/list/openstack-discuss@lists.openstack.org/thread/BE7PPQL4DGNDZ2SIMUVSK67I5NF3TFCX/

Change-Id: I9d2ca8f90b8244a09832da673491312095520968
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
2025-04-16 17:55:06 +01:00
Stephen Finucane
7d64003196 tests: Stop returning FakeResource in compute tests
This was still being used in places where we have our own API bindings
because SDK does not support the API. Those bindings should be returning
dicts, not FakeResource objects. Correct this, and in doing so fix the
bug this highlights in our cell-down output.

Change-Id: I6647d94fcf5ada8186edbf64c03abd3d8ae7ca56
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
2025-04-14 16:43:22 +01:00
Stephen Finucane
abed52f106 tests: Remove sdk prefix
It is no longer necessary.

Change-Id: I6bcc14be90be7c93ec4729f241299d55885570fd
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
2025-04-14 16:27:32 +01:00
Stephen Finucane
2c878ad2b7 tests: Remove dead code
None of these fakes are used anymore. Remove them.

Change-Id: I06721aa77f93b76b189901bbdc13a9825fe2fc3d
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
2025-04-14 16:27:31 +01:00
Stephen Finucane
b0fe724caf tests: Remove use of legacy resource helpers
This allows us to remove get_servers, create_servers, and
create_one_servers.

Change-Id: I31a86b6333fdc3da1b54407f077873511260a5df
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
2025-04-14 14:10:55 +01:00
Zuul
db4739fc5c Merge "Require confirmation to reset server state." 2025-04-11 13:23:47 +00:00
Sean Mooney
25cd1178b3 Require confirmation to reset server state.
This change updates the server set state command to require confirmation
before it is applied. The same pattern as project clean is used and a
new --auto-approve flag is added to allow skipping the prompt.

Operators often use reset state in cases that are incorrect
this change updates the warning to be more explicit
about when and when not to use it.

Change-Id: Iab14739cf6043ad45ad49edff0580e81d75af2fd
2025-04-11 11:53:14 +01:00
melanie witt
d123be0819 Fix 'openstack keypair list --project <project>'
The --project option of 'openstack keypair list' is supposed to filter
keypairs by a project but has not been working and instead returns
keypairs from all projects.

The reason appears to be because it uses a request for a user list
filtered by project but tenant_id/project_id is not a valid filter for
GET /users.

This fixes the issue by requesting role assignments for the specified
project and then requesting keypairs for users with a role in the
project.

This change depends on a recent openstacksdk bug fix change
Ic552dee83d56278d2b866de0cb365a0c394fe26a which fixed the user_id query
parameter for the compute /os-keypairs APIs. The bug fix was released in
openstacksdk 4.4.0.

Closes-Bug: #2096947

Change-Id: Ibb5757766e3040e58d64388b95678fab9b2b6f23
2025-04-10 17:59:29 -07:00
Zuul
616d6f3a29 Merge "Add support for showing scheduler_hints in server details" 2025-04-10 18:37:26 +00:00
Stephen Finucane
11495e655a Don't warn about unsupported version with SDK-based commands
This doesn't make sense: SDK (and the server) will handle this for us.

Change-Id: I31b84e09eca0dc2bc5316d6727620346ae519512
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
Closes-bug: #2106760
2025-04-10 18:21:17 +01:00
Rajesh Tailor
c66abfc76f Workaround for failing tests on openstacksdk change
The 'show-instance-action-finish-time' blueprint [1] adds support for
showing 'finish_time' for InstanceAction object.

This change adds 'finish_time' as hidden column, so it doesn't fail
tests.

We need to remove this from hidden_column list, once all the changes
related to blueprint are merged and show the field only if microversion
is >= 2.101

This is a workaround for failing tests on patch [2], as per suggestion
from Stephen.

[1] https://blueprints.launchpad.net/openstack/?searchtext=show-instance-action-finish-time
[2] https://review.opendev.org/c/openstack/openstacksdk/+/930562

Implements: blueprint show-instance-action-finish-time
Change-Id: Ib9294a603daed0fdb936be128dfba254b9108799
2025-04-09 12:23:38 +05:30
Douglas Viroel
c68622402e Add support for showing scheduler_hints in server details
Adds support for a new compute microversion that returns the
associated scheduler_hints in ``GET /servers/{server_id}``,
``GET /servers/detail``, ``PUT /servers/{server_id}`` and
``POST /server/{server_id}/action`` (rebuild) responses.

Change-Id: Ia5a4e0047b5123f2fb063cfc9ab1f58b2844308f
2025-04-08 19:27:29 +00:00
Zuul
7ecdb69f08 Merge "Refactor network fakes to sdk properties PART6" 2025-04-08 18:40:06 +00:00
Zuul
0874e2d33f Merge "Refactor network fakes to sdk properties PART 5" 2025-04-08 17:37:57 +00:00
Stephen Finucane
d96c81ff7f Refactor network fakes to sdk properties PART6
Included resources:
router
security_group
security_group_rule

Change-Id: I2423fc31d94f85aeefc7b0a205dfb38829417a29
2025-04-08 16:36:48 +00:00
Stephen Finucane
f870548c7f Refactor network fakes to sdk properties PART 5
Included resources:
qos_policy
qos_rule
qos_rule_type

Note: Parameters in unittests was modified
for compatibility with sdk

Change-Id: Iaa902d64ff1b29a07c28ed2100d437da506be475
2025-04-07 15:53:30 +01:00
Stephen Finucane
80eaa33ffe volume: Make better use of argparse
The latest in a series.

Change-Id: I8273c817e38120ba0b25aebdbfa1c2872222765e
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
2025-04-04 18:11:27 +01:00
Stephen Finucane
181bb194c7 image: Migrate 'create image' volume calls to SDK
Change-Id: Ie57a5c17a6df5a333abd6b11e28b65833740e102
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
2025-04-04 18:11:27 +01:00
Zuul
30aa27b7f9 Merge "tests: Rename 'compute_sdk_client' -> 'compute_client'" 2025-04-03 18:09:11 +00:00
Zuul
dd8e4740ec Merge "compute: Migrate to 'compute' client alias" 2025-04-03 18:08:17 +00:00
Zuul
1e8d243986 Merge "Fix: extend in-use volumes check" 2025-04-03 16:40:17 +00:00
Stephen Finucane
dc68be6b7b tests: Rename 'compute_sdk_client' -> 'compute_client'
Resolve a TODO. Achieved using:

  sed -i 's/self.compute_sdk_client/self.compute_client/' \
    $(ag -w self.compute_sdk_client -l)

Change-Id: I76798058b9dee9fc7ef01ff8656543fbb1266d43
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
2025-04-03 16:39:04 +01:00
Stephen Finucane
dae2539490 compute: Migrate to 'compute' client alias
This is no longer assigned to novaclient, meaning we can use it for SDK.

Change-Id: I43d9ede39d36cc29301f94fa462b9b9d9441807c
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
2025-04-03 16:39:03 +01:00
Zuul
54a5d2f40c Merge "Remove use of formatter function" 2025-04-02 14:40:12 +00:00
Zuul
01f9279dbf Merge "Imported Translations from Zanata" 2025-04-02 12:57:43 +00:00
Stephen Finucane
ac1ad1c4e4 Remove use of formatter function
We also update tests to use proper SDK fakes so we actually test this.

Change-Id: Ib98348cab613b7139f0faa0b5df90ff44353974f
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
2025-04-02 12:39:26 +01:00
Zuul
2148a86fc5 Merge "Remove use of formatter function" 2025-04-02 10:19:34 +00:00
OpenStack Proposal Bot
0554ff60b4 Imported Translations from Zanata
For more information about this automatic import see:
https://docs.openstack.org/i18n/latest/reviewing-translation-import.html

Change-Id: I2493ee06a6d47791be683577f0a9b2c63199a67c
2025-04-02 03:07:33 +00:00
Zuul
ade7da8797 Merge "Prepare for osc-lib changes" 2025-04-01 17:57:57 +00:00
Zuul
4ba21fd6f4 Merge "pre-commit: Enable mypy" 2025-04-01 16:25:53 +00:00
Stephen Finucane
d95e23d92b Remove use of formatter function
Change-Id: I9ef88a4d69ffc3eaae183c77445ac10358d86337
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
2025-04-01 16:42:13 +01:00
Zuul
8e95c0ed31 Merge "typing: Indicate another tuple to be extended" 2025-04-01 14:49:36 +00:00
Zuul
425e430c31 Merge "docs: Remove irrelevant TODO" 2025-04-01 14:01:24 +00:00
Zuul
1f25a2f935 Merge "volume: Remove Cinder v1 support" 2025-04-01 14:01:22 +00:00
Zuul
438878a3ed Merge "docs: Migrate remaining block storage commands to autocommand" 2025-04-01 12:44:07 +00:00
Zuul
c0521743ba Merge "Remove contributor specs" 2025-04-01 12:44:05 +00:00
Stephen Finucane
dc8596fe74 Prepare for osc-lib changes
Change-Id: I665cd61272f881dce2d387da6035a2f35c866add
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
2025-04-01 13:39:50 +01:00
Stephen Finucane
ec4fd81c11 pre-commit: Enable mypy
To ease migration of various commands from OSC to SDK.

Change-Id: I4645237e8808239e4d605f7f45138449c9439949
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
2025-04-01 13:14:47 +01:00
Stephen Finucane
62c8b8217e typing: Indicate another tuple to be extended
One has been introduced since Ie5907de8d60f2f39e98f6a88227cebb2e2ff565c
merged.

Change-Id: I37f7bf58a2cbecb69b370e832e56daa310cea3b6
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
2025-04-01 13:14:47 +01:00
Zuul
c9e4e5404f Merge "Permit use of tuple API_VERSIONS" 2025-04-01 12:02:24 +00:00
Zuul
77143f9bed Merge "typing: Resolve incompatible operand issues" 2025-04-01 11:50:55 +00:00
Zuul
28bd00a642 Merge "typing: Correct type for missing attributes" 2025-04-01 11:50:53 +00:00
Zuul
e6ae7d8533 Merge "typing: Remove use of optional imports" 2025-04-01 11:50:51 +00:00
Zuul
93da5f7af5 Merge "typing: Use consistent types" 2025-04-01 11:50:49 +00:00
Zuul
6e89f9da63 Merge "identity: Migrate 'endpoint' commands to SDK" 2025-04-01 10:55:58 +00:00
Zuul
514a46ee5d Merge "Remove tags from README" 2025-04-01 10:48:12 +00:00
Zuul
cf25526e94 Merge "Return the `port` column headers expected in the list command" 2025-04-01 10:48:10 +00:00
Stephen Finucane
662405e55c docs: Remove irrelevant TODO
cue is a dead project.

Change-Id: Ie860312a9ea481741bf5e7ab29610f621daba702
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
2025-04-01 10:46:06 +01:00
Stephen Finucane
e6be9a3edf volume: Remove Cinder v1 support
The Cinder v1 API was removed in Queens [1]. Its replacement, the v2
API, has existed since Grizzly [2]. More importantly, the v1 commands
are implemented using python-cinderclient but support for the v1 API was
removed from python-cinderclient in Train [3], meaning none of these
have worked since then. Clearly if no one has noticed or cared in the 6
years or so since that happened, it's safe to say we can delete these
commands.

[1] 3e91de956e
[2] 75ca60f619
[3] 2189e5702b

Change-Id: Ibe1cd6461d2cb78826467078aa17272f171746aa
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
2025-04-01 10:46:03 +01:00
Stephen Finucane
3eb063d4f7 docs: Migrate remaining block storage commands to autocommand
Change-Id: I6924f7053e14e843420deea1a023201fad7d4999
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
2025-04-01 10:45:32 +01:00
Stephen Finucane
0b9c998d8a Remove contributor specs
There is only one and it was never implemented. Remove the directory.

Change-Id: Ibfffe7936556a626b407deca0e8de17b45db5313
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
2025-04-01 10:45:32 +01:00
Stephen Finucane
b2eccdcb1a Permit use of tuple API_VERSIONS
The values of these dictionaries are not used when SDK is in use,
which should soon account for all use cases. Eventually we should
probably look for plugins to return a proper class or typeddict but
that's a job for another day.

This began as a fix for in openstackclient/object/client.py which
referenced a non-existent class and quickly snowballed.

Change-Id: I7b807ec3a97124b35828ffdecbb36f6fde11e7b5
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
2025-04-01 10:36:02 +01:00
Antonia Gaete
7750fc1cf4 identity: Migrate 'endpoint' commands to SDK
Change-Id: I71b5ae8a4bbcb2fdebf894d8bd5cc8322c31bdb5
Depends-On: I599ff3e88d4e1e9ffafc638bb74186f2739b5a77
Depends-On: I9aa39810fe94f7ee9b68d34050f4adb9dbdfccb8
2025-03-31 16:58:27 -07:00
Stephen Finucane
9de592ebaf typing: Resolve incompatible operand issues
Change-Id: I7f3dd908053b9ace5206d0a1bd3b8258fd0264ef
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
2025-03-31 17:52:16 +01:00
Stephen Finucane
e28046cc19 typing: Correct type for missing attributes
Change-Id: I55652220ecd663fa024937646dfef92595e1cd0f
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
2025-03-31 17:52:16 +01:00
Stephen Finucane
9435ef825a typing: Remove use of optional imports
Do them inline instead.

Change-Id: Icab1a0452249efc79f214c4d7b369d02291e94b4
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
2025-03-31 17:48:36 +01:00
Stephen Finucane
2c0a3ba137 typing: Use consistent types
Resolve 'Incompatible types in assignment' errors.

Change-Id: I1ea186ff766e0f72cac384fab22d1c2f82e02ef0
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
2025-03-31 17:48:36 +01:00
Stephen Finucane
7380fbe300 typing: Add types for empty dicts, tuples
In some cases, simply remove them.

Change-Id: I24a311a24eb533325dda83005777bcb2e0afc320
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
2025-03-31 17:48:36 +01:00
Stephen Finucane
bdd55d989d typing: Indicate tuples to be extended
Change-Id: Ie5907de8d60f2f39e98f6a88227cebb2e2ff565c
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
2025-03-31 17:48:36 +01:00
Stephen Finucane
9c7a5d4e51 pre-commit: Bump versions
Apply manual changes required by ruff. Automatic changes were done
separately.

Change-Id: I7db65bd2ac3f31b0479699946398752d8d729338
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
2025-03-31 17:48:31 +01:00
Zuul
ab2e68f407 Merge "Prepare for ruff bump" 2025-03-31 10:37:30 +00:00
Zuul
5918b6b478 Merge "[Neutron] Add "qos-policy" parameter to router creation command" 2025-03-28 19:27:30 +00:00
Brian Haley
107c6b143f Fix neutron typos and formatting
Just trying to make things consistent in the neutron files.

TrivialFix

Change-Id: I9c0ac838f5a956f55161e1636472cca761b14781
2025-03-26 20:02:25 -04:00
Rodolfo Alonso Hernandez
4dbfc47552 Return the `port` column headers expected in the list command
In [1], it was added the ability to print in the "port list" command
any field not defined in the hardcoded column set for this command.

But in [2], it was added a filter list in the API call in order to
reduce the CLI execution time. The unintentional drawback of this
optimization was that is no longer possible to print any field outside
the "port list" column set.

Because the optimization if preferred and it is always possible to use
"port show" to see all the port fields, the code added in [1] is
removed.

[1]https://review.opendev.org/c/openstack/python-openstackclient/+/522901
[2]https://review.opendev.org/c/openstack/python-openstackclient/+/754117

Closes-Bug: #2098980
Related-Bug: #1707848
Related-Bug: #1897100
Change-Id: Ia944b8e108c454219d642cfa595ffafdf060a57f
2025-03-26 15:57:12 +00:00
Jan Ueberacker
3483117259 Add filters to search for enabled/disabled users and projects
Change-Id: Ie7d84f9e0158018083af2156d02dc86fefd79256
Signed-off-by: Jan Ueberacker <jan.ueberacker@inovex.de>
2025-03-26 14:41:50 +01:00
Rodolfo Alonso Hernandez
07515cd160 [Neutron] Add "qos-policy" parameter to router creation command
This patch adds the parameter "qos-policy" to the router creation
command.

Closes-Bug: #2103774
Change-Id: I742b3273c5e9d3ec16e8018beddc8cdace8a57c6
2025-03-26 07:46:40 +00:00
Zuul
866009211f Merge "Fix networking quota usage show" 2025-03-25 13:13:29 +00:00
Slawek Kaplonski
6d27b2f2b6 Fix networking quota usage show
Quotas details returned from the Neutron service are in different format
then quota details returned from Nova and Cinder services. This patch
fixes helper function to convert data from Neutron to the same
format as data from Nova and Cinder is given.

Closes-Bug: #2102513
Change-Id: I18649f6c2ee179b64b7e605f4ea07d4b0c7a1635
2025-03-25 09:52:39 +01:00
Alfredo Moralejo
2e5a830276 Replace description-content-type by its underscore name
Since v78.0.0, setuptools no longer accepts options containing
uppercase or dash characters in setup.cfg [1].

[1] https://github.com/pypa/setuptools/blob/main/NEWS.rst#v7800

Closes-Bug: #2104030

Change-Id: Id88b9c73a4cd9511750f38da9393dae3adbc5c1e
2025-03-24 17:03:44 +01:00
Zuul
b01c138e9e Merge "Specifying project-domain for project" 2025-03-24 13:04:53 +00:00
Dmitriy Chubinidze
2883f3fb95 Specifying project-domain for project
The fix ensures that if a user wants to set a default project,
they must also provide the project domain. If it's missing,
an explicit error message is shown, making it clear that the
project domain is required.

Also adding some unit tests by modifying respective calls.

Change-Id: Ia6e921a53da55ab1bce85a42c8160872a9d47d64
Closes-Bug: #2102146
2025-03-23 09:38:24 +00:00
Zuul
f28f97f23f Merge "zuul: Make image job non-voting" 2025-03-21 17:16:02 +00:00
Zuul
9366405806 Merge "Update master for stable/2025.1" 2025-03-21 15:18:38 +00:00
Stephen Finucane
7ef588d695 zuul: Make image job non-voting
We may need to remove this soon enough, given the new Docker rate limits
that we keep bumping into.

Change-Id: Id4a9d8df770d107986b20e4a98835ee4e0b6117d
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
2025-03-21 15:03:44 +00:00
Zuul
71dbac498e Merge "Fix missing space in help messages" 2025-03-19 13:11:46 +00:00
Zuul
669a50be55 Merge "Update README for use python3" 2025-03-19 13:06:39 +00:00
Zuul
1763c11963 Merge "Fix image import --disallow-failure flag" 2025-03-19 13:06:38 +00:00
Stephen Finucane
49708f6d3f Remove tags from README
The tags framework has been discontinued [1].

[1] https://governance.openstack.org/tc/reference/tags/index.html

Change-Id: Ife108e6ae191641b56e872e4616a3f4ec78281e8
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
2025-03-19 12:33:52 +00:00
Stephen Finucane
290bc580e6 Prepare for ruff bump
Change-Id: Ia9c402edebc8537d5019d18920b6679b05ea4378
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
2025-03-19 12:04:39 +00:00
OpenStack Release Bot
f2df31387b Update master for stable/2025.1
Add file to the reno documentation build to show release notes for
stable/2025.1.

Use pbr instruction to increment the minor version number
automatically so that master versions are higher than the versions on
stable/2025.1.

Sem-Ver: feature
Change-Id: I2789e8605f5e9bae63382ca9e822bbc3e2241f36
2025-03-18 09:01:53 +00:00
Ghanshyam Mann
702a37c7ca Add libpcre3-dev in bindep.txt for pcre.h
Doc job is going to run on Ubuntu Noble[1]
and we need libpcre3-dev package for pcre.h

[1] https://review.opendev.org/c/openstack/openstack-zuul-jobs/+/935459

Change-Id: I0fe73c02b093805d8eb1b15303f92633fad809cb
2025-03-17 09:23:09 -07:00
Pavlo Shchelokovskyy
1efca54465 Fix image import --disallow-failure flag
the flag should store False to 'allow_failure', not True.

Also, make the --allow-failure and --disallow-failure flags
mutually exclusive.

Change-Id: I03699e14d4d69d9f08caab647293732fc211dbad
2025-03-14 10:52:39 +00:00
Tobias Urdin
f1bd417861 Add device ID and device owner to port unset
This adds support to unset the device_id and
device_owner property on a port.

Change-Id: I43b1ea63e3a119f57162948e128a85f8ba323d41
2025-03-12 19:39:03 +00:00
Vladimir Kozhukalov
1458330d3b identity: Fix 'trust' commands to work with SDK
Closes-Bug: #2102039
Change-Id: I632937e06683cc76e78390a4e6f3de4e3c4f1f87
2025-03-11 12:31:09 -05:00
Rajesh Tailor
f65e4835d3 Fix missing space in help messages
This change fixes missing space in help messages to make those
consistent and pretty rendering when calling help.

Change-Id: I947374821a4dbb5e68651c0e72fc5fd2f938e6a1
2025-03-05 19:58:27 +05:30
Zuul
966aede8ab Merge "Add four new network agent types to the list command filter" 2025-02-28 01:43:25 +00:00
Zuul
4b7e32ca37 Merge "handle 'router create --flavor' option" 2025-02-25 22:48:45 +00:00
Tim Burke
1979c20ff0 Fix credential creation
openstacksdk's Credential expects user_id and project_id, not user and
project. Previously, we would send payloads like

   {'type': 'ec2', 'blob': '{"access": "s3-user1", "secret": "s3-secret1"}'}

which Keystone would reject with

   'user_id' is a required property

Change-Id: I0544bef7df9247395f0726ea075112d6ac992252
2025-02-25 10:30:22 -08:00
Rodolfo Alonso Hernandez
762a3b10d1 Add four new network agent types to the list command filter
Added four new network agent types to the list method filter:
* ``ovn-controller``
* ``ovn-controller-gateway``
* ``ovn-metadata``
* ``ovn-agent``

These agents are represented in the OVN network agent classes defined
in [1]. The OVN agent names are defined in [2].

[1]86f94de99a/neutron/plugins/ml2/drivers/ovn/agent/neutron_agent.py
[2]86f94de99a/neutron/common/ovn/constants.py (L91-L94)

Closes-Bug: #2097124
Change-Id: I117be7e60f67fdd94677cbaa65a3aff01e57bb5e
2025-02-24 10:58:48 +00:00
Zuul
d22b7732ad Merge "[Neutron] Support `uplink-status-propagation-updatable` extension" 2025-02-24 09:05:05 +00:00
Zuul
12e264adc6 Merge "[Neutron] "uplink-status-propagation" enabled by default" 2025-02-24 08:53:02 +00:00
Rodolfo Alonso Hernandez
0ba77e6727 [Neutron] Fix the "port show" command for trunk details
In [1], the "port list --long" command received a new column, showing
the trunk subports related to a parent port. The problem of this patch
is that the ``_formatters`` definition, that is shared with the "port
show" command too, is changed. The "trunk_details" information presented
in both commands differ:
* In the "port list" command, only the subports are presented, in order
  to print a list of dictionaries without showing the ``trunk_id``.
* In the "port show" command, it is presented all the trunk information,
  including the ``trunk_id``.

This patch includes functional tests to validate the fix.

[1]https://review.opendev.org/c/openstack/python-openstackclient/+/926611

Closes-Bug: #2098950
Change-Id: Ib1107fb3dbb025b39a7c55f90f5fe51ae433a72f
2025-02-20 01:07:39 +00:00
Rodolfo Alonso Hernandez
426abbdc68 [Neutron] Support `uplink-status-propagation-updatable` extension
Added ``--enable-uplink-status-propagation`` option and
``--disable-uplink-status-propagation`` option to ``port update``
command.

Now the Neutron extension "uplink-status-propagation-updatable" allows
to update the related value in a port. That was implemented in the
following patches (during 2025.1 Epoxy release):
* https://review.opendev.org/c/openstack/neutron-lib/+/927820
* https://review.opendev.org/c/openstack/neutron-lib/+/936234
* https://review.opendev.org/c/openstack/neutron/+/931641

Related-Bug: #1722720
Change-Id: I99cdcf21438d6d85092c995b50cb10b26ae7c059
2025-02-18 12:09:37 +00:00
Doug Goldstein
1f407afe1c
handle 'router create --flavor' option
The '--flavor' option appears in the usage and arglist but is not
actually parsed. The '--flavor-id' option is what is silently parsed.
Since the goal is to allow the name or the id, this adds the '--flavor'
option to being parsed.

Closes-Bug: 2091731
Change-Id: Id83facd3825f472e7d864427699bd072d1c08779
Signed-off-by: Doug Goldstein <cardoe@cardoe.com>
2025-02-15 14:04:28 -06:00
Rodolfo Alonso Hernandez
b50ac8d2a2 [Neutron] "uplink-status-propagation" enabled by default
Since [1][2], the port flag "uplink-status-propagation" is enabled
by default.

[1]https://review.opendev.org/c/openstack/neutron-lib/+/744208
[2]https://review.opendev.org/c/openstack/neutron/+/744210

Related-Bug: #1888487
Change-Id: I522707b36c73b3c5bfe0d644bd07774918660b68
2025-02-14 14:13:26 +00:00
Slawek Kaplonski
3412147372 Add "qinq-vlan" and "no-qinq-vlan" params to the "network create" cmd
Depends-On: https://review.opendev.org/c/openstack/openstacksdk/+/939703

Related-Bug: #1915151
Change-Id: Icacf83c20c3650a9d75f665f020b8818e1b4a585
2025-02-13 10:10:42 +01:00
Slawek Kaplonski
e761ef8e32 Temporary ignore "is_vlan_qinq" column in the output
We need such temporary workaround until [1] in SDK will be merged, as
without that change here py{39,312}-tips jobs are failing on that SDK
patch.

[1] https://review.opendev.org/c/openstack/openstacksdk/+/939703

Related-bug: #1915151
Change-Id: Id39a6482de54fe78e26fa33c9252253886cf1f3d
2025-02-12 16:40:37 +01:00
Zuul
0c2dee5e1f Merge "Identity: Deprecate --region None in limits/registered limits" 2025-02-12 00:22:23 +00:00
Zuul
e27fd93226 Merge "Identity: Migrate 'role' commands to SDK" 2025-02-11 18:10:57 +00:00
Zuul
5de803b39f Merge "Show final image state after image create" 2025-02-10 11:57:11 +00:00
ArtofBugs
060d706bf4 Identity: Deprecate --region None in limits/registered limits
Change-Id: I37afac1185595216e868202c861d3de719b32073
2025-01-28 17:19:05 +00:00
Ivan Anfimov
3de1ac66e0 Update README for use python3
And small change for `long_description_content_type` missing.

Change-Id: I042a319bfc5009ce625565effa7ccf634222be28
2025-01-26 11:27:13 +00:00
Fernando Royo
d2d7219231 Get "security_groups" when port list
Neutron API is accepting 'security_groups' field  in
order to return the list of security_groups attached
to a port, but openstackclient is parsing the output
over a Openstack Port object that has security_group_ids
to map. This patch sends to the Neutron API the expected
field value and replace the output key to allow the
mapping just in case '--long' argument is passed.

Closes-Bug: #2095414
Change-Id: I188edc3c620ce29d7b16497ca24fd7d972a06618
2025-01-21 18:51:32 +00:00
Zuul
146a1814b6 Merge "Add the trunk subports information to the port list command" 2025-01-18 12:01:38 +00:00
Pavlo Shchelokovskyy
4f95e0aa18 Show final image state after image create
creating the image is a 2step process, first an 'empty' image is created
and then the data is uploaded.
Currently the output of the 'image create' command is that 'empty'
image, in `queued` status etc.
A more user friendly approach would be to make a second refresh call
to show the user image as it is after data was uploaded.

Change-Id: I2f78b113dcc3c941f8cf8dd9b63262971a780a39
2025-01-16 15:35:43 +00:00
ArtofBugs
8f1382eda3 Identity: Migrate 'role' commands to SDK
Depends-On: https://review.opendev.org/c/openstack/openstacksdk/+/929578
Change-Id: I22254604705080095ac852a1e03506b1552a1fd2
2025-01-13 21:55:10 +00:00
Zuul
eb0dbd5c33 Merge "Fix: Volume backup restore output" 2025-01-07 18:35:53 +00:00
Zuul
6ce1f7730c Merge "identity: Migrate 'trust' commands to SDK" 2024-12-19 21:33:28 +00:00
Rodolfo Alonso Hernandez
2a0431e825 Add the trunk subports information to the port list command
Added the subports information to the port list command, when the
"--long" qualifier is specified.

Depends-On: https://review.opendev.org/c/openstack/openstacksdk/+/926609

Closes-Bug: #2074187
Change-Id: I8ef66c3415279caf0ebea4ba6232ca3696188de9
2024-12-18 15:04:54 +01:00
Zuul
a631014551 Merge "Fix volume backup show by name" 2024-12-17 19:09:39 +00:00
Zuul
4e8be5aa64 Merge "tests: Stop setting attributes on class" 2024-12-17 18:47:00 +00:00
Rajat Dhasmana
03e2fdd162 Fix: Volume backup restore output
Currently the volume backup restore command returns with error
even though the restore is initiated.
This patch corrects the response received from SDK and processes
it in a human readable form.

Change-Id: I7f020631fbb39ceef8740775fd82686d90a6c703
Closes-Bug: #2063335
Depends-On: https://review.opendev.org/c/openstack/openstacksdk/+/931755
2024-12-17 17:46:35 +00:00
Zuul
c74af3f01e Merge "identity: Migrate 'credential' commands to SDK" 2024-12-16 20:45:20 +00:00
Zuul
fd232a43bc Merge "identity: Migrate 'service provider' commands to SDK" 2024-12-16 19:52:03 +00:00
Antonia Gaete
769bf87d0a identity: Migrate 'trust' commands to SDK
Change-Id: Idb1fda3428ccf3022ee03c8fb7e42c7121683181
2024-12-16 18:24:10 +00:00
Stephen Finucane
38407c6a78 tests: Stop setting attributes on class
For some reason we were setting a property mock on the FakeClientManager
class. In multiple places, no less. This has a nasty habit of causing
side-effects in other tests, depending on the order that tests run in.
Resolve this simply setting the attribute as we'd expect.

Change-Id: I8bf9055e3f5b885dd5a7a6d751b774934da4a7d7
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
2024-12-16 18:23:09 +00:00
Zuul
db3d4c98f2 Merge "requirements: Remove unused test deps" 2024-12-16 18:07:15 +00:00
Stephen Finucane
32bd5d3562 Adopt sdk_fakes for compute.test_server_volume
Change-Id: I5a82ff970ebb3622e6920cdb240a3c1dbc96e27d
2024-12-12 11:19:27 +00:00
Artem Goncharov
4cef5f5549 Adopt sdk_fakes for compute.test_server_group
Change-Id: I1c97a7b2e28233a3b345a81b62c01e74a0aec914
2024-12-12 11:19:20 +00:00
Stephen Finucane
38029c6988 Adopt sdk_fakes for compute.test_hypervisor
Change-Id: Ibed4390be61c98f8c9e348835493bc714f8b9e87
2024-12-12 11:19:02 +00:00
Artem Goncharov
776b7d0c66 Adopt sdk_fakes for compute.test_usage
Depends-On: https://review.opendev.org/c/openstack/openstacksdk/+/882682
Change-Id: Ia8fd2ccfd1e86749fdeeb49c2d57de64635cbb82
2024-12-12 11:18:56 +00:00
Stephen Finucane
afc0d3c252 Adopt sdk_fakes for compute.test_service
Change-Id: I331283f42914b91bd80dfca354635214fb7ba8a7
2024-12-12 11:18:49 +00:00
Artem Goncharov
42b1698e5c Adopt sdk_fakes for compute.test_keypair
Change-Id: Ifb5df852a9ae6eea3fabce13d450b16cb8348315
2024-12-12 11:18:42 +00:00
Artem Goncharov
d175dea6bc Adopt sdk_fakes for compute.test_console
Change-Id: I6181009a8b2ae7e632214364527ccc6fdd03fff9
2024-12-12 11:18:34 +00:00
Stephen Finucane
329b351cb8 Adopt sdk_fakes for compute.aggregate
Drop fakes generation for compute aggregates in favor of sdk_fakes

Change-Id: I4965a5fe8fc3d70390ca0268716519b617ca24eb
2024-12-12 10:43:24 +00:00
Antonia Gaete
56baf50655 identity: Migrate 'service provider' commands to SDK
Change-Id: I7f5fba408b7c350bb0a279f8dd17bd7bae451774
2024-12-11 22:37:44 +00:00
Zuul
52b2944b78 Merge "identity: Migrate region commands to SDK" 2024-12-10 18:32:38 +00:00
Zuul
90148ffc45 Merge "quota: Catch correct exception type for Compute quotas" 2024-12-10 18:32:36 +00:00
Zuul
6579daeac7 Merge "tests: Add functional test for adding, removing SGs" 2024-12-10 14:03:51 +00:00
Zuul
83de58fa33 Merge "compute: Workaround bug #2089821" 2024-12-10 14:03:49 +00:00
Stephen Finucane
99cef9354b quota: Catch correct exception type for Compute quotas
There is a flaw (IMO) in the design of Nova's os-quota-sets API: despite
project IDs forming the identifier for an individual resource, we get a
HTTP 400 (Bad Request) error if you pass an ID that does not exist,
rather than the HTTP 404 (Not Found) we would expect.

Correct this, noting why we're doing what we're doing for readers from
the future (hi!). Note that HTTP 400 is unfortunately quite broad and
means we'll also catch things like invalid requests but the exception
may have been translated so we can't rely on a string match.

Change-Id: I720502930d50be8ead5f2033d9dbcab5d99a37a9
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
Closes-bug: #2091086
2024-12-09 15:49:40 +00:00
Stephen Finucane
e736394d1b tests: Add functional test for adding, removing SGs
The fix is in openstacksdk. Let's test it here though.

Change-Id: I661e6d66c8196e8c9ca8b9cda3d08e756e3d5877
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
Depends-on: https://review.opendev.org/c/openstack/openstacksdk/+/936947
Related-bug: #2089821
2024-12-09 13:56:58 +00:00
Stephen Finucane
22b30b99ce compute: Workaround bug #2089821
By passing a dict instead of a single value, we force SDK to populate
the correct attribute on the object.

Change-Id: I9f4c5964dc0546215474c92db567966ffad68a1a
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
Related-bug: #2089821
2024-12-09 13:56:58 +00:00
Rajat Dhasmana
03933e9a73 Fix: extend in-use volumes check
Currently we have 2 issues with extending volumes checks:

1. We don't specify explicitly that MV 3.42 needs to be passed for
in-use volumes
2. Any state of volume (error, attaching, detaching etc) can pass
this check by specifying MV 3.42

The fundamentally correct approach to these checks should be:

1. Only allow 'available' and 'in-use' volumes to be extended
2. Check MV 3.42 or greater is specified in case of 'in-use' volumes
otherwise fail

This approach is implemented in the patch.

Change-Id: I45ab9af953f7d060379f48ca429eaea7cfe857cc
2024-12-09 19:01:22 +05:30
Stephen Finucane
c888cf2556 network: Make better use of argparse
Change-Id: I7421a0ab957412a8283eee6ae9783dac9d3f6a4a
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
2024-11-29 10:33:36 +00:00
Stephen Finucane
5ef5cc9c82 compute: Add server create --no-security-group option
To allow users to create servers with no security groups associated with
the ports.

Change-Id: I91b1d9dd5c3fbba838640841d98341cd8ccb1b16
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
2024-11-29 10:33:18 +00:00
Takashi Kajinami
ecc744a4fd Remove ceilometer service overrides
devstack no longer installs ceilometer services unlesss ceilometer
devstack plugin is enabled, so we can safely remove the options to
disable ceilometer services.

This allows us to remove references to removed services such as
ceilometer-api .

Change-Id: I4201878d0deba4490cf2a08bbabec8fe64474385
2024-11-25 00:41:46 +09:00
Antonia Gaete
4bdd51cbea identity: Migrate 'domain' commands to SDK
Change-Id: Ide9cb9491334e139482f8cf1ea1874d01da0884f
Depends-On: Id1b7b00fe5b96f0cc922716afabcc678193f0f57
2024-11-20 23:28:47 +00:00
Antonia Gaete
4c8290012d identity: Migrate region commands to SDK
Change-Id: I980693732d794f1ccbfc8d7f06d61b4a9824ef15
2024-11-20 23:26:30 +00:00
Zuul
6ff3a92089 Merge "Show Created At column for volume backups in v3" 2024-11-20 16:38:57 +00:00
OpenStack Release Bot
8f56d3f5e4 reno: Update master for unmaintained/2023.1
Update the 2023.1 release notes configuration to build from
unmaintained/2023.1.

Change-Id: Id29f0413825372df296cc123d63cc092fbba1821
2024-11-14 10:17:59 +00:00
Antonia Gaete
9c6df823e2 identity: Migrate 'credential' commands to SDK
Change-Id: I49391fec3d7b6a1b81438a2a311ac7b86173a6a4
2024-11-13 18:13:19 +00:00
Takashi Natsume
fb1f841d2d Replace deprecated datetime.utcnow()
The datetime.utcnow() is deprecated in Python 3.12.
Replace datetime.utcnow() with
datetime.now(datetime.timezone.utc).replace(tzinfo=None).

Change-Id: Ic20174a9c6cacac05471fa57b105c1f784a73057
Signed-off-by: Takashi Natsume <takanattie@gmail.com>
2024-11-13 15:48:59 +00:00
Stephen Finucane
58e21d8e2b requirements: Remove unused test deps
oslotest is not used anywhere, while requests is already a runtime dep.

Change-Id: I852b7d8664cddf22b1314b057c42930580a1f9f4
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
2024-11-13 12:14:04 +00:00
Stephen Finucane
8890981491 Skip tips jobs on pre-commit config update
We only need to care about the unversioned tips jobs since [1] will take
care of the others.

[1] https://review.opendev.org/c/openstack/openstack-zuul-jobs/+/933197

Change-Id: I3d569dc496a995eee58fdbcf4a42a187143d1b24
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
2024-11-12 14:10:50 +00:00
Zuul
79e01a5533 Merge "Add "trusted" attribute to the "port"" 2024-11-08 17:32:37 +00:00
Zuul
d17d99faa9 Merge "common: Use correct argument for volume limits" 2024-11-05 20:38:15 +00:00
Stephen Finucane
e5ccf1eb1c common: Use correct argument for volume limits
The sooner we have type hints in SDK, the better /o\

Change-Id: Iaf9596aea02f683c280ae68504a14d43dbd6134a
Closes-bug: #2077634
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
2024-11-05 17:07:49 +00:00
Zuul
974cdd9a4e Merge "Fix ignored --user-domain in role assignment list" 2024-11-05 17:01:54 +00:00
Slawek Kaplonski
47144103ca Add "trusted" attribute to the "port"
Depends-On: https://review.opendev.org/c/openstack/openstacksdk/+/927723

Related-bug: #2060916
Change-Id: I8e3d4ee2208ef6bb6c96ee430d7b550a0720431e
2024-11-04 09:12:43 +00:00
Takashi Kajinami
2e491191e5 Fix ignored --user-domain in role assignment list
Fix the wrong value assignment which made the --user-domain option
ignored. Unit tests are updated to verify usage of domain options to
avoid further regressions.

Also drop the redundant look up of domain id to avoid unnecessary API
call.

Closes-Bug: #2085604
Change-Id: I5112b8e831fb26eb6544615277f0d3fe4f15dc5a
2024-10-29 21:20:07 +09:00
Tobias Urdin
695d025f00 Show Created At column for volume backups in v3
The change in [1] erroneously only added the
created_at column for volume v2 API and not
also for the volume v3 API.

[1] https://review.opendev.org/c/openstack/python-openstackclient/+/920003

Change-Id: Iae0b4f21cbc31ae7464a79c8f5e01446ca4ff098
2024-10-17 16:25:13 +02:00
Yosef Salmalian
db115c09a2 remove project from network flavor profile
Removing project from network_flavor_profile as neutron api
does not uses project.

Closes-Bug: 2046496
Change-Id: I77b0544cf8629fb0a5b9914361a007d28b2b5662
2024-10-16 10:19:59 +03:30
Stephen Finucane
7c6b47b451 clientmanager: Check for 'block-storage' service type
This is a fun one driven by two separate changes. We recently started
checking whether the volume service was available before setting quotas
in order to allow us to use quota set for other services [1]. This
merged a number of weeks ago and was included in 7.1.0. More recently,
we modified DevStack to stop publishing a service catalog entry with a
service type of 'volumev3', preferring instead to use the correct
'block-storage' service type. Taken separately, neither of these changes
would have caused issues. Together, they mean our lookups for the volume
service now fail and we can't set volume quotas.

Fix things by checking for the block-storage service type also. A future
change will raise a warning (later an error) if the volume service is
not found and you're attempting to set a quota since this is clearly a
mistake.

Change-Id: Ibbeef52225e18757cd28d0fbfb14c1ca06975b60
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
Closes-bug: #2084580
2024-10-15 18:07:26 +01:00
Rajat Dhasmana
b6b18489b0 Fix volume backup show by name
When we show a volume backup by name, it calls the get_backup
method in SDK which is only used for getting a backup by ID.
This patch modifies the approach to call find_backup method
which first tries the find by ID and then find by name logic
eventually returning the backup details.

Story: 2011234
Task: 51127
Change-Id: I926d8de9810fcf2e5335bbe35aaab15e1e36a5cb
2024-10-07 13:25:14 +00:00
419 changed files with 19118 additions and 23017 deletions

View file

@ -1,13 +1,13 @@
--- ---
repos: repos:
- repo: https://github.com/pre-commit/pre-commit-hooks - repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0 rev: v6.0.0
hooks: hooks:
- id: trailing-whitespace - id: trailing-whitespace
- id: mixed-line-ending - id: mixed-line-ending
args: ['--fix', 'lf'] args: ['--fix', 'lf']
exclude: '.*\.(svg)$' exclude: '.*\.(svg)$'
- id: check-byte-order-marker - id: fix-byte-order-marker
- id: check-executables-have-shebangs - id: check-executables-have-shebangs
- id: check-merge-conflict - id: check-merge-conflict
- id: debug-statements - id: debug-statements
@ -15,9 +15,9 @@ repos:
files: .*\.(yaml|yml)$ files: .*\.(yaml|yml)$
args: ['--unsafe'] args: ['--unsafe']
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.6.2 rev: v0.14.0
hooks: hooks:
- id: ruff - id: ruff-check
args: ['--fix', '--unsafe-fixes'] args: ['--fix', '--unsafe-fixes']
- id: ruff-format - id: ruff-format
- repo: https://opendev.org/openstack/hacking - repo: https://opendev.org/openstack/hacking
@ -26,3 +26,17 @@ repos:
- id: hacking - id: hacking
additional_dependencies: [] additional_dependencies: []
exclude: '^(doc|releasenotes)/.*$' exclude: '^(doc|releasenotes)/.*$'
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.18.2
hooks:
- id: mypy
additional_dependencies:
- types-requests
# keep this in-sync with '[tool.mypy] exclude' in 'pyproject.toml'
exclude: |
(?x)(
doc/.*
| examples/.*
| hacking/.*
| releasenotes/.*
)

View file

@ -6,6 +6,11 @@
Run unit tests for OpenStackClient with master branch of important libs. Run unit tests for OpenStackClient with master branch of important libs.
Takes advantage of the base tox job's install-siblings feature. Takes advantage of the base tox job's install-siblings feature.
irrelevant-files: &common-irrelevant-files
- ^.*\.rst$
- ^doc/.*$
- ^releasenotes/.*$
- ^\.pre-commit-config\.yaml$
required-projects: required-projects:
- openstack/cliff - openstack/cliff
- openstack/keystoneauth - openstack/keystoneauth
@ -18,8 +23,8 @@
zuul_work_dir: src/opendev.org/openstack/python-openstackclient zuul_work_dir: src/opendev.org/openstack/python-openstackclient
- job: - job:
name: osc-tox-py39-tips name: osc-tox-py310-tips
parent: openstack-tox-py39 parent: openstack-tox-py310
description: | description: |
Run unit tests for OpenStackClient with master branch of important libs. Run unit tests for OpenStackClient with master branch of important libs.
@ -38,8 +43,8 @@
zuul_work_dir: src/opendev.org/openstack/python-openstackclient zuul_work_dir: src/opendev.org/openstack/python-openstackclient
- job: - job:
name: osc-tox-py312-tips name: osc-tox-py313-tips
parent: openstack-tox-py312 parent: openstack-tox-py313
description: | description: |
Run unit tests for OpenStackClient with master branch of important libs. Run unit tests for OpenStackClient with master branch of important libs.
@ -81,13 +86,6 @@
# NOTE(amotoki): Some neutron features are enabled by devstack plugin # NOTE(amotoki): Some neutron features are enabled by devstack plugin
neutron: https://opendev.org/openstack/neutron neutron: https://opendev.org/openstack/neutron
devstack_services: devstack_services:
ceilometer-acentral: false
ceilometer-acompute: false
ceilometer-alarm-evaluator: false
ceilometer-alarm-notifier: false
ceilometer-anotification: false
ceilometer-api: false
ceilometer-collector: false
s-account: true s-account: true
s-container: true s-container: true
s-object: true s-object: true
@ -139,22 +137,6 @@
tox_envlist: functional tox_envlist: functional
tox_install_siblings: true tox_install_siblings: true
- secret:
name: osc-dockerhub
data:
username: osclientzuul
password: !encrypted/pkcs1-oaep
- LbIZjJiVstRVXMpoLQ3+/JcNB6lKVUWJXXo5+Outf+PKAaO7mNnv8XLiFMKnJ6ftopLyu
hWbX9rA+NddvplLQkf1xxkh7QBBU8PToLr58quI2SENUclt4tpjxbZfZu451kFSNJvNvR
E58cHHpfJZpyRnS2htXmN/Qy24gbV2w7CQxSZD2YhlcrerD8uQ8rWEnlY1wcJEaEGomtS
ZTGxsdK2TsZC2cd4b7TG7+xbl2i+hjADzwSQAgUzlLlwuG71667+IWk4SOZ7OycJTv9NN
ZTak8+CGfiMKdmsxZ1Z8uD7DC+RIklDjMWyly6zuhWzfhOmsmU0CesR50moodRUvbK79p
NZM8u0hBex5cl2EpUEwJL/FSPJXUhDMPoMoTZT/SAuXf25R9eZ9JGrKsIAlmVhpl8ifoE
8TpPyvIHGS3YelTQjhqOX0wGb9T4ZauQCcI5Ajzy9NuCTyD9xxme9OX1zz7gMACRnVHvz
q7U7Ue90MnmGH6E2SgKjIZhyzy9Efwb7JUvH1Zb3hlrjCjEhwi9MV5FnABTEeXyYwE10s
3o/KZg2zvdWkVG6x0dEkjpoQaNuaB7T2Na7Sm421n/z3LCzhiQGuTUjENnL6cMEtuA6Pp
BfI5+Qlg7HMwkBXNB73EPfWHzbCR3VNrzGYTy9FvhGud0/cXsuBXgps4WH63ic=
- job: - job:
name: osc-build-image name: osc-build-image
parent: opendev-build-docker-image parent: opendev-build-docker-image
@ -164,49 +146,21 @@
- python-builder-3.11-bookworm-container-image - python-builder-3.11-bookworm-container-image
- python-base-3.11-bookworm-container-image - python-base-3.11-bookworm-container-image
provides: osc-container-image provides: osc-container-image
vars: &osc_image_vars vars:
docker_images: docker_images:
- context: . - context: .
repository: osclient/python-openstackclient tags: []
- job:
name: osc-upload-image
parent: opendev-upload-docker-image
description: Build Docker images and upload to Docker Hub.
allowed-projects: openstack/python-openstackclient
requires:
- python-builder-3.11-bookworm-container-image
- python-base-3.11-bookworm-container-image
provides: osc-container-image
secrets:
- name: docker_credentials
secret: osc-dockerhub
pass-to-parent: true
vars: *osc_image_vars
- job:
name: osc-promote-image
parent: opendev-promote-docker-image
allowed-projects: openstack/python-openstackclient
description: Promote previously uploaded Docker images.
secrets:
- name: docker_credentials
secret: osc-dockerhub
pass-to-parent: true
nodeset:
nodes: []
vars: *osc_image_vars
- project-template: - project-template:
name: osc-tox-unit-tips name: osc-tox-unit-tips
check: check:
jobs: jobs:
- osc-tox-py39-tips - osc-tox-py310-tips
- osc-tox-py312-tips - osc-tox-py313-tips
gate: gate:
jobs: jobs:
- osc-tox-py39-tips - osc-tox-py310-tips
- osc-tox-py312-tips - osc-tox-py313-tips
- project: - project:
templates: templates:
@ -219,7 +173,10 @@
- release-notes-jobs-python3 - release-notes-jobs-python3
check: check:
jobs: jobs:
- osc-build-image - openstackclient-check-plugins:
voting: true
- osc-build-image:
voting: false
- osc-functional-devstack - osc-functional-devstack
- osc-functional-devstack-tips: - osc-functional-devstack-tips:
# The functional-tips job only tests the latest and shouldn't be run # The functional-tips job only tests the latest and shouldn't be run
@ -227,8 +184,4 @@
branches: ^master$ branches: ^master$
gate: gate:
jobs: jobs:
- osc-upload-image
- osc-functional-devstack - osc-functional-devstack
promote:
jobs:
- osc-promote-image

View file

@ -13,12 +13,19 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
FROM docker.io/opendevorg/python-builder:3.11-bookworm as builder FROM docker.io/opendevorg/python-builder:3.12-bookworm AS builder
COPY . /tmp/src COPY . /tmp/src
RUN assemble RUN assemble
FROM docker.io/opendevorg/python-base:3.11-bookworm FROM docker.io/opendevorg/python-base:3.12-bookworm
LABEL org.opencontainers.image.title="python-openstackclient"
LABEL org.opencontainers.image.description="Client for OpenStack services."
LABEL org.opencontainers.image.licenses="Apache License 2.0"
LABEL org.opencontainers.image.url="https://www.openstack.org/"
LABEL org.opencontainers.image.documentation="https://docs.openstack.org/python-openstackclient/latest/"
LABEL org.opencontainers.image.source="https://opendev.org/openstack/python-openstackclient"
COPY --from=builder /output/ /output COPY --from=builder /output/ /output
RUN /output/install-from-bindep RUN /output/install-from-bindep

View file

@ -1,12 +1,3 @@
========================
Team and repository tags
========================
.. image:: https://governance.openstack.org/tc/badges/python-openstackclient.svg
:target: https://governance.openstack.org/tc/reference/tags/index.html
.. Change things from this point on
=============== ===============
OpenStackClient OpenStackClient
=============== ===============
@ -15,96 +6,158 @@ OpenStackClient
:target: https://pypi.org/project/python-openstackclient/ :target: https://pypi.org/project/python-openstackclient/
:alt: Latest Version :alt: Latest Version
OpenStackClient (aka OSC) is a command-line client for OpenStack that brings OpenStackClient (OSC) is a command-line client for OpenStack that brings
the command set for Compute, Identity, Image, Network, Object Store and Block the command set for Compute, Identity, Image, Network, Object Store and Block
Storage APIs together in a single shell with a uniform command structure. Storage APIs together in a single shell with a uniform command structure.
Support for additional service APIs is provided via plugins.
The primary goal is to provide a unified shell command structure and a common The primary goal is to provide a unified shell command structure and a common
language to describe operations in OpenStack. language to describe operations in OpenStack.
* `PyPi`_ - package installation
* `Online Documentation`_
* `Launchpad project`_ - bugs and feature requests
* `Blueprints`_ - feature specifications (historical only)
* `Source`_
* `Developer`_ - getting started as a developer
* `Contributing`_ - contributing code
* `Testing`_ - testing code
* IRC: #openstack-sdks on OFTC (irc.oftc.net)
* License: Apache 2.0
.. _PyPi: https://pypi.org/project/python-openstackclient
.. _Online Documentation: https://docs.openstack.org/python-openstackclient/latest/
.. _Blueprints: https://blueprints.launchpad.net/python-openstackclient
.. _`Launchpad project`: https://bugs.launchpad.net/python-openstackclient
.. _Source: https://opendev.org/openstack/python-openstackclient
.. _Developer: https://docs.openstack.org/project-team-guide/project-setup/python.html
.. _Contributing: https://docs.openstack.org/infra/manual/developers.html
.. _Testing: https://docs.openstack.org/python-openstackclient/latest/contributor/developing.html#testing
.. _Release Notes: https://docs.openstack.org/releasenotes/python-openstackclient
Getting Started Getting Started
=============== ===============
OpenStack Client can be installed from PyPI using pip:: OpenStack Client can be installed from PyPI using pip:
pip install python-openstackclient .. code-block:: shell
There are a few variants on getting help. A list of global options and supported python3 -m pip install python-openstackclient
commands is shown with ``--help``::
You can use ``--help`` or the ``help`` command to get a list of global options
and supported commands:
.. code-block:: shell
openstack --help openstack --help
There is also a ``help`` command that can be used to get help text for a specific
command::
openstack help openstack help
You can also get help for a specific command:
.. code-block:: shell
openstack server create --help
openstack help server create openstack help server create
If you want to make changes to the OpenStackClient for testing and contribution, You can add support for additional services by installing their clients. For
make any changes and then run:: example, to add support for the DNS service (designate):
python setup.py develop .. code-block:: shell
or:: python3 -m pip install python3-designateclient
pip install -e . A ``Dockerfile`` is provided for your convenience in the repository. You can
use this to build your own container images:
.. code-block:: shell
git clone https://opendev.org/openstack/python-openstackclient
cd python-openstackclient
podman build . -t example.com/myuser/openstackclient
For more information the available options and commands, refer to the `Users
Guide`__.
.. __: https://docs.openstack.org/python-openstackclient/latest/cli/index.html
Configuration Configuration
============= =============
The CLI is configured via environment variables and command-line OpenStack Client must be configured with authentication information in order to
options as listed in https://docs.openstack.org/python-openstackclient/latest/cli/authentication.html. communicate with a given OpenStack cloud. This configuration can be achieved
via a ``clouds.yaml`` file, a set of environment variables (often shared via an
``openrc`` file), a set of command-line options, or a combination of all three.
Your cloud provider or deployment tooling will typically provide either a
``clouds.yaml`` file or ``openrc`` file for you. If using a ``clouds.yaml``
file, OpenStack Client expects to find it in one of the following locations:
Authentication using username/password is most commonly used: * If set, the path indicated by the ``OS_CLIENT_CONFIG_FILE`` environment
variable
* ``.`` (the current directory)
* ``$HOME/.config/openstack``
* ``/etc/openstack``
- For a local user, your configuration will look like the one below:: The options you should set will depend on the configuration of your cloud and
the authentication mechanism(s) supported. For example, consider a cloud that
supports username/password authentication. Configuration for this cloud using a
``clouds.yaml`` file would look like so:
.. code-block:: yaml
clouds:
my-cloud:
auth:
auth_url: '<url-to-openstack-identity>'
project_name: '<project-name>'
project_domain_name: '<project-domain-name>'
username: '<username>'
user_domain_name: '<user-domain-name>'
password: '<password>' # (optional)
region_name: '<region>'
The corresponding environment variables would look very similar:
.. code-block:: shell
export OS_AUTH_URL=<url-to-openstack-identity> export OS_AUTH_URL=<url-to-openstack-identity>
export OS_IDENTITY_API_VERSION=3 export OS_REGION_NAME=<region>
export OS_PROJECT_NAME=<project-name> export OS_PROJECT_NAME=<project-name>
export OS_PROJECT_DOMAIN_NAME=<project-domain-name> export OS_PROJECT_DOMAIN_NAME=<project-domain-name>
export OS_USERNAME=<username> export OS_USERNAME=<username>
export OS_USER_DOMAIN_NAME=<user-domain-name> export OS_USER_DOMAIN_NAME=<user-domain-name>
export OS_PASSWORD=<password> # (optional) export OS_PASSWORD=<password> # (optional)
The corresponding command-line options look very similar:: Likewise, the corresponding command-line options would look very similar:
--os-auth-url <url> ::
--os-identity-api-version 3
openstack
--os-auth-url <url-to-openstack-identity>
--os-region <region>
--os-project-name <project-name> --os-project-name <project-name>
--os-project-domain-name <project-domain-name> --os-project-domain-name <project-domain-name>
--os-username <username> --os-username <username>
--os-user-domain-name <user-domain-name> --os-user-domain-name <user-domain-name>
[--os-password <password>] [--os-password <password>]
- For a federated user, your configuration will look the so:: .. note::
If a password is not provided above (in plaintext), you will be
interactively prompted to provide one securely.
Some clouds use federated authentication. If this is the case, your
configuration will be slightly more involved. For example, to configure
username/password authentication for a federated user using a ``clouds.yaml``
file:
.. code-block:: yaml
clouds:
my-cloud:
auth:
auth_url: '<url-to-openstack-identity>'
project_name: '<project-name>'
project_domain_name: '<project-domain-name>'
username: '<username-in-idp>'
user_domain_name: '<user-domain-name>'
password: '<password-in-idp>'
identity_provider: '<the-desired-idp-in-keystone>'
client_id: '<the-client-id-configured-in-the-idp>'
client_secret: '<the-client-secret-configured-in-the-idp>'
openid_scope: '<the-scopes-of-desired-attributes-to-claim-from-idp>'
protocol: '<the-protocol-used-in-the-apache2-oidc-proxy>'
access_token_type: '<the-access-token-type-used-by-your-idp>'
discovery_endpoint: '<the-well-known-endpoint-of-the-idp>'
auth_type: 'v3oidcpassword'
region_name: '<region>'
The corresponding environment variables would look very similar:
.. code-block:: shell
export OS_PROJECT_NAME=<project-name> export OS_PROJECT_NAME=<project-name>
export OS_PROJECT_DOMAIN_NAME=<project-domain-name> export OS_PROJECT_DOMAIN_NAME=<project-domain-name>
export OS_AUTH_URL=<url-to-openstack-identity> export OS_AUTH_URL=<url-to-openstack-identity>
export OS_IDENTITY_API_VERSION=3 export OS_IDENTITY_API_VERSION=3
export OS_AUTH_PLUGIN=openid
export OS_AUTH_TYPE=v3oidcpassword export OS_AUTH_TYPE=v3oidcpassword
export OS_USERNAME=<username-in-idp> export OS_USERNAME=<username-in-idp>
export OS_PASSWORD=<password-in-idp> export OS_PASSWORD=<password-in-idp>
@ -116,7 +169,9 @@ Authentication using username/password is most commonly used:
export OS_ACCESS_TOKEN_TYPE=<the-access-token-type-used-by-your-idp> export OS_ACCESS_TOKEN_TYPE=<the-access-token-type-used-by-your-idp>
export OS_DISCOVERY_ENDPOINT=<the-well-known-endpoint-of-the-idp> export OS_DISCOVERY_ENDPOINT=<the-well-known-endpoint-of-the-idp>
The corresponding command-line options look very similar:: Likewise, the corresponding command-line options would look very similar:
.. code-block:: shell
--os-project-name <project-name> --os-project-name <project-name>
--os-project-domain-name <project-domain-name> --os-project-domain-name <project-domain-name>
@ -134,5 +189,41 @@ Authentication using username/password is most commonly used:
--os-access-token-type <the-access-token-type-used-by-your-idp> --os-access-token-type <the-access-token-type-used-by-your-idp>
--os-discovery-endpoint <the-well-known-endpoint-of-the-idp> --os-discovery-endpoint <the-well-known-endpoint-of-the-idp>
If a password is not provided above (in plaintext), you will be interactively For more information on configuring authentication, including an overview of
prompted to provide one securely. the many authentication mechanisms supported, refer to the `Authentication
guide`__. For more information on configuration in general, refer to the
`Configuration guide`__.
.. __: https://docs.openstack.org/python-openstackclient/latest/cli/authentication.html.
.. __: https://docs.openstack.org/python-openstackclient/latest/configuration/index.html
Contributing
============
You can clone the repository from opendev.org::
git clone https://opendev.org/openstack/python-openstackclient
cd python-openstackclient
OpenStack Client uses the same contributor process as other OpenStack projects.
For information on this process, including help on setting up you Gerrit
account and an overview of the CI process, refer to the `OpenStack Contributors
Guide`__.
For more information on contributing to OpenStack Client itself, including
guidance on how to design new commands and how to report bugs, refer to the
`Contributors Guide`__.
.. __: https://docs.openstack.org/python-openstackclient/latest/contributor/index.html
.. __: https://docs.opendev.org/opendev/infra-manual/latest/developers.html
Links
-----
* `Issue Tracker <https://bugs.launchpad.net/python-openstackclient>`_
* `Code Review <https://review.opendev.org/#/q/status:open+project:openstack/openstacksdk,n,z>`_
* `Documentation <https://docs.openstack.org/python-openstackclient/latest/>`_
* `PyPi <https://pypi.org/project/python-openstackclient>`_
* `Mailing list <https://lists.openstack.org/mailman3/lists/openstack-discuss.lists.openstack.org/>`_
* `Release Notes <https://docs.openstack.org/releasenotes/python-openstackclient>`_
* `IRC (#openstack-sdks on OFTC (irc.oftc.net)) <irc://irc.oftc.net/openstack-sdks>`_

View file

@ -8,3 +8,4 @@ libffi-dev [compile test platform:dpkg]
libssl-dev [compile test platform:dpkg] libssl-dev [compile test platform:dpkg]
python3-dev [compile test platform:dpkg] python3-dev [compile test platform:dpkg]
python3-devel [compile test platform:rpm] python3-devel [compile test platform:rpm]
libpcre3-dev [test platform:dpkg]

View file

@ -3,7 +3,7 @@ image
===== =====
.. NOTE(efried): This page is hidden from the main TOC; it's here so links in .. NOTE(efried): This page is hidden from the main TOC; it's here so links in
the wild redirect somewhere sane, because previously identity v2 and v3 were the wild redirect somewhere sane, because previously image v2 and v3 were
combined in a single page. combined in a single page.
.. toctree:: .. toctree::

View file

@ -295,6 +295,15 @@ or, using environment variables:
$ TOKEN=$(openstack token issue -f value -c id) $ TOKEN=$(openstack token issue -f value -c id)
.. note::
The above examples assume you require a project-scoped token. You can omit
the project-related configuration if your user has a default project ID set.
Conversely, if requesting domain-scoped or system-scoped, you should update
these examples accordingly. If the user does not have a default project
configured and no scoping information is provided, the resulting token will
be unscoped.
``v3totp`` ``v3totp``
~~~~~~~~~~ ~~~~~~~~~~

View file

@ -9,53 +9,11 @@ rule comprises of a service type, a request path, and a request method. Access
rules may only be created as attributes of application credentials, but they may rules may only be created as attributes of application credentials, but they may
be viewed and deleted independently. be viewed and deleted independently.
.. autoprogram-cliff:: openstack.identity.v3
:command: access rule delete
access rule delete .. autoprogram-cliff:: openstack.identity.v3
------------------ :command: access rule list
Delete access rule(s) .. autoprogram-cliff:: openstack.identity.v3
:command: access rule show
.. program:: access rule delete
.. code:: bash
openstack access rule delete <access-rule> [<access-rule> ...]
.. describe:: <access-rule>
Access rule(s) to delete (ID)
access rule list
----------------
List access rules
.. program:: access rule list
.. code:: bash
openstack access rule list
[--user <user>]
[--user-domain <user-domain>]
.. option:: --user
User whose access rules to list (name or ID). If not provided, looks up the
current user's access rules.
.. option:: --user-domain
Domain the user belongs to (name or ID). This can be
used in case collisions between user names exist.
access rule show
---------------------------
Display access rule details
.. program:: access rule show
.. code:: bash
openstack access rule show <access-rule>
.. describe:: <access-rule>
Access rule to display (ID)

View file

@ -2,95 +2,16 @@
consistency group snapshot consistency group snapshot
========================== ==========================
Block Storage v2 Block Storage v2, v3
consistency group snapshot create .. autoprogram-cliff:: openstack.volume.v3
--------------------------------- :command: consistency group snapshot create
Create new consistency group snapshot. .. autoprogram-cliff:: openstack.volume.v3
:command: consistency group snapshot delete
.. program:: consistency group snapshot create .. autoprogram-cliff:: openstack.volume.v3
.. code:: bash :command: consistency group snapshot list
openstack consistency group snapshot create .. autoprogram-cliff:: openstack.volume.v3
[--consistency-group <consistency-group>] :command: consistency group snapshot show
[--description <description>]
[<snapshot-name>]
.. option:: --consistency-group <consistency-group>
Consistency group to snapshot (name or ID)
(default to be the same as <snapshot-name>)
.. option:: --description <description>
Description of this consistency group snapshot
.. _consistency_group_snapshot_create-snapshot-name:
.. describe:: <snapshot-name>
Name of new consistency group snapshot (default to None)
consistency group snapshot delete
---------------------------------
Delete consistency group snapshot(s)
.. program:: consistency group snapshot delete
.. code:: bash
openstack consistency group snapshot delete
<consistency-group-snapshot> [<consistency-group-snapshot> ...]
.. _consistency_group_snapshot_delete-consistency-group-snapshot:
.. describe:: <consistency-group-snapshot>
Consistency group snapshot(s) to delete (name or ID)
consistency group snapshot list
-------------------------------
List consistency group snapshots.
.. program:: consistency group snapshot list
.. code:: bash
openstack consistency group snapshot list
[--all-projects]
[--long]
[--status <status>]
[--consistency-group <consistency-group>]
.. option:: --all-projects
Show detail for all projects. Admin only.
(defaults to False)
.. option:: --long
List additional fields in output
.. option:: --status <status>
Filters results by a status
("available", "error", "creating", "deleting" or "error_deleting")
.. option:: --consistency-group <consistency-group>
Filters results by a consistency group (name or ID)
consistency group snapshot show
-------------------------------
Display consistency group snapshot details.
.. program:: consistency group snapshot show
.. code:: bash
openstack consistency group snapshot show
<consistency-group-snapshot>
.. _consistency_group_snapshot_show-consistency-group-snapshot:
.. describe:: <consistency-group-snapshot>
Consistency group snapshot to display (name or ID)

View file

@ -2,172 +2,25 @@
consistency group consistency group
================= =================
Block Storage v2 Block Storage v2, v3
consistency group add volume .. autoprogram-cliff:: openstack.volume.v3
---------------------------- :command: consistency group add volume
Add volume(s) to consistency group. .. autoprogram-cliff:: openstack.volume.v3
:command: consistency group create
.. program:: consistency group add volume .. autoprogram-cliff:: openstack.volume.v3
.. code:: bash :command: consistency group delete
openstack consistency group add volume .. autoprogram-cliff:: openstack.volume.v3
<consistency-group> :command: consistency group list
<volume> [<volume> ...]
.. _consistency_group_add_volume: .. autoprogram-cliff:: openstack.volume.v3
.. describe:: <consistency-group> :command: consistency group remove volume
Consistency group to contain <volume> (name or ID) .. autoprogram-cliff:: openstack.volume.v3
:command: consistency group set
.. describe:: <volume> .. autoprogram-cliff:: openstack.volume.v3
:command: consistency group show
Volume(s) to add to <consistency-group> (name or ID)
(repeat option to add multiple volumes)
consistency group create
------------------------
Create new consistency group.
.. program:: consistency group create
.. code:: bash
openstack consistency group create
--volume-type <volume-type> | --consistency-group-source <consistency-group> | --consistency-group-snapshot <consistency-group-snapshot>
[--description <description>]
[--availability-zone <availability-zone>]
[<name>]
.. option:: --volume-type <volume-type>
Volume type of this consistency group (name or ID)
.. option:: --consistency-group-source <consistency-group>
Existing consistency group (name or ID)
.. option:: --consistency-group-snapshot <consistency-group-snapshot>
Existing consistency group snapshot (name or ID)
.. option:: --description <description>
Description of this consistency group
.. option:: --availability-zone <availability-zone>
Availability zone for this consistency group
(not available if creating consistency group from source)
.. _consistency_group_create-name:
.. describe:: <name>
Name of new consistency group (default to None)
consistency group delete
------------------------
Delete consistency group(s).
.. program:: consistency group delete
.. code:: bash
openstack consistency group delete
[--force]
<consistency-group> [<consistency-group> ...]
.. option:: --force
Allow delete in state other than error or available
.. _consistency_group_delete-consistency-group:
.. describe:: <consistency-group>
Consistency group(s) to delete (name or ID)
consistency group list
----------------------
List consistency groups.
.. program:: consistency group list
.. code:: bash
openstack consistency group list
[--all-projects]
[--long]
.. option:: --all-projects
Show detail for all projects. Admin only.
(defaults to False)
.. option:: --long
List additional fields in output
consistency group remove volume
-------------------------------
Remove volume(s) from consistency group.
.. program:: consistency group remove volume
.. code:: bash
openstack consistency group remove volume
<consistency-group>
<volume> [<volume> ...]
.. _consistency_group_remove_volume:
.. describe:: <consistency-group>
Consistency group containing <volume> (name or ID)
.. describe:: <volume>
Volume(s) to remove from <consistency-group> (name or ID)
(repeat option to remove multiple volumes)
consistency group set
---------------------
Set consistency group properties.
.. program:: consistency group set
.. code:: bash
openstack consistency group set
[--name <name>]
[--description <description>]
<consistency-group>
.. option:: --name <name>
New consistency group name
.. option:: --description <description>
New consistency group description
.. _consistency_group_set-consistency-group:
.. describe:: <consistency-group>
Consistency group to modify (name or ID)
consistency group show
----------------------
Display consistency group details.
.. program:: consistency group show
.. code:: bash
openstack consistency group show
<consistency-group>
.. _consistency_group_show-consistency-group:
.. describe:: <consistency-group>
Consistency group to display (name or ID)

View file

@ -0,0 +1,10 @@
==================
console connection
==================
Server console connection information
Compute v2
.. autoprogram-cliff:: openstack.compute.v2
:command: console connection show

View file

@ -4,7 +4,7 @@ limits
The Compute and Block Storage APIs have resource usage limits. The Compute and Block Storage APIs have resource usage limits.
Compute v2, Block Storage v1 Block Storage v2, v3; Compute v2
.. autoprogram-cliff:: openstack.common .. autoprogram-cliff:: openstack.common

View file

@ -5,7 +5,7 @@ quota
Resource quotas appear in multiple APIs, OpenStackClient presents them as a Resource quotas appear in multiple APIs, OpenStackClient presents them as a
single object with multiple properties. single object with multiple properties.
Block Storage v1, v2, Compute v2, Network v2 Block Storage v1, v3; Compute v2; Network v2
.. autoprogram-cliff:: openstack.common .. autoprogram-cliff:: openstack.common
:command: quota * :command: quota *

View file

@ -4,103 +4,5 @@ role assignment
Identity v2, v3 Identity v2, v3
role assignment list .. autoprogram-cliff:: openstack.identity.v3
-------------------- :command: role assignment list
List role assignments
.. program:: role assignment list
.. code:: bash
openstack role assignment list
[--role <role>]
[--role-domain <role-domain>]
[--user <user>]
[--user-domain <user-domain>]
[--group <group>]
[--group-domain <group-domain>]
[--domain <domain>]
[--project <project>]
[--project-domain <project-domain>]
[--effective]
[--inherited]
[--names]
.. option:: --role <role>
Role to filter (name or ID)
.. versionadded:: 3
.. option:: --role-domain <role-domain>
Domain the role belongs to (name or ID).
This can be used in case collisions between role names exist.
.. versionadded:: 3
.. option:: --user <user>
User to filter (name or ID)
.. option:: --user-domain <user-domain>
Domain the user belongs to (name or ID).
This can be used in case collisions between user names exist.
.. versionadded:: 3
.. option:: --group <group>
Group to filter (name or ID)
.. versionadded:: 3
.. option:: --group-domain <group-domain>
Domain the group belongs to (name or ID).
This can be used in case collisions between group names exist.
.. versionadded:: 3
.. option:: --domain <domain>
Domain to filter (name or ID)
.. versionadded:: 3
.. option:: --project <project>
Project to filter (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.
.. versionadded:: 3
.. option:: --effective
Returns only effective role assignments (defaults to False)
.. versionadded:: 3
.. option:: --inherited
Specifies if the role grant is inheritable to the sub projects
.. versionadded:: 3
.. option:: --names
Returns role assignments with names instead of IDs
.. option:: --auth-user
Returns role assignments for the authenticated user.
.. option:: --auth-project
Returns role assignments for the project to which the authenticated user
is scoped.

View file

@ -2,7 +2,7 @@
volume backup volume backup
============= =============
Block Storage v1, v2, v3 Block Storage v2, v3
.. autoprogram-cliff:: openstack.volume.v3 .. autoprogram-cliff:: openstack.volume.v3
:command: volume backup * :command: volume backup *

View file

@ -2,7 +2,7 @@
volume qos volume qos
========== ==========
Block Storage v1, v2, v3 Block Storage v2, v3
.. autoprogram-cliff:: openstack.volume.v3 .. autoprogram-cliff:: openstack.volume.v3
:command: volume qos * :command: volume qos *

View file

@ -2,7 +2,7 @@
volume service volume service
============== ==============
Block Storage v1, v2, v3 Block Storage v2, v3
.. autoprogram-cliff:: openstack.volume.v3 .. autoprogram-cliff:: openstack.volume.v3
:command: volume service * :command: volume service *

View file

@ -2,7 +2,7 @@
volume snapshot volume snapshot
=============== ===============
Block Storage v1, v2, v3 Block Storage v2, v3
.. autoprogram-cliff:: openstack.volume.v3 .. autoprogram-cliff:: openstack.volume.v3
:command: volume snapshot * :command: volume snapshot *

View file

@ -2,7 +2,7 @@
volume transfer request volume transfer request
======================= =======================
Block Storage v1, v2, v3 Block Storage v2, v3
.. autoprogram-cliff:: openstack.volume.v3 .. autoprogram-cliff:: openstack.volume.v3
:command: volume transfer request * :command: volume transfer request *

View file

@ -2,7 +2,7 @@
volume type volume type
=========== ===========
Block Storage v1, v2, v3 Block Storage v2, v3
.. autoprogram-cliff:: openstack.volume.v3 .. autoprogram-cliff:: openstack.volume.v3
:command: volume type * :command: volume type *

View file

@ -2,392 +2,28 @@
volume volume
====== ======
Block Storage v1, v2 Block Storage v2, v3
volume create .. autoprogram-cliff:: openstack.volume.v3
------------- :command: volume create
Create new volume .. autoprogram-cliff:: openstack.volume.v3
:command: volume delete
.. program:: volume create .. autoprogram-cliff:: openstack.volume.v3
.. code:: bash :command: volume list
openstack volume create .. autoprogram-cliff:: openstack.volume.v3
[--size <size>] :command: volume migrate
[--type <volume-type>]
[--image <image> | --snapshot <snapshot> | --source <volume> ]
[--description <description>]
[--availability-zone <availability-zone>]
[--consistency-group <consistency-group>]
[--property <key=value> [...] ]
[--hint <key=value> [...] ]
[--bootable | --non-bootable]
[--read-only | --read-write]
<name>
.. option:: --size <size> .. autoprogram-cliff:: openstack.volume.v3
:command: volume set
Volume size in GB .. autoprogram-cliff:: openstack.volume.v3
(Required unless --snapshot or --source is specified) :command: volume show
.. option:: --type <volume-type> .. autoprogram-cliff:: openstack.volume.v3
:command: volume unset
Set the type of volume
Select ``<volume-type>`` from the available types as shown
by ``volume type list``.
.. option:: --image <image>
Use ``<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 volume (name or ID)
.. option:: --source <volume>
Volume to clone (name or ID)
.. option:: --description <description>
Volume description
.. option:: --availability-zone <availability-zone>
Create volume in ``<availability-zone>``
.. option:: --consistency-group <consistency-group>
Consistency group where the new volume belongs to
.. option:: --property <key=value>
Set a property on this volume (repeat option to set multiple properties)
.. option:: --hint <key=value>
Arbitrary scheduler hint key-value pairs to help boot an instance
(repeat option to set multiple hints)
.. option:: --bootable
Mark volume as bootable
.. option:: --non-bootable
Mark volume as non-bootable (default)
.. option:: --read-only
Set volume to read-only access mode
.. option:: --read-write
Set volume to read-write access mode (default)
.. _volume_create-name:
.. describe:: <name>
Volume name
volume delete
-------------
Delete volume(s)
.. program:: volume delete
.. code:: bash
openstack volume delete
[--force | --purge]
<volume> [<volume> ...]
.. option:: --force
Attempt forced removal of volume(s), regardless of state (defaults to False)
.. option:: --purge
Remove any snapshots along with volume(s) (defaults to False)
*Volume version 2 only*
.. _volume_delete-volume:
.. describe:: <volume>
Volume(s) to delete (name or ID)
volume list
-----------
List volumes
.. program:: volume list
.. code:: bash
openstack volume list
[--project <project> [--project-domain <project-domain>]]
[--user <user> [--user-domain <user-domain>]]
[--name <name>]
[--status <status>]
[--all-projects]
[--long]
[--limit <num-volumes>]
[--marker <volume>]
.. option:: --project <project>
Filter results by ``<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)
*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*
.. option:: --name <name>
Filter results by volume name
.. option:: --status <status>
Filter results by status
.. option:: --all-projects
Include all projects (admin only)
.. option:: --long
List additional fields in output
.. option:: --limit <num-volumes>
Maximum number of volumes to display
.. option:: --marker <volume>
The last volume ID of the previous page
*Volume version 2 only*
volume migrate
--------------
Migrate volume to a new host
.. program:: volume migrate
.. code:: bash
openstack volume migrate
--host <host>
[--force-host-copy]
[--lock-volume]
<volume>
.. option:: --host <host>
Destination host (takes the form: host@backend-name#pool) (required)
.. option:: --force-host-copy
Enable generic host-based force-migration,
which bypasses driver optimizations
.. option:: --lock-volume
If specified, the volume state will be locked and will not allow
a migration to be aborted (possibly by another operation)
*Volume version 2 only*
.. _volume_migrate-volume:
.. describe:: <volume>
Volume to migrate (name or ID)
volume set
----------
Set volume properties
.. program:: volume set
.. code:: bash
openstack volume set
[--name <name>]
[--size <size>]
[--description <description>]
[--no-property]
[--property <key=value> [...] ]
[--image-property <key=value> [...] ]
[--state <state>]
[--attached | --detached ]
[--type <volume-type>]
[--retype-policy <retype-policy>]
[--bootable | --non-bootable]
[--read-only | --read-write]
<volume>
.. option:: --name <name>
New volume name
.. option:: --size <size>
Extend volume size in GB
.. option:: --description <description>
New volume description
.. option:: --no-property
Remove all properties from :ref:`\<volume\> <volume_set-volume>`
(specify both :option:`--no-property` and :option:`--property` to
remove the current properties before setting new properties.)
.. option:: --property <key=value>
Set a property on this volume (repeat option to set multiple properties)
.. option:: --type <volume-type>
New volume type (name or ID)
*Volume version 2 only*
.. option:: --retype-policy <retype-policy>
Migration policy while re-typing volume
("never" or "on-demand", default is "never" )
(available only when :option:`--type` option is specified)
*Volume version 2 only*
.. option:: --bootable
Mark volume as bootable
.. option:: --non-bootable
Mark volume as non-bootable
.. option:: --read-only
Set volume to read-only access mode
.. option:: --read-write
Set volume to read-write access mode
.. option:: --image-property <key=value>
Set an image property on this volume
(repeat option to set multiple image properties)
Image properties are copied along with the image when creating a volume
using ``--image``. Note that these properties are immutable on the image
itself, this option updates the copy attached to this volume.
*Volume version 2 only*
.. option:: --state <state>
New volume state
("available", "error", "creating", "deleting", "in-use",
"attaching", "detaching", "error_deleting" or "maintenance") (admin only)
(This option simply changes the state of the volume in the database with
no regard to actual status, exercise caution when using)
*Volume version 2 only*
.. option:: --attached
Set volume attachment status to "attached" (admin only)
(This option simply changes the state of the volume in the database with
no regard to actual status, exercise caution when using)
*Volume version 2 only*
.. option:: --deattach
Set volume attachment status to "detached" (admin only)
(This option simply changes the state of the volume in the database with
no regard to actual status, exercise caution when using)
*Volume version 2 only*
.. _volume_set-volume:
.. describe:: <volume>
Volume to modify (name or ID)
volume show
-----------
Show volume details
.. program:: volume show
.. code:: bash
openstack volume show
<volume>
.. _volume_show-volume:
.. describe:: <volume>
Volume to display (name or ID)
volume unset
------------
Unset volume properties
.. program:: volume unset
.. code:: bash
openstack volume unset
[--property <key>]
[--image-property <key>]
<volume>
.. option:: --property <key>
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)
Block Storage v3 Block Storage v3

View file

@ -24,10 +24,10 @@ location-delete,,Remove locations (and related metadata) from an image.
location-update,,Update metadata of an image's location. location-update,,Update metadata of an image's location.
md-namespace-create,image metadef namespace create,Create a new metadata definitions namespace. md-namespace-create,image metadef namespace create,Create a new metadata definitions namespace.
md-namespace-delete,image metadef namespace delete,Delete specified metadata definitions namespace with its contents. md-namespace-delete,image metadef namespace delete,Delete specified metadata definitions namespace with its contents.
md-namespace-import,,Import a metadata definitions namespace from file or standard input. md-namespace-import,WONTFIX,Import a metadata definitions namespace from file or standard input.
md-namespace-list,image metadef namespace list,List metadata definitions namespaces. md-namespace-list,image metadef namespace list,List metadata definitions namespaces.
md-namespace-objects-delete,,Delete all metadata definitions objects inside a specific namespace. md-namespace-objects-delete,image metadef object delete,Delete all metadata definitions objects inside a specific namespace.
md-namespace-properties-delete,,Delete all metadata definitions property inside a specific namespace. md-namespace-properties-delete,image metadef property delete,Delete all metadata definitions property inside a specific namespace.
md-namespace-resource-type-list,image metadef resource type association list,List resource types associated to specific namespace. md-namespace-resource-type-list,image metadef resource type association list,List resource types associated to specific namespace.
md-namespace-show,image metadef namespace show,Describe a specific metadata definitions namespace. md-namespace-show,image metadef namespace show,Describe a specific metadata definitions namespace.
md-namespace-tags-delete,,Delete all metadata definitions tags inside a specific namespace. md-namespace-tags-delete,,Delete all metadata definitions tags inside a specific namespace.

1 cache-clear cached image clear Clear all images from cache, queue or both.
24 location-update Update metadata of an image's location.
25 md-namespace-create image metadef namespace create Create a new metadata definitions namespace.
26 md-namespace-delete image metadef namespace delete Delete specified metadata definitions namespace with its contents.
27 md-namespace-import WONTFIX Import a metadata definitions namespace from file or standard input.
28 md-namespace-list image metadef namespace list List metadata definitions namespaces.
29 md-namespace-objects-delete image metadef object delete Delete all metadata definitions objects inside a specific namespace.
30 md-namespace-properties-delete image metadef property delete Delete all metadata definitions property inside a specific namespace.
31 md-namespace-resource-type-list image metadef resource type association list List resource types associated to specific namespace.
32 md-namespace-show image metadef namespace show Describe a specific metadata definitions namespace.
33 md-namespace-tags-delete Delete all metadata definitions tags inside a specific namespace.

View file

@ -25,10 +25,3 @@ Plugin Commands
watcher watcher
zaqar zaqar
zun zun
.. TODO(efried): Make pages for the following once they're fixed.
.. cue
.. # cueclient is not in global-requirements
.. # list-plugins:: openstack.mb.v1
.. # :detailed:

View file

@ -41,7 +41,7 @@ opened.
## ... ## ...
def take_action(self, parsed_args): def take_action(self, parsed_args):
compute_client = self.app.client_manager.sdk_connection.compute compute_client = self.app.client_manager.compute
public_key = parsed_args.public_key public_key = parsed_args.public_key
if public_key: if public_key:

View file

@ -11,7 +11,6 @@
command-wrappers command-wrappers
command-errors command-errors
command-logs command-logs
specs/commands
plugins plugins
humaninterfaceguide humaninterfaceguide
api/modules api/modules

View file

@ -1,86 +0,0 @@
=======
example
=======
This is a specification for the ``example`` command object. It is not intended
to be a complete template for new commands since other actions, options
and/or arguments may be used. You can include general specification information
before the commands below. This information could include links to related material
or descriptions of similar commands.
[example API name] [example API version]
example create
--------------
Create new example
.. program:: example create
.. code:: bash
openstack example create
<name>
.. describe:: <name>
New example name
example delete
--------------
Delete example(s)
.. program:: example delete
.. code:: bash
openstack example delete
<example> [<example> ...]
.. describe:: <example>
Example(s) to delete (name or ID)
example list
------------
List examples
.. program:: example list
.. code:: bash
openstack example list
example set
-----------
Set example properties
.. program:: example set
.. code:: bash
openstack example set
[--name <new-name>]
<example>
.. option:: --name <new-name>
New example name
.. describe:: <example>
Example to modify (name or ID)
example show
------------
Display example details
.. program:: example show
.. code:: bash
openstack example show
<example>
.. describe:: <example>
Example to display (name or ID)

View file

@ -1,44 +0,0 @@
=============
Command Specs
=============
Specifications for new commands, objects and actions are listed below.
These specifications have not been implemented. See
:ref:`command-list` for implemented commands and
:ref:`command-structure` for implemented objects and actions.
It is optional to propose a specifications patch for new commands,
objects and actions here before submitting the implementation. Once your
specifications patch merges then you may proceed with the implementation.
Your implementation patches should move applicable portions of the
specifications patch to the official :ref:`command-list`
and :ref:`command-structure` documentation.
Objects Specs
-------------
Add specifications for new objects based on the ``example`` object.
Actions Specs
-------------
Add specifications for new actions based on the ``example`` action.
.. toctree::
:maxdepth: 1
network-topology
Commands Specs
--------------
Add specifications for new commands based on the commands for the
``example`` object. The ``example`` commands are not intended to
be a complete template for new commands since other actions, options
and/or arguments may be used.
.. toctree::
:glob:
:maxdepth: 2
command-objects/*

View file

@ -1,44 +0,0 @@
================
network topology
================
A **network topology** shows a topological graph about
devices which connect to the specific network. Also, it
will return availability information for each individual
device within the network as well. One other thing to note
is that it is the intention for OSC to collect data from
existing REST APIs
Network v2
network topology list
---------------------
List network topologies
.. program:: network topology list
.. code:: bash
openstack network topology list
[--project <project>]
.. option:: --project <project>
List network topologies for given project
(name or ID)
network topology show
---------------------
Show network topology details
.. program:: network topology show
.. code:: bash
openstack network topology show
<network>
.. _network_topology_show-network:
.. describe:: <network>
Show network topology for a specific network (name or ID)

View file

@ -94,7 +94,7 @@ def run(opts):
c_list = obj_api.container_list() c_list = obj_api.container_list()
print("Name\tCount\tBytes") print("Name\tCount\tBytes")
for c in c_list: for c in c_list:
print("%s\t%d\t%d" % (c['name'], c['count'], c['bytes'])) print(f"{c['name']}\t{c['count']}\t{c['bytes']}")
if len(c_list) > 0: if len(c_list) > 0:
# See what is in the first container # See what is in the first container

View file

@ -87,7 +87,7 @@ def run(opts):
c_list = client_manager.object_store.container_list() c_list = client_manager.object_store.container_list()
print("Name\tCount\tBytes") print("Name\tCount\tBytes")
for c in c_list: for c in c_list:
print("%s\t%d\t%d" % (c['name'], c['count'], c['bytes'])) print(f"{c['name']}\t{c['count']}\t{c['bytes']}")
if len(c_list) > 0: if len(c_list) > 0:
# See what is in the first container # See what is in the first container

179
hacking/checks.py Normal file
View file

@ -0,0 +1,179 @@
# 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 ast
import os
import re
from hacking import core
"""
Guidelines for writing new hacking checks
- Use only for python-openstackclient specific tests. OpenStack general tests
should be submitted to the common 'hacking' module.
- Pick numbers in the range O4xx. Find the current test with the highest
allocated number and then pick the next value.
"""
@core.flake8ext
def assert_no_oslo(logical_line):
"""Check for use of oslo libraries.
O400
"""
if re.match(r'(from|import) oslo_.*', logical_line):
yield (0, "0400: oslo libraries should not be used in SDK projects")
@core.flake8ext
def assert_no_duplicated_setup(logical_line, filename):
"""Check for use of various unnecessary test duplications.
O401
"""
if os.path.join('openstackclient', 'tests', 'unit') not in filename:
return
if re.match(r'self.app = .*\(self.app, self.namespace\)', logical_line):
yield (
0,
'O401: It is not necessary to create dummy Namespace objects',
)
if os.path.basename(filename) != 'fakes.py':
if re.match(
r'self.[a-z_]+_client = self.app.client_manager.*', logical_line
):
yield (
0,
"O401: Aliases for mocks of the service client are already "
"provided by the respective service's FakeClientMixin class",
)
if match := re.match(
r'self.app.client_manager.([a-z_]+) = mock.Mock', logical_line
):
service = match.group(1)
if service == 'auth_ref':
return
yield (
0,
f"O401: client_manager.{service} mocks are already provided "
f"by the {service} service's FakeClientMixin class",
)
@core.flake8ext
def assert_use_of_client_aliases(logical_line):
"""Ensure we use $service_client instead of $sdk_connection.service.
O402
"""
# we should expand the list of services as we drop legacy clients
if match := re.match(
r'self\.app\.client_manager\.sdk_connnection\.(compute|network|image)',
logical_line,
):
service = match.group(1)
yield (0, f"0402: prefer {service}_client to sdk_connection.{service}")
if match := re.match(
r'(self\.app\.client_manager\.(compute|network|image)+\.[a-z_]+) = mock.Mock', # noqa: E501
logical_line,
):
yield (
0,
f"O402: {match.group(1)} is already a mock: there's no need to "
f"assign a new mock.Mock instance.",
)
if match := re.match(
r'(self\.(compute|network|image)_client\.[a-z_]+) = mock.Mock',
logical_line,
):
yield (
0,
f"O402: {match.group(1)} is already a mock: there's no need to "
f"assign a new mock.Mock instance.",
)
class SDKProxyFindChecker(ast.NodeVisitor):
"""NodeVisitor to find ``*_client.find_*`` statements."""
def __init__(self):
self.error = False
def visit_Call(self, node):
# No need to keep visiting the AST if we already found something.
if self.error:
return
self.generic_visit(node)
if not (
isinstance(node.func, ast.Attribute)
and node.func.attr.startswith('find_') # and
# isinstance(node.func.value, ast.Attribute) and
# node.func.value.attr.endswith('_client')
):
# print(f'skipping: got {node.func}')
return
if not (
(
# handle calls like 'identity_client.find_project'
isinstance(node.func.value, ast.Name)
and node.func.value.id.endswith('client')
)
or (
# handle calls like 'self.app.client_manager.image.find_image'
isinstance(node.func.value, ast.Attribute)
and node.func.value.attr
in ('identity', 'network', 'image', 'compute')
)
):
return
if not any(kw.arg == 'ignore_missing' for kw in node.keywords):
self.error = True
@core.flake8ext
def assert_find_ignore_missing_kwargs(logical_line, filename):
"""Ensure ignore_missing is always used for ``find_*`` SDK proxy calls.
Okay: self.compute_client.find_server(foo, ignore_missing=True)
Okay: self.image_client.find_server(foo, ignore_missing=False)
Okay: self.volume_client.volumes.find(name='foo')
O403: self.network_client.find_network(parsed_args.network)
O403: self.compute_client.find_flavor(flavor_id, get_extra_specs=True)
"""
if 'tests' in filename:
return
checker = SDKProxyFindChecker()
try:
parsed_logical_line = ast.parse(logical_line)
except SyntaxError:
# let flake8 catch this itself
# https://github.com/PyCQA/flake8/issues/1948
return
checker.visit(parsed_logical_line)
if checker.error:
yield (
0,
'O403: Calls to find_* proxy methods must explicitly set '
'ignore_missing',
)

View file

@ -64,7 +64,7 @@ def list_security_groups(compute_client, all_projects=None):
def find_security_group(compute_client, name_or_id): def find_security_group(compute_client, name_or_id):
"""Find the name for a given security group name or ID """Find the security group for a given name or ID
https://docs.openstack.org/api-ref/compute/#show-security-group-details https://docs.openstack.org/api-ref/compute/#show-security-group-details
@ -240,7 +240,7 @@ def list_networks(compute_client):
def find_network(compute_client, name_or_id): def find_network(compute_client, name_or_id):
"""Find the ID for a given network name or ID """Find the network for a given name or ID
https://docs.openstack.org/api-ref/compute/#show-network-details https://docs.openstack.org/api-ref/compute/#show-network-details

View file

@ -256,7 +256,10 @@ class APIv1(api.BaseAPI):
# object's name in the container. # object's name in the container.
object_name_str = name if name else object object_name_str = name if name else object
full_url = f"{urllib.parse.quote(container)}/{urllib.parse.quote(object_name_str)}" full_url = (
f"{urllib.parse.quote(container)}/"
f"{urllib.parse.quote(object_name_str)}"
)
with open(object, 'rb') as f: with open(object, 'rb') as f:
response = self.create( response = self.create(
full_url, full_url,

View file

@ -0,0 +1,60 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""Volume v2 API Library
A collection of wrappers for deprecated Block Storage v2 APIs that are not
intentionally supported by SDK.
"""
import http
from openstack import exceptions as sdk_exceptions
from osc_lib import exceptions
# consistency groups
def find_consistency_group(compute_client, name_or_id):
"""Find the consistency group for a given name or ID
https://docs.openstack.org/api-ref/block-storage/v3/#show-a-consistency-group-s-details
:param volume_client: A volume client
:param name_or_id: The name or ID of the consistency group to look up
:returns: A consistency group object
:raises exception.NotFound: If a matching consistency group could not be
found or more than one match was found
"""
response = compute_client.get(f'/consistencygroups/{name_or_id}')
if response.status_code != http.HTTPStatus.NOT_FOUND:
# there might be other, non-404 errors
sdk_exceptions.raise_from_response(response)
return response.json()['consistencygroup']
response = compute_client.get('/consistencygroups')
sdk_exceptions.raise_from_response(response)
found = None
consistency_groups = response.json()['consistencygroups']
for consistency_group in consistency_groups:
if consistency_group['name'] == name_or_id:
if found:
raise exceptions.NotFound(
f'multiple matches found for {name_or_id}'
)
found = consistency_group
if not found:
raise exceptions.NotFound(f'{name_or_id} not found')
return found

View file

@ -0,0 +1,60 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""Volume v3 API Library
A collection of wrappers for deprecated Block Storage v3 APIs that are not
intentionally supported by SDK.
"""
import http
from openstack import exceptions as sdk_exceptions
from osc_lib import exceptions
# consistency groups
def find_consistency_group(compute_client, name_or_id):
"""Find the consistency group for a given name or ID
https://docs.openstack.org/api-ref/block-storage/v3/#show-a-consistency-group-s-details
:param volume_client: A volume client
:param name_or_id: The name or ID of the consistency group to look up
:returns: A consistency group object
:raises exception.NotFound: If a matching consistency group could not be
found or more than one match was found
"""
response = compute_client.get(f'/consistencygroups/{name_or_id}')
if response.status_code != http.HTTPStatus.NOT_FOUND:
# there might be other, non-404 errors
sdk_exceptions.raise_from_response(response)
return response.json()['consistencygroup']
response = compute_client.get('/consistencygroups')
sdk_exceptions.raise_from_response(response)
found = None
consistency_groups = response.json()['consistencygroups']
for consistency_group in consistency_groups:
if consistency_group['name'] == name_or_id:
if found:
raise exceptions.NotFound(
f'multiple matches found for {name_or_id}'
)
found = consistency_group
if not found:
raise exceptions.NotFound(f'{name_or_id} not found')
return found

View file

@ -0,0 +1,27 @@
# 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 cliff import lister
from cliff import show
from osc_lib.command import command
from openstackclient import shell
class Command(command.Command):
app: shell.OpenStackShell
class Lister(Command, lister.Lister): ...
class ShowOne(Command, show.ShowOne): ...

View file

@ -17,9 +17,9 @@ import copy
import logging import logging
from openstack import exceptions as sdk_exceptions from openstack import exceptions as sdk_exceptions
from osc_lib.command import command
from osc_lib import utils from osc_lib import utils
from openstackclient import command
from openstackclient.i18n import _ from openstackclient.i18n import _
@ -117,7 +117,7 @@ class ListAvailabilityZone(command.Lister):
return parser return parser
def _get_compute_availability_zones(self, parsed_args): def _get_compute_availability_zones(self, parsed_args):
compute_client = self.app.client_manager.sdk_connection.compute compute_client = self.app.client_manager.compute
try: try:
data = list(compute_client.availability_zones(details=True)) data = list(compute_client.availability_zones(details=True))
except sdk_exceptions.ForbiddenException: # policy doesn't allow except sdk_exceptions.ForbiddenException: # policy doesn't allow
@ -172,17 +172,14 @@ class ListAvailabilityZone(command.Lister):
return result return result
def take_action(self, parsed_args): def take_action(self, parsed_args):
columns: tuple[str, ...] = ('Zone Name', 'Zone Status')
if parsed_args.long: if parsed_args.long:
columns = ( columns += (
'Zone Name',
'Zone Status',
'Zone Resource', 'Zone Resource',
'Host Name', 'Host Name',
'Service Name', 'Service Name',
'Service Status', 'Service Status',
) )
else:
columns = ('Zone Name', 'Zone Status')
# Show everything by default. # Show everything by default.
show_all = ( show_all = (

View file

@ -15,18 +15,29 @@
"""Manage access to the clients, including authenticating when needed.""" """Manage access to the clients, including authenticating when needed."""
import argparse
from collections.abc import Callable
import importlib import importlib
import logging import logging
import sys import sys
import typing as ty
from osc_lib.cli import client_config
from osc_lib import clientmanager from osc_lib import clientmanager
from osc_lib import shell from osc_lib import shell
import stevedore import stevedore
if ty.TYPE_CHECKING:
from keystoneauth1 import access as ksa_access
from openstack.compute.v2 import _proxy as compute_proxy
from openstack.image.v2 import _proxy as image_proxy
from openstack.network.v2 import _proxy as network_proxy
from openstackclient.api import object_store_v1
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
PLUGIN_MODULES = [] PLUGIN_MODULES: list[ty.Any] = []
USER_AGENT = 'python-openstackclient' USER_AGENT = 'python-openstackclient'
@ -39,11 +50,23 @@ class ClientManager(clientmanager.ClientManager):
in osc-lib so we need to maintain a transition period. in osc-lib so we need to maintain a transition period.
""" """
# A simple incrementing version for the plugin to know what is available if ty.TYPE_CHECKING:
PLUGIN_INTERFACE_VERSION = "2" # we know this will be set by us and will not be nullable
auth_ref: ksa_access.AccessInfo
# Let the commands set this # this is a hack to keep mypy happy: the actual attributes are set in
_auth_required = False # get_plugin_modules below
# TODO(stephenfin): Change the types of identity and volume once we've
# migrated everything to SDK. Hopefully by then we'll have figured out
# how to statically distinguish between the v2 and v3 versions of both
# services...
# TODO(stephenfin): We also need to migrate object storage...
compute: compute_proxy.Proxy
identity: ty.Any
image: image_proxy.Proxy
network: network_proxy.Proxy
object_store: object_store_v1.APIv1
volume: ty.Any
def __init__( def __init__(
self, self,
@ -80,6 +103,12 @@ class ClientManager(clientmanager.ClientManager):
self._auth_required self._auth_required
and self._cli_options._openstack_config is not None and self._cli_options._openstack_config is not None
): ):
if not isinstance(
self._cli_options._openstack_config, client_config.OSC_Config
):
# programmer error
raise TypeError('unexpected type for _openstack_config')
self._cli_options._openstack_config._pw_callback = ( self._cli_options._openstack_config._pw_callback = (
shell.prompt_for_password shell.prompt_for_password
) )
@ -106,6 +135,13 @@ class ClientManager(clientmanager.ClientManager):
self._cli_options.config['auth_type'] = self._original_auth_type self._cli_options.config['auth_type'] = self._original_auth_type
del self._cli_options.config['auth']['token'] del self._cli_options.config['auth']['token']
del self._cli_options.config['auth']['endpoint'] del self._cli_options.config['auth']['endpoint']
if not isinstance(
self._cli_options._openstack_config, client_config.OSC_Config
):
# programmer error
raise TypeError('unexpected type for _openstack_config')
self._cli_options._auth = ( self._cli_options._auth = (
self._cli_options._openstack_config.load_auth_plugin( self._cli_options._openstack_config.load_auth_plugin(
self._cli_options.config, self._cli_options.config,
@ -129,20 +165,39 @@ class ClientManager(clientmanager.ClientManager):
# TODO(stephenfin): Drop volume_client argument in OSC 8.0 or later. # TODO(stephenfin): Drop volume_client argument in OSC 8.0 or later.
def is_volume_endpoint_enabled(self, volume_client=None): def is_volume_endpoint_enabled(self, volume_client=None):
"""Check if volume endpoint is enabled""" """Check if volume endpoint is enabled"""
# We check against the service type and all aliases defined by the
# Service Types Authority
# https://service-types.openstack.org/service-types.json
return ( return (
self.is_service_available('volume') is not False self.is_service_available('block-storage') is not False
or self.is_service_available('volume') is not False
or self.is_service_available('volumev3') is not False or self.is_service_available('volumev3') is not False
or self.is_service_available('volumev2') is not False or self.is_service_available('volumev2') is not False
or self.is_service_available('block-store') is not False
) )
# Plugin Support # Plugin Support
ArgumentParserT = ty.TypeVar('ArgumentParserT', bound=argparse.ArgumentParser)
@ty.runtime_checkable # Optional: allows usage with isinstance()
class PluginModule(ty.Protocol):
DEFAULT_API_VERSION: str
API_VERSION_OPTION: str
API_NAME: str
API_VERSIONS: tuple[str]
make_client: Callable[..., ty.Any]
build_option_parser: Callable[[ArgumentParserT], ArgumentParserT]
check_api_version: Callable[[str], bool]
def _on_load_failure_callback( def _on_load_failure_callback(
manager: stevedore.ExtensionManager, manager: stevedore.ExtensionManager,
ep: importlib.metadata.EntryPoint, ep: importlib.metadata.EntryPoint,
err: Exception, err: BaseException,
) -> None: ) -> None:
sys.stderr.write( sys.stderr.write(
f"WARNING: Failed to import plugin {ep.group}:{ep.name}: {err}.\n" f"WARNING: Failed to import plugin {ep.group}:{ep.name}: {err}.\n"
@ -152,36 +207,25 @@ def _on_load_failure_callback(
def get_plugin_modules(group): def get_plugin_modules(group):
"""Find plugin entry points""" """Find plugin entry points"""
mod_list = [] mod_list = []
mgr: stevedore.ExtensionManager[PluginModule]
mgr = stevedore.ExtensionManager( mgr = stevedore.ExtensionManager(
group, on_load_failure_callback=_on_load_failure_callback group, on_load_failure_callback=_on_load_failure_callback
) )
for ep in mgr: for ep in mgr:
LOG.debug('Found plugin %s', ep.name) LOG.debug('Found plugin %s', ep.name)
# Different versions of stevedore use different
# implementations of EntryPoint from other libraries, which
# are not API-compatible.
try:
module_name = ep.entry_point.module_name
except AttributeError:
try:
module_name = ep.entry_point.module module_name = ep.entry_point.module
except AttributeError:
module_name = ep.entry_point.value
try: try:
module = importlib.import_module(module_name) module = importlib.import_module(module_name)
except Exception as err: except Exception as err:
sys.stderr.write( sys.stderr.write(
f"WARNING: Failed to import plugin {ep.group}:{ep.name}: " f"WARNING: Failed to import plugin "
f"{err}.\n" f"{ep.module_name}:{ep.name}: {err}.\n"
) )
continue continue
mod_list.append(module) mod_list.append(module)
init_func = getattr(module, 'Initialize', None)
if init_func:
init_func('x')
# Add the plugin to the ClientManager # Add the plugin to the ClientManager
setattr( setattr(

View file

@ -14,8 +14,8 @@
"""Configuration action implementations""" """Configuration action implementations"""
from keystoneauth1.loading import base from keystoneauth1.loading import base
from osc_lib.command import command
from openstackclient import command
from openstackclient.i18n import _ from openstackclient.i18n import _
REDACTED = "<redacted>" REDACTED = "<redacted>"

View file

@ -0,0 +1,57 @@
# 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 os
from openstackclient.i18n import _
def bool_from_str(value, strict=False):
true_strings = ('1', 't', 'true', 'on', 'y', 'yes')
false_strings = ('0', 'f', 'false', 'off', 'n', 'no')
if isinstance(value, bool):
return value
lowered = value.strip().lower()
if lowered in true_strings:
return True
elif lowered in false_strings or not strict:
return False
msg = _(
"Unrecognized value '%(value)s'; acceptable values are: %(valid)s"
) % {
'value': value,
'valid': ', '.join(
f"'{s}'" for s in sorted(true_strings + false_strings)
),
}
raise ValueError(msg)
def boolenv(*vars, default=False):
"""Search for the first defined of possibly many bool-like env vars.
Returns the first environment variable defined in vars, or returns the
default.
:param vars: Arbitrary strings to search for. Case sensitive.
:param default: The default to return if no value found.
:returns: A boolean corresponding to the value found, else the default if
no value found.
"""
for v in vars:
value = os.environ.get(v, None)
if value:
return bool_from_str(value)
return default

View file

@ -17,9 +17,9 @@
import logging import logging
from osc_lib.command import command
from osc_lib import utils from osc_lib import utils
from openstackclient import command
from openstackclient.i18n import _ from openstackclient.i18n import _
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
@ -73,17 +73,9 @@ class ListExtension(command.Lister):
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
columns: tuple[str, ...] = ('Name', 'Alias', 'Description')
if parsed_args.long: if parsed_args.long:
columns = ( columns += ('Namespace', 'Updated At', 'Links')
'Name',
'Alias',
'Description',
'Namespace',
'Updated At',
'Links',
)
else:
columns = ('Name', 'Alias', 'Description')
data = [] data = []
@ -106,7 +98,7 @@ class ListExtension(command.Lister):
LOG.warning(message) LOG.warning(message)
if parsed_args.compute or show_all: if parsed_args.compute or show_all:
compute_client = self.app.client_manager.sdk_connection.compute compute_client = self.app.client_manager.compute
try: try:
data += compute_client.extensions() data += compute_client.extensions()
except Exception: except Exception:

View file

@ -17,9 +17,9 @@
import itertools import itertools
from osc_lib.command import command
from osc_lib import utils from osc_lib import utils
from openstackclient import command
from openstackclient.i18n import _ from openstackclient.i18n import _
from openstackclient.identity import common as identity_common from openstackclient.identity import common as identity_common
@ -122,7 +122,7 @@ class ShowLimits(command.Lister):
volume_limits = None volume_limits = None
if self.app.client_manager.is_compute_endpoint_enabled(): if self.app.client_manager.is_compute_endpoint_enabled():
compute_client = self.app.client_manager.sdk_connection.compute compute_client = self.app.client_manager.compute
compute_limits = compute_client.get_limits( compute_limits = compute_client.get_limits(
reserved=parsed_args.is_reserved, tenant_id=project_id reserved=parsed_args.is_reserved, tenant_id=project_id
) )
@ -130,7 +130,7 @@ class ShowLimits(command.Lister):
if self.app.client_manager.is_volume_endpoint_enabled(): if self.app.client_manager.is_volume_endpoint_enabled():
volume_client = self.app.client_manager.sdk_connection.volume volume_client = self.app.client_manager.sdk_connection.volume
volume_limits = volume_client.get_limits( volume_limits = volume_client.get_limits(
project_id=project_id, project=project_id,
) )
if parsed_args.is_absolute: if parsed_args.is_absolute:

View file

@ -17,9 +17,9 @@
import sys import sys
from osc_lib.command import command
from osc_lib import utils from osc_lib import utils
from openstackclient import command
from openstackclient.i18n import _ from openstackclient.i18n import _
@ -48,7 +48,9 @@ class ListCommand(command.Lister):
columns = ('Command Group', 'Commands') columns = ('Command Group', 'Commands')
if parsed_args.group: if parsed_args.group:
groups = (group for group in groups if parsed_args.group in group) groups = sorted(
group for group in groups if parsed_args.group in group
)
commands = [] commands = []
for group in groups: for group in groups:

View file

@ -17,10 +17,11 @@ import getpass
import logging import logging
import os import os
import queue import queue
import typing as ty
from cliff.formatters import table from cliff.formatters import table
from osc_lib.command import command
from openstackclient import command
from openstackclient.i18n import _ from openstackclient.i18n import _
from openstackclient.identity import common as identity_common from openstackclient.identity import common as identity_common
@ -35,7 +36,7 @@ def ask_user_yesno(msg):
:return bool: User choice :return bool: User choice
""" """
while True: while True:
answer = getpass._raw_input('{} [{}]: '.format(msg, 'y/n')) answer = getpass.getpass('{} [{}]: '.format(msg, 'y/n'))
if answer in ('y', 'Y', 'yes'): if answer in ('y', 'Y', 'yes'):
return True return True
elif answer in ('n', 'N', 'no'): elif answer in ('n', 'N', 'no'):
@ -89,18 +90,20 @@ class ProjectCleanup(command.Command):
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
sdk = self.app.client_manager.sdk_connection connection = self.app.client_manager.sdk_connection
if parsed_args.auth_project: if parsed_args.auth_project:
project_connect = sdk # is we've got a project already configured, use the connection
# as-is
pass
elif parsed_args.project: elif parsed_args.project:
project = sdk.identity.find_project( project = connection.identity.find_project(
name_or_id=parsed_args.project, ignore_missing=False name_or_id=parsed_args.project, ignore_missing=False
) )
project_connect = sdk.connect_as_project(project) connection = connection.connect_as_project(project)
if project_connect: if connection:
status_queue = queue.Queue() status_queue: queue.Queue[ty.Any] = queue.Queue()
parsed_args.max_width = int( parsed_args.max_width = int(
os.environ.get('CLIFF_MAX_TERM_WIDTH', 0) os.environ.get('CLIFF_MAX_TERM_WIDTH', 0)
) )
@ -119,7 +122,7 @@ class ProjectCleanup(command.Command):
if parsed_args.updated_before: if parsed_args.updated_before:
filters['updated_at'] = parsed_args.updated_before filters['updated_at'] = parsed_args.updated_before
project_connect.project_cleanup( connection.project_cleanup(
dry_run=True, dry_run=True,
status_queue=status_queue, status_queue=status_queue,
filters=filters, filters=filters,
@ -149,7 +152,7 @@ class ProjectCleanup(command.Command):
self.log.warning(_('Deleting resources')) self.log.warning(_('Deleting resources'))
project_connect.project_cleanup( connection.project_cleanup(
dry_run=False, dry_run=False,
status_queue=status_queue, status_queue=status_queue,
filters=filters, filters=filters,

View file

@ -18,12 +18,13 @@ import argparse
import itertools import itertools
import logging import logging
import sys import sys
import typing as ty
from openstack import exceptions as sdk_exceptions from openstack import exceptions as sdk_exceptions
from osc_lib.command import command
from osc_lib import exceptions from osc_lib import exceptions
from osc_lib import utils from osc_lib import utils
from openstackclient import command
from openstackclient.i18n import _ from openstackclient.i18n import _
from openstackclient.network import common from openstackclient.network import common
@ -132,7 +133,7 @@ def get_compute_quotas(
default=False, default=False,
): ):
try: try:
client = app.client_manager.sdk_connection.compute client = app.client_manager.compute
if default: if default:
quota = client.get_quota_set_defaults(project_id) quota = client.get_quota_set_defaults(project_id)
else: else:
@ -176,25 +177,37 @@ def get_network_quotas(
default=False, default=False,
): ):
def _network_quota_to_dict(network_quota, detail=False): def _network_quota_to_dict(network_quota, detail=False):
if not isinstance(network_quota, dict): dict_quota = network_quota.to_dict(computed=False)
dict_quota = network_quota.to_dict()
else:
dict_quota = network_quota
result = {} if not detail:
return dict_quota
# Neutron returns quota details in dict which is in format like:
# {'resource_name': {'in_use': X, 'limit': Y, 'reserved': Z},
# 'resource_name_2': {'in_use': X2, 'limit': Y2, 'reserved': Z2}}
#
# but Nova and Cinder returns quota in different format, like:
# {'resource_name': X,
# 'resource_name_2': X2,
# 'usage': {
# 'resource_name': Y,
# 'resource_name_2': Y2
# },
# 'reserved': {
# 'resource_name': Z,
# 'resource_name_2': Z2
# }}
#
# so we need to make conversion to have data in same format from
# all of the services
result: dict[str, ty.Any] = {"usage": {}, "reservation": {}}
for key, values in dict_quota.items(): for key, values in dict_quota.items():
if values is None: if values is None:
continue continue
if isinstance(values, dict):
# NOTE(slaweq): Neutron returns values with key "used" but Nova for result[key] = values['limit']
# example returns same data with key "in_use" instead. Because of result["reservation"][key] = values['reserved']
# that we need to convert Neutron key to the same as is returned result["usage"][key] = values['used']
# from Nova to make result more consistent
if isinstance(values, dict) and 'used' in values:
values['in_use'] = values.pop("used")
result[key] = values
return result return result
@ -243,15 +256,20 @@ class ListQuota(command.Lister):
return parser return parser
def _list_quota_compute(self, parsed_args, project_ids): def _list_quota_compute(self, parsed_args, project_ids):
compute_client = self.app.client_manager.sdk_connection.compute compute_client = self.app.client_manager.compute
result = [] result = []
for project_id in project_ids: for project_id in project_ids:
try: try:
project_data = compute_client.get_quota_set(project_id) project_data = compute_client.get_quota_set(project_id)
# NOTE(stephenfin): Unfortunately, Nova raises a HTTP 400 (Bad
# Request) if the project ID is invalid, even though the project
# ID is actually the resource's identifier which would normally
# lead us to expect a HTTP 404 (Not Found).
except ( except (
sdk_exceptions.NotFoundException, sdk_exceptions.BadRequestException,
sdk_exceptions.ForbiddenException, sdk_exceptions.ForbiddenException,
sdk_exceptions.NotFoundException,
) as exc: ) as exc:
# Project not found, move on to next one # Project not found, move on to next one
LOG.warning(f"Project {project_id} not found: {exc}") LOG.warning(f"Project {project_id} not found: {exc}")
@ -273,7 +291,7 @@ class ListQuota(command.Lister):
if default_result != project_result: if default_result != project_result:
result += project_result result += project_result
columns = ( columns: tuple[str, ...] = (
'id', 'id',
'cores', 'cores',
'injected_files', 'injected_files',
@ -286,7 +304,7 @@ class ListQuota(command.Lister):
'server_groups', 'server_groups',
'server_group_members', 'server_group_members',
) )
column_headers = ( column_headers: tuple[str, ...] = (
'Project ID', 'Project ID',
'Cores', 'Cores',
'Injected Files', 'Injected Files',
@ -312,8 +330,8 @@ class ListQuota(command.Lister):
try: try:
project_data = volume_client.get_quota_set(project_id) project_data = volume_client.get_quota_set(project_id)
except ( except (
sdk_exceptions.NotFoundException,
sdk_exceptions.ForbiddenException, sdk_exceptions.ForbiddenException,
sdk_exceptions.NotFoundException,
) as exc: ) as exc:
# Project not found, move on to next one # Project not found, move on to next one
LOG.warning(f"Project {project_id} not found: {exc}") LOG.warning(f"Project {project_id} not found: {exc}")
@ -335,7 +353,7 @@ class ListQuota(command.Lister):
if default_result != project_result: if default_result != project_result:
result += project_result result += project_result
columns = ( columns: tuple[str, ...] = (
'id', 'id',
'backups', 'backups',
'backup_gigabytes', 'backup_gigabytes',
@ -344,7 +362,7 @@ class ListQuota(command.Lister):
'snapshots', 'snapshots',
'volumes', 'volumes',
) )
column_headers = ( column_headers: tuple[str, ...] = (
'Project ID', 'Project ID',
'Backups', 'Backups',
'Backup Gigabytes', 'Backup Gigabytes',
@ -390,7 +408,7 @@ class ListQuota(command.Lister):
if default_result != project_result: if default_result != project_result:
result += project_result result += project_result
columns = ( columns: tuple[str, ...] = (
'id', 'id',
'floating_ips', 'floating_ips',
'networks', 'networks',
@ -402,7 +420,7 @@ class ListQuota(command.Lister):
'subnets', 'subnets',
'subnet_pools', 'subnet_pools',
) )
column_headers = ( column_headers: tuple[str, ...] = (
'Project ID', 'Project ID',
'Floating IPs', 'Floating IPs',
'Networks', 'Networks',
@ -570,7 +588,7 @@ class SetQuota(common.NetDetectionMixin, command.Command):
network_kwargs = {} network_kwargs = {}
if self.app.client_manager.is_compute_endpoint_enabled(): if self.app.client_manager.is_compute_endpoint_enabled():
compute_client = self.app.client_manager.sdk_connection.compute compute_client = self.app.client_manager.compute
for k, v in COMPUTE_QUOTAS.items(): for k, v in COMPUTE_QUOTAS.items():
value = getattr(parsed_args, k, None) value = getattr(parsed_args, k, None)
@ -728,21 +746,32 @@ and ``server-group-members`` output for a given quota class."""
# values if the project or class does not exist. This is expected # values if the project or class does not exist. This is expected
# behavior. However, we have already checked for the presence of the # behavior. However, we have already checked for the presence of the
# project above so it shouldn't be an issue. # project above so it shouldn't be an issue.
if parsed_args.service in {'all', 'compute'}: if parsed_args.service == 'compute' or (
parsed_args.service == 'all'
and self.app.client_manager.is_compute_endpoint_enabled()
):
compute_quota_info = get_compute_quotas( compute_quota_info = get_compute_quotas(
self.app, self.app,
project, project,
detail=parsed_args.usage, detail=parsed_args.usage,
default=parsed_args.default, default=parsed_args.default,
) )
if parsed_args.service in {'all', 'volume'}:
if parsed_args.service == 'volume' or (
parsed_args.service == 'all'
and self.app.client_manager.is_volume_endpoint_enabled()
):
volume_quota_info = get_volume_quotas( volume_quota_info = get_volume_quotas(
self.app, self.app,
project, project,
detail=parsed_args.usage, detail=parsed_args.usage,
default=parsed_args.default, default=parsed_args.default,
) )
if parsed_args.service in {'all', 'network'}:
if parsed_args.service == 'network' or (
parsed_args.service == 'all'
and self.app.client_manager.is_network_endpoint_enabled()
):
network_quota_info = get_network_quotas( network_quota_info = get_network_quotas(
self.app, self.app,
project, project,
@ -751,10 +780,24 @@ and ``server-group-members`` output for a given quota class."""
) )
info = {} info = {}
if parsed_args.usage:
info["reservation"] = compute_quota_info.pop("reservation", {})
info["reservation"].update(
volume_quota_info.pop("reservation", {})
)
info["reservation"].update(
network_quota_info.pop("reservation", {})
)
info["usage"] = compute_quota_info.pop("usage", {})
info["usage"].update(volume_quota_info.pop("usage", {}))
info["usage"].update(network_quota_info.pop("usage", {}))
info.update(compute_quota_info) info.update(compute_quota_info)
info.update(volume_quota_info) info.update(volume_quota_info)
info.update(network_quota_info) info.update(network_quota_info)
def _normalize_names(section: dict) -> None:
# Map the internal quota names to the external ones # Map the internal quota names to the external ones
# COMPUTE_QUOTAS and NETWORK_QUOTAS share floating-ips, # COMPUTE_QUOTAS and NETWORK_QUOTAS share floating-ips,
# secgroup-rules and secgroups as dict value, so when # secgroup-rules and secgroups as dict value, so when
@ -766,9 +809,13 @@ and ``server-group-members`` output for a given quota class."""
VOLUME_QUOTAS.items(), VOLUME_QUOTAS.items(),
NETWORK_QUOTAS.items(), NETWORK_QUOTAS.items(),
): ):
if not k == v and info.get(k) is not None: if not k == v and section.get(k) is not None:
info[v] = info[k] section[v] = section.pop(k)
info.pop(k)
_normalize_names(info)
if parsed_args.usage:
_normalize_names(info["reservation"])
_normalize_names(info["usage"])
# Remove the 'id' field since it's not very useful # Remove the 'id' field since it's not very useful
if 'id' in info: if 'id' in info:
@ -793,11 +840,11 @@ and ``server-group-members`` output for a given quota class."""
if k not in ('usage', 'reservation') if k not in ('usage', 'reservation')
] ]
columns = ( columns: tuple[str, ...] = (
'resource', 'resource',
'limit', 'limit',
) )
column_headers = ( column_headers: tuple[str, ...] = (
'Resource', 'Resource',
'Limit', 'Limit',
) )
@ -875,12 +922,18 @@ class DeleteQuota(command.Command):
) )
# compute quotas # compute quotas
if parsed_args.service in {'all', 'compute'}: if parsed_args.service == 'compute' or (
compute_client = self.app.client_manager.sdk_connection.compute parsed_args.service == 'all'
and self.app.client_manager.is_compute_endpoint_enabled()
):
compute_client = self.app.client_manager.compute
compute_client.revert_quota_set(project.id) compute_client.revert_quota_set(project.id)
# volume quotas # volume quotas
if parsed_args.service in {'all', 'volume'}: if parsed_args.service == 'volume' or (
parsed_args.service == 'all'
and self.app.client_manager.is_volume_endpoint_enabled()
):
volume_client = self.app.client_manager.sdk_connection.volume volume_client = self.app.client_manager.sdk_connection.volume
volume_client.revert_quota_set(project.id) volume_client.revert_quota_set(project.id)

View file

@ -14,8 +14,7 @@
"""Versions Action Implementation""" """Versions Action Implementation"""
from osc_lib.command import command from openstackclient import command
from openstackclient.i18n import _ from openstackclient.i18n import _

View file

@ -11,7 +11,6 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
#
import logging import logging
@ -21,13 +20,11 @@ from openstackclient.i18n import _
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
# global variables used when building the shell
DEFAULT_API_VERSION = '2.1' DEFAULT_API_VERSION = '2.1'
API_VERSION_OPTION = 'os_compute_api_version' API_VERSION_OPTION = 'os_compute_api_version'
API_NAME = 'compute' API_NAME = 'compute'
API_VERSIONS = { API_VERSIONS = ('2', '2.1')
'2': 'openstack.connection.Connection',
'2.1': 'openstack.connection.Connection',
}
def make_client(instance): def make_client(instance):
@ -49,3 +46,8 @@ def build_option_parser(parser):
% DEFAULT_API_VERSION, % DEFAULT_API_VERSION,
) )
return parser return parser
def check_api_version(check_version):
# SDK supports auto-negotiation for us: always return True
return True

View file

@ -18,10 +18,10 @@
import logging import logging
from openstack import exceptions as sdk_exceptions from openstack import exceptions as sdk_exceptions
from osc_lib.command import command
from osc_lib import exceptions from osc_lib import exceptions
from osc_lib import utils from osc_lib import utils
from openstackclient import command
from openstackclient.i18n import _ from openstackclient.i18n import _
@ -56,7 +56,7 @@ class CreateAgent(command.ShowOne):
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
compute_client = self.app.client_manager.sdk_connection.compute compute_client = self.app.client_manager.compute
# doing this since openstacksdk has decided not to support this # doing this since openstacksdk has decided not to support this
# deprecated command # deprecated command
@ -95,7 +95,7 @@ class DeleteAgent(command.Command):
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
compute_client = self.app.client_manager.sdk_connection.compute compute_client = self.app.client_manager.compute
result = 0 result = 0
for id in parsed_args.id: for id in parsed_args.id:
try: try:
@ -114,7 +114,7 @@ class DeleteAgent(command.Command):
if result > 0: if result > 0:
total = len(parsed_args.id) total = len(parsed_args.id)
msg = _("%(result)s of %(total)s agents failed " "to delete.") % { msg = _("%(result)s of %(total)s agents failed to delete.") % {
'result': result, 'result': result,
'total': total, 'total': total,
} }
@ -139,7 +139,7 @@ class ListAgent(command.Lister):
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
compute_client = self.app.client_manager.sdk_connection.compute compute_client = self.app.client_manager.compute
columns = ( columns = (
"Agent ID", "Agent ID",
"Hypervisor", "Hypervisor",
@ -194,7 +194,7 @@ class SetAgent(command.Command):
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
compute_client = self.app.client_manager.sdk_connection.compute compute_client = self.app.client_manager.compute
response = compute_client.get('/os-agents', microversion='2.1') response = compute_client.get('/os-agents', microversion='2.1')
sdk_exceptions.raise_from_response(response) sdk_exceptions.raise_from_response(response)

View file

@ -17,21 +17,23 @@
"""Compute v2 Aggregate action implementations""" """Compute v2 Aggregate action implementations"""
import logging import logging
import typing as ty
from cliff import columns
from openstack import utils as sdk_utils from openstack import utils as sdk_utils
from osc_lib.cli import format_columns from osc_lib.cli import format_columns
from osc_lib.cli import parseractions from osc_lib.cli import parseractions
from osc_lib.command import command
from osc_lib import exceptions from osc_lib import exceptions
from osc_lib import utils from osc_lib import utils
from openstackclient import command
from openstackclient.i18n import _ from openstackclient.i18n import _
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
_aggregate_formatters = { _aggregate_formatters: dict[str, type[columns.FormattableColumn[ty.Any]]] = {
'Hosts': format_columns.ListColumn, 'Hosts': format_columns.ListColumn,
'Metadata': format_columns.DictColumn, 'Metadata': format_columns.DictColumn,
'hosts': format_columns.ListColumn, 'hosts': format_columns.ListColumn,
@ -67,7 +69,7 @@ class AddAggregateHost(command.ShowOne):
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
compute_client = self.app.client_manager.sdk_connection.compute compute_client = self.app.client_manager.compute
aggregate = compute_client.find_aggregate( aggregate = compute_client.find_aggregate(
parsed_args.aggregate, ignore_missing=False parsed_args.aggregate, ignore_missing=False
@ -110,7 +112,7 @@ class CreateAggregate(command.ShowOne):
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
compute_client = self.app.client_manager.sdk_connection.compute compute_client = self.app.client_manager.compute
attrs = {'name': parsed_args.name} attrs = {'name': parsed_args.name}
@ -146,7 +148,7 @@ class DeleteAggregate(command.Command):
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
compute_client = self.app.client_manager.sdk_connection.compute compute_client = self.app.client_manager.compute
result = 0 result = 0
for a in parsed_args.aggregate: for a in parsed_args.aggregate:
try: try:
@ -168,9 +170,10 @@ class DeleteAggregate(command.Command):
if result > 0: if result > 0:
total = len(parsed_args.aggregate) total = len(parsed_args.aggregate)
msg = _( msg = _("%(result)s of %(total)s aggregates failed to delete.") % {
"%(result)s of %(total)s aggregates failed " "to delete." 'result': result,
) % {'result': result, 'total': total} 'total': total,
}
raise exceptions.CommandError(msg) raise exceptions.CommandError(msg)
@ -188,13 +191,13 @@ class ListAggregate(command.Lister):
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
compute_client = self.app.client_manager.sdk_connection.compute compute_client = self.app.client_manager.compute
aggregates = list(compute_client.aggregates()) aggregates = list(compute_client.aggregates())
if sdk_utils.supports_microversion(compute_client, '2.41'): if sdk_utils.supports_microversion(compute_client, '2.41'):
column_headers = ("ID", "UUID") column_headers: tuple[str, ...] = ("ID", "UUID")
columns = ("id", "uuid") columns: tuple[str, ...] = ("id", "uuid")
else: else:
column_headers = ("ID",) column_headers = ("ID",)
columns = ("id",) columns = ("id",)
@ -250,7 +253,7 @@ class RemoveAggregateHost(command.ShowOne):
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
compute_client = self.app.client_manager.sdk_connection.compute compute_client = self.app.client_manager.compute
aggregate = compute_client.find_aggregate( aggregate = compute_client.find_aggregate(
parsed_args.aggregate, ignore_missing=False parsed_args.aggregate, ignore_missing=False
@ -307,7 +310,7 @@ class SetAggregate(command.Command):
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
compute_client = self.app.client_manager.sdk_connection.compute compute_client = self.app.client_manager.compute
aggregate = compute_client.find_aggregate( aggregate = compute_client.find_aggregate(
parsed_args.aggregate, ignore_missing=False parsed_args.aggregate, ignore_missing=False
) )
@ -320,7 +323,7 @@ class SetAggregate(command.Command):
if kwargs: if kwargs:
compute_client.update_aggregate(aggregate.id, **kwargs) compute_client.update_aggregate(aggregate.id, **kwargs)
properties = {} properties: dict[str, ty.Any] = {}
if parsed_args.no_property: if parsed_args.no_property:
# NOTE(RuiChen): "availability_zone" can not be unset from # NOTE(RuiChen): "availability_zone" can not be unset from
# properties. It is already excluded from show and create output. # properties. It is already excluded from show and create output.
@ -352,7 +355,7 @@ class ShowAggregate(command.ShowOne):
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
compute_client = self.app.client_manager.sdk_connection.compute compute_client = self.app.client_manager.compute
aggregate = compute_client.find_aggregate( aggregate = compute_client.find_aggregate(
parsed_args.aggregate, ignore_missing=False parsed_args.aggregate, ignore_missing=False
) )
@ -392,7 +395,7 @@ class UnsetAggregate(command.Command):
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
compute_client = self.app.client_manager.sdk_connection.compute compute_client = self.app.client_manager.compute
aggregate = compute_client.find_aggregate( aggregate = compute_client.find_aggregate(
parsed_args.aggregate, ignore_missing=False parsed_args.aggregate, ignore_missing=False
) )
@ -427,7 +430,7 @@ class CacheImageForAggregate(command.Command):
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
compute_client = self.app.client_manager.sdk_connection.compute compute_client = self.app.client_manager.compute
if not sdk_utils.supports_microversion(compute_client, '2.81'): if not sdk_utils.supports_microversion(compute_client, '2.81'):
msg = _( msg = _(
@ -436,15 +439,15 @@ class CacheImageForAggregate(command.Command):
) )
raise exceptions.CommandError(msg) raise exceptions.CommandError(msg)
image_client = self.app.client_manager.sdk_connection.image
aggregate = compute_client.find_aggregate( aggregate = compute_client.find_aggregate(
parsed_args.aggregate, ignore_missing=False parsed_args.aggregate, ignore_missing=False
) )
images = [] images = []
for img in parsed_args.image: for img in parsed_args.image:
image = self.app.client_manager.sdk_connection.image.find_image( image = image_client.find_image(img, ignore_missing=False)
img, ignore_missing=False
)
images.append(image.id) images.append(image.id)
compute_client.aggregate_precache_images(aggregate.id, images) compute_client.aggregate_precache_images(aggregate.id, images)

View file

@ -16,19 +16,18 @@
"""Compute v2 Console action implementations""" """Compute v2 Console action implementations"""
from osc_lib.cli import parseractions from osc_lib.cli import parseractions
from osc_lib.command import command
from osc_lib import utils from osc_lib import utils
from openstackclient import command
from openstackclient.i18n import _ from openstackclient.i18n import _
def _get_console_columns(item): def _get_console_columns(item):
# To maintain backwards compatibility we need to rename sdk props to # To maintain backwards compatibility we need to rename sdk props to
# whatever OSC was using before # whatever OSC was using before
column_map = {}
hidden_columns = ['id', 'links', 'location', 'name'] hidden_columns = ['id', 'links', 'location', 'name']
return utils.get_osc_show_columns_for_sdk_resource( return utils.get_osc_show_columns_for_sdk_resource(
item, column_map, hidden_columns item, {}, hidden_columns
) )
@ -56,7 +55,7 @@ class ShowConsoleLog(command.Command):
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
compute_client = self.app.client_manager.sdk_connection.compute compute_client = self.app.client_manager.compute
server = compute_client.find_server( server = compute_client.find_server(
name_or_id=parsed_args.server, ignore_missing=False name_or_id=parsed_args.server, ignore_missing=False
@ -65,12 +64,14 @@ class ShowConsoleLog(command.Command):
output = compute_client.get_server_console_output( output = compute_client.get_server_console_output(
server.id, length=parsed_args.lines server.id, length=parsed_args.lines
) )
data = None data: str | None = None
if output: if output:
data = output.get('output', None) data = output.get('output', None)
if data and data[-1] != '\n': if data and data[-1] != '\n':
data += '\n' data += '\n'
if data:
self.app.stdout.write(data) self.app.stdout.write(data)
@ -107,6 +108,13 @@ class ShowConsoleURL(command.ShowOne):
const='spice-html5', const='spice-html5',
help=_("Show SPICE console URL"), help=_("Show SPICE console URL"),
) )
type_group.add_argument(
'--spice-direct',
dest='url_type',
action='store_const',
const='spice-direct',
help=_("Show SPICE direct protocol native console URL"),
)
type_group.add_argument( type_group.add_argument(
'--rdp', '--rdp',
dest='url_type', dest='url_type',
@ -131,7 +139,7 @@ class ShowConsoleURL(command.ShowOne):
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
compute_client = self.app.client_manager.sdk_connection.compute compute_client = self.app.client_manager.compute
server = compute_client.find_server( server = compute_client.find_server(
parsed_args.server, ignore_missing=False parsed_args.server, ignore_missing=False
) )

View file

@ -0,0 +1,48 @@
# 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 Console auth token implementations."""
from osc_lib import utils
from openstackclient import command
from openstackclient.i18n import _
def _get_console_connection_columns(item):
column_map: dict[str, str] = {}
hidden_columns = ['id', 'location', 'name']
return utils.get_osc_show_columns_for_sdk_resource(
item, column_map, hidden_columns
)
class ShowConsoleConnectionInformation(command.ShowOne):
_description = _("Show server's remote console connection information")
def get_parser(self, prog_name):
parser = super().get_parser(prog_name)
parser.add_argument(
'token',
metavar='<token>',
help=_("Nova console token to lookup"),
)
return parser
def take_action(self, parsed_args):
compute_client = self.app.client_manager.compute
data = compute_client.validate_console_auth_token(parsed_args.token)
display_columns, columns = _get_console_connection_columns(data)
data = utils.get_dict_properties(data, columns)
return (display_columns, data)

View file

@ -21,10 +21,10 @@ from openstack import exceptions as sdk_exceptions
from openstack import utils as sdk_utils from openstack import utils as sdk_utils
from osc_lib.cli import format_columns from osc_lib.cli import format_columns
from osc_lib.cli import parseractions from osc_lib.cli import parseractions
from osc_lib.command import command
from osc_lib import exceptions from osc_lib import exceptions
from osc_lib import utils from osc_lib import utils
from openstackclient import command
from openstackclient.common import pagination from openstackclient.common import pagination
from openstackclient.i18n import _ from openstackclient.i18n import _
from openstackclient.identity import common as identity_common from openstackclient.identity import common as identity_common
@ -149,19 +149,32 @@ class CreateFlavor(command.ShowOne):
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
compute_client = self.app.client_manager.sdk_connection.compute compute_client = self.app.client_manager.compute
identity_client = self.app.client_manager.identity identity_client = self.app.client_manager.identity
if parsed_args.project and parsed_args.public: if parsed_args.project and parsed_args.public:
msg = _("--project is only allowed with --private") msg = _("--project is only allowed with --private")
raise exceptions.CommandError(msg) raise exceptions.CommandError(msg)
flavor_id = parsed_args.id
if parsed_args.id == 'auto':
# novaclient aliased 'auto' to mean "generate a UUID for me": we
# do the same to avoid breaking existing users
flavor_id = None
msg = _(
"Passing '--id auto' is deprecated. Nova will automatically "
"assign a UUID-like ID if no ID is provided. Omit the '--id' "
"parameter instead."
)
self.log.warning(msg)
args = { args = {
'name': parsed_args.name, 'name': parsed_args.name,
'ram': parsed_args.ram, 'ram': parsed_args.ram,
'vcpus': parsed_args.vcpus, 'vcpus': parsed_args.vcpus,
'disk': parsed_args.disk, 'disk': parsed_args.disk,
'id': parsed_args.id, 'id': flavor_id,
'ephemeral': parsed_args.ephemeral, 'ephemeral': parsed_args.ephemeral,
'swap': parsed_args.swap, 'swap': parsed_args.swap,
'rxtx_factor': parsed_args.rxtx_factor, 'rxtx_factor': parsed_args.rxtx_factor,
@ -190,8 +203,7 @@ class CreateFlavor(command.ShowOne):
compute_client.flavor_add_tenant_access(flavor.id, project_id) compute_client.flavor_add_tenant_access(flavor.id, project_id)
except Exception as e: except Exception as e:
msg = _( msg = _(
"Failed to add project %(project)s access to " "Failed to add project %(project)s access to flavor: %(e)s"
"flavor: %(e)s"
) )
LOG.error(msg, {'project': parsed_args.project, 'e': e}) LOG.error(msg, {'project': parsed_args.project, 'e': e})
if parsed_args.properties: if parsed_args.properties:
@ -224,7 +236,7 @@ class DeleteFlavor(command.Command):
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
compute_client = self.app.client_manager.sdk_connection.compute compute_client = self.app.client_manager.compute
result = 0 result = 0
for f in parsed_args.flavor: for f in parsed_args.flavor:
try: try:
@ -242,7 +254,7 @@ class DeleteFlavor(command.Command):
if result > 0: if result > 0:
total = len(parsed_args.flavor) total = len(parsed_args.flavor)
msg = _("%(result)s of %(total)s flavors failed " "to delete.") % { msg = _("%(result)s of %(total)s flavors failed to delete.") % {
'result': result, 'result': result,
'total': total, 'total': total,
} }
@ -297,7 +309,7 @@ class ListFlavor(command.Lister):
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
compute_client = self.app.client_manager.sdk_connection.compute compute_client = self.app.client_manager.compute
# is_public is ternary - None means give all flavors, # is_public is ternary - None means give all flavors,
# True is public only and False is private only # True is public only and False is private only
# By default Nova assumes True and gives admins public flavors # By default Nova assumes True and gives admins public flavors
@ -330,7 +342,7 @@ class ListFlavor(command.Lister):
if parsed_args.long and not f.extra_specs: if parsed_args.long and not f.extra_specs:
compute_client.fetch_flavor_extra_specs(f) compute_client.fetch_flavor_extra_specs(f)
columns = ( columns: tuple[str, ...] = (
"id", "id",
"name", "name",
"ram", "ram",
@ -346,7 +358,7 @@ class ListFlavor(command.Lister):
"extra_specs", "extra_specs",
) )
column_headers = ( column_headers: tuple[str, ...] = (
"ID", "ID",
"Name", "Name",
"RAM", "RAM",
@ -404,9 +416,7 @@ class SetFlavor(command.Command):
parser.add_argument( parser.add_argument(
'--project', '--project',
metavar='<project>', metavar='<project>',
help=_( help=_('Set flavor access to project (name or ID) (admin only)'),
'Set flavor access to project (name or ID) ' '(admin only)'
),
) )
identity_common.add_project_domain_option_to_parser(parser) identity_common.add_project_domain_option_to_parser(parser)
parser.add_argument( parser.add_argument(
@ -421,7 +431,7 @@ class SetFlavor(command.Command):
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
compute_client = self.app.client_manager.sdk_connection.compute compute_client = self.app.client_manager.compute
identity_client = self.app.client_manager.identity identity_client = self.app.client_manager.identity
try: try:
@ -483,7 +493,7 @@ class SetFlavor(command.Command):
if result > 0: if result > 0:
raise exceptions.CommandError( raise exceptions.CommandError(
_("Command Failed: One or more of" " the operations failed") _("Command Failed: One or more of the operations failed")
) )
@ -500,7 +510,7 @@ class ShowFlavor(command.ShowOne):
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
compute_client = self.app.client_manager.sdk_connection.compute compute_client = self.app.client_manager.compute
flavor = compute_client.find_flavor( flavor = compute_client.find_flavor(
parsed_args.flavor, get_extra_specs=True, ignore_missing=False parsed_args.flavor, get_extra_specs=True, ignore_missing=False
) )
@ -560,8 +570,7 @@ class UnsetFlavor(command.Command):
'--project', '--project',
metavar='<project>', metavar='<project>',
help=_( help=_(
'Remove flavor access from project (name or ID) ' 'Remove flavor access from project (name or ID) (admin only)'
'(admin only)'
), ),
) )
identity_common.add_project_domain_option_to_parser(parser) identity_common.add_project_domain_option_to_parser(parser)
@ -569,7 +578,7 @@ class UnsetFlavor(command.Command):
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
compute_client = self.app.client_manager.sdk_connection.compute compute_client = self.app.client_manager.compute
identity_client = self.app.client_manager.identity identity_client = self.app.client_manager.identity
try: try:
@ -612,5 +621,5 @@ class UnsetFlavor(command.Command):
if result > 0: if result > 0:
raise exceptions.CommandError( raise exceptions.CommandError(
_("Command Failed: One or more of" " the operations failed") _("Command Failed: One or more of the operations failed")
) )

View file

@ -16,9 +16,9 @@
"""Host action implementations""" """Host action implementations"""
from openstack import exceptions as sdk_exceptions from openstack import exceptions as sdk_exceptions
from osc_lib.command import command
from osc_lib import utils from osc_lib import utils
from openstackclient import command
from openstackclient.i18n import _ from openstackclient.i18n import _
@ -35,7 +35,7 @@ class ListHost(command.Lister):
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
compute_client = self.app.client_manager.sdk_connection.compute compute_client = self.app.client_manager.compute
self.log.warning( self.log.warning(
"API has been deprecated; " "API has been deprecated; "
@ -83,7 +83,7 @@ class SetHost(command.Command):
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
compute_client = self.app.client_manager.sdk_connection.compute compute_client = self.app.client_manager.compute
self.log.warning( self.log.warning(
"API has been deprecated; " "API has been deprecated; "
@ -121,7 +121,7 @@ class ShowHost(command.Lister):
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
compute_client = self.app.client_manager.sdk_connection.compute compute_client = self.app.client_manager.compute
self.log.warning( self.log.warning(
"API has been deprecated; " "API has been deprecated; "

View file

@ -21,10 +21,10 @@ import re
from openstack import exceptions as sdk_exceptions from openstack import exceptions as sdk_exceptions
from openstack import utils as sdk_utils from openstack import utils as sdk_utils
from osc_lib.cli import format_columns from osc_lib.cli import format_columns
from osc_lib.command import command
from osc_lib import exceptions from osc_lib import exceptions
from osc_lib import utils from osc_lib import utils
from openstackclient import command
from openstackclient.common import pagination from openstackclient.common import pagination
from openstackclient.i18n import _ from openstackclient.i18n import _
@ -90,7 +90,7 @@ class ListHypervisor(command.Lister):
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
compute_client = self.app.client_manager.sdk_connection.compute compute_client = self.app.client_manager.compute
list_opts = {} list_opts = {}
@ -119,14 +119,20 @@ class ListHypervisor(command.Lister):
if parsed_args.matching: if parsed_args.matching:
list_opts['hypervisor_hostname_pattern'] = parsed_args.matching list_opts['hypervisor_hostname_pattern'] = parsed_args.matching
column_headers = ( column_headers: tuple[str, ...] = (
"ID", "ID",
"Hypervisor Hostname", "Hypervisor Hostname",
"Hypervisor Type", "Hypervisor Type",
"Host IP", "Host IP",
"State", "State",
) )
columns = ('id', 'name', 'hypervisor_type', 'host_ip', 'state') columns: tuple[str, ...] = (
'id',
'name',
'hypervisor_type',
'host_ip',
'state',
)
if parsed_args.long: if parsed_args.long:
if not sdk_utils.supports_microversion(compute_client, '2.88'): if not sdk_utils.supports_microversion(compute_client, '2.88'):
@ -164,7 +170,7 @@ class ShowHypervisor(command.ShowOne):
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
compute_client = self.app.client_manager.sdk_connection.compute compute_client = self.app.client_manager.compute
hypervisor_id = compute_client.find_hypervisor( hypervisor_id = compute_client.find_hypervisor(
parsed_args.hypervisor, ignore_missing=False, details=False parsed_args.hypervisor, ignore_missing=False, details=False

View file

@ -13,9 +13,9 @@
"""Hypervisor Stats action implementations""" """Hypervisor Stats action implementations"""
from osc_lib.command import command
from osc_lib import utils from osc_lib import utils
from openstackclient import command
from openstackclient.i18n import _ from openstackclient.i18n import _
@ -42,7 +42,7 @@ class ShowHypervisorStats(command.ShowOne):
def take_action(self, parsed_args): def take_action(self, parsed_args):
# The command is deprecated since it is being dropped in Nova. # The command is deprecated since it is being dropped in Nova.
self.log.warning(_("This command is deprecated.")) self.log.warning(_("This command is deprecated."))
compute_client = self.app.client_manager.sdk_connection.compute compute_client = self.app.client_manager.compute
# We do API request directly cause this deprecated method is not and # We do API request directly cause this deprecated method is not and
# will not be supported by OpenStackSDK. # will not be supported by OpenStackSDK.
response = compute_client.get( response = compute_client.get(

View file

@ -22,10 +22,10 @@ import os
from cryptography.hazmat.primitives.asymmetric import ed25519 from cryptography.hazmat.primitives.asymmetric import ed25519
from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives import serialization
from openstack import utils as sdk_utils from openstack import utils as sdk_utils
from osc_lib.command import command
from osc_lib import exceptions from osc_lib import exceptions
from osc_lib import utils from osc_lib import utils
from openstackclient import command
from openstackclient.common import pagination from openstackclient.common import pagination
from openstackclient.i18n import _ from openstackclient.i18n import _
from openstackclient.identity import common as identity_common from openstackclient.identity import common as identity_common
@ -61,14 +61,13 @@ def _generate_keypair():
def _get_keypair_columns(item, hide_pub_key=False, hide_priv_key=False): def _get_keypair_columns(item, hide_pub_key=False, hide_priv_key=False):
# To maintain backwards compatibility we need to rename sdk props to # To maintain backwards compatibility we need to rename sdk props to
# whatever OSC was using before # whatever OSC was using before
column_map = {}
hidden_columns = ['links', 'location'] hidden_columns = ['links', 'location']
if hide_pub_key: if hide_pub_key:
hidden_columns.append('public_key') hidden_columns.append('public_key')
if hide_priv_key: if hide_priv_key:
hidden_columns.append('private_key') hidden_columns.append('private_key')
return utils.get_osc_show_columns_for_sdk_resource( return utils.get_osc_show_columns_for_sdk_resource(
item, column_map, hidden_columns item, {}, hidden_columns
) )
@ -121,13 +120,12 @@ class CreateKeypair(command.ShowOne):
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
compute_client = self.app.client_manager.sdk_connection.compute compute_client = self.app.client_manager.compute
identity_client = self.app.client_manager.identity identity_client = self.app.client_manager.identity
kwargs = {'name': parsed_args.name} kwargs = {'name': parsed_args.name}
if parsed_args.public_key: if parsed_args.public_key:
generated_keypair = None
try: try:
with open(os.path.expanduser(parsed_args.public_key)) as p: with open(os.path.expanduser(parsed_args.public_key)) as p:
public_key = p.read() public_key = p.read()
@ -230,7 +228,7 @@ class DeleteKeypair(command.Command):
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
compute_client = self.app.client_manager.sdk_connection.compute compute_client = self.app.client_manager.compute
identity_client = self.app.client_manager.identity identity_client = self.app.client_manager.identity
kwargs = {} kwargs = {}
@ -258,13 +256,13 @@ class DeleteKeypair(command.Command):
except Exception as e: except Exception as e:
result += 1 result += 1
LOG.error( LOG.error(
_("Failed to delete key with name " "'%(name)s': %(e)s"), _("Failed to delete key with name '%(name)s': %(e)s"),
{'name': n, 'e': e}, {'name': n, 'e': e},
) )
if result > 0: if result > 0:
total = len(parsed_args.name) total = len(parsed_args.name)
msg = _("%(result)s of %(total)s keys failed " "to delete.") % { msg = _("%(result)s of %(total)s keys failed to delete.") % {
'result': result, 'result': result,
'total': total, 'total': total,
} }
@ -300,8 +298,9 @@ class ListKeypair(command.Lister):
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
compute_client = self.app.client_manager.sdk_connection.compute compute_client = self.app.client_manager.compute
identity_client = self.app.client_manager.identity identity_client = self.app.client_manager.identity
identity_sdk_client = self.app.client_manager.sdk_connection.identity
kwargs = {} kwargs = {}
@ -347,11 +346,17 @@ class ListKeypair(command.Lister):
parsed_args.project, parsed_args.project,
parsed_args.project_domain, parsed_args.project_domain,
).id ).id
users = identity_client.users.list(tenant_id=project) assignments = identity_sdk_client.role_assignments(
scope_project_id=project
)
user_ids = set()
for assignment in assignments:
if assignment.user:
user_ids.add(assignment.user['id'])
data = [] data = []
for user in users: for user_id in user_ids:
kwargs['user_id'] = user.id kwargs['user_id'] = user_id
data.extend(compute_client.keypairs(**kwargs)) data.extend(compute_client.keypairs(**kwargs))
elif parsed_args.user: elif parsed_args.user:
if not sdk_utils.supports_microversion(compute_client, '2.10'): if not sdk_utils.supports_microversion(compute_client, '2.10'):
@ -372,7 +377,7 @@ class ListKeypair(command.Lister):
else: else:
data = compute_client.keypairs(**kwargs) data = compute_client.keypairs(**kwargs)
columns = ("Name", "Fingerprint") columns: tuple[str, ...] = ("Name", "Fingerprint")
if sdk_utils.supports_microversion(compute_client, '2.2'): if sdk_utils.supports_microversion(compute_client, '2.2'):
columns += ("Type",) columns += ("Type",)
@ -411,7 +416,7 @@ class ShowKeypair(command.ShowOne):
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
compute_client = self.app.client_manager.sdk_connection.compute compute_client = self.app.client_manager.compute
identity_client = self.app.client_manager.identity identity_client = self.app.client_manager.identity
kwargs = {} kwargs = {}

View file

@ -29,11 +29,12 @@ from openstack import exceptions as sdk_exceptions
from openstack import utils as sdk_utils from openstack import utils as sdk_utils
from osc_lib.cli import format_columns from osc_lib.cli import format_columns
from osc_lib.cli import parseractions from osc_lib.cli import parseractions
from osc_lib.command import command
from osc_lib import exceptions from osc_lib import exceptions
from osc_lib import utils from osc_lib import utils
from openstackclient.api import compute_v2 from openstackclient.api import compute_v2
from openstackclient import command
from openstackclient.common import envvars
from openstackclient.common import pagination from openstackclient.common import pagination
from openstackclient.i18n import _ from openstackclient.i18n import _
from openstackclient.identity import common as identity_common from openstackclient.identity import common as identity_common
@ -44,7 +45,7 @@ LOG = logging.getLogger(__name__)
IMAGE_STRING_FOR_BFV = 'N/A (booted from volume)' IMAGE_STRING_FOR_BFV = 'N/A (booted from volume)'
class PowerStateColumn(cliff_columns.FormattableColumn): class PowerStateColumn(cliff_columns.FormattableColumn[int]):
"""Generate a formatted string of a server's power state.""" """Generate a formatted string of a server's power state."""
power_states = [ power_states = [
@ -65,7 +66,7 @@ class PowerStateColumn(cliff_columns.FormattableColumn):
return 'N/A' return 'N/A'
class AddressesColumn(cliff_columns.FormattableColumn): class AddressesColumn(cliff_columns.FormattableColumn[ty.Any]):
"""Generate a formatted string of a server's addresses.""" """Generate a formatted string of a server's addresses."""
def human_readable(self): def human_readable(self):
@ -86,7 +87,7 @@ class AddressesColumn(cliff_columns.FormattableColumn):
} }
class HostColumn(cliff_columns.FormattableColumn): class HostColumn(cliff_columns.FormattableColumn[str | None]):
"""Generate a formatted string of a hostname.""" """Generate a formatted string of a hostname."""
def human_readable(self): def human_readable(self):
@ -183,8 +184,17 @@ def _prep_server_detail(compute_client, image_client, server, *, refresh=True):
'updated_at': 'updated', 'updated_at': 'updated',
'user_data': 'OS-EXT-SRV-ATTR:user_data', 'user_data': 'OS-EXT-SRV-ATTR:user_data',
'vm_state': 'OS-EXT-STS:vm_state', 'vm_state': 'OS-EXT-STS:vm_state',
'pinned_availability_zone': 'pinned_availability_zone',
} }
# NOTE(ratailor): microversion 2.96 introduces
# pinned_availability_zone support
if sdk_utils.supports_microversion(compute_client, '2.96'):
column_map['pinned_availability_zone'] = 'pinned_availability_zone'
# NOTE(ratailor): microversion 2.100 introduces
# scheduler_hints support
if sdk_utils.supports_microversion(compute_client, '2.100'):
column_map['scheduler_hints'] = 'scheduler_hints'
# Some columns returned by openstacksdk should not be shown because they're # Some columns returned by openstacksdk should not be shown because they're
# either irrelevant or duplicates # either irrelevant or duplicates
ignored_columns = { ignored_columns = {
@ -204,7 +214,6 @@ def _prep_server_detail(compute_client, image_client, server, *, refresh=True):
'min_count', 'min_count',
'networks', 'networks',
'personality', 'personality',
'scheduler_hints',
# aliases # aliases
'volumes', 'volumes',
# unnecessary # unnecessary
@ -235,6 +244,16 @@ def _prep_server_detail(compute_client, image_client, server, *, refresh=True):
info = data info = data
# NOTE(dviroel): microversion 2.100 is now retrieving scheduler_hints
# content from request_spec on detailed responses
if not sdk_utils.supports_microversion(compute_client, '2.100'):
info.pop('scheduler_hints', None)
# NOTE(ratailor): microversion 2.96 introduces
# pinned_availability_zone support
if not sdk_utils.supports_microversion(compute_client, '2.96'):
info.pop('pinned_availability_zone', None)
# Convert the image blob to a name # Convert the image blob to a name
image_info = info.get('image', {}) image_info = info.get('image', {})
if image_info and any(image_info.values()): if image_info and any(image_info.values()):
@ -321,51 +340,15 @@ def _prep_server_detail(compute_client, image_client, server, *, refresh=True):
info['OS-EXT-STS:power_state'] info['OS-EXT-STS:power_state']
) )
if sdk_utils.supports_microversion(compute_client, '2.100'):
if 'scheduler_hints' in info:
info['scheduler_hints'] = format_columns.DictListColumn(
info.pop('scheduler_hints', {}),
)
return info return info
def bool_from_str(value, strict=False):
true_strings = ('1', 't', 'true', 'on', 'y', 'yes')
false_strings = ('0', 'f', 'false', 'off', 'n', 'no')
if isinstance(value, bool):
return value
lowered = value.strip().lower()
if lowered in true_strings:
return True
elif lowered in false_strings or not strict:
return False
msg = _(
"Unrecognized value '%(value)s'; acceptable values are: %(valid)s"
) % {
'value': value,
'valid': ', '.join(
f"'{s}'" for s in sorted(true_strings + false_strings)
),
}
raise ValueError(msg)
def boolenv(*vars, default=False):
"""Search for the first defined of possibly many bool-like env vars.
Returns the first environment variable defined in vars, or returns the
default.
:param vars: Arbitrary strings to search for. Case sensitive.
:param default: The default to return if no value found.
:returns: A boolean corresponding to the value found, else the default if
no value found.
"""
for v in vars:
value = os.environ.get(v, None)
if value:
return bool_from_str(value)
return default
class AddFixedIP(command.ShowOne): class AddFixedIP(command.ShowOne):
_description = _("Add fixed IP address to server") _description = _("Add fixed IP address to server")
@ -399,7 +382,7 @@ class AddFixedIP(command.ShowOne):
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
compute_client = self.app.client_manager.sdk_connection.compute compute_client = self.app.client_manager.compute
server = compute_client.find_server( server = compute_client.find_server(
parsed_args.server, ignore_missing=False parsed_args.server, ignore_missing=False
) )
@ -430,7 +413,7 @@ class AddFixedIP(command.ShowOne):
interface = compute_client.create_server_interface(server.id, **kwargs) interface = compute_client.create_server_interface(server.id, **kwargs)
columns = ( columns: tuple[str, ...] = (
'port_id', 'port_id',
'server_id', 'server_id',
'net_id', 'net_id',
@ -438,7 +421,7 @@ class AddFixedIP(command.ShowOne):
'port_state', 'port_state',
'fixed_ips', 'fixed_ips',
) )
column_headers = ( column_headers: tuple[str, ...] = (
'Port ID', 'Port ID',
'Server ID', 'Server ID',
'Network ID', 'Network ID',
@ -493,7 +476,7 @@ class AddFloatingIP(network_common.NetworkAndComputeCommand):
return parser return parser
def take_action_network(self, client, parsed_args): def take_action_network(self, client, parsed_args):
compute_client = self.app.client_manager.sdk_connection.compute compute_client = self.app.client_manager.compute
attrs = {} attrs = {}
obj = client.find_ip( obj = client.find_ip(
@ -586,7 +569,7 @@ class AddPort(command.Command):
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
compute_client = self.app.client_manager.sdk_connection.compute compute_client = self.app.client_manager.compute
server = compute_client.find_server( server = compute_client.find_server(
parsed_args.server, ignore_missing=False parsed_args.server, ignore_missing=False
@ -640,7 +623,7 @@ class AddNetwork(command.Command):
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
compute_client = self.app.client_manager.sdk_connection.compute compute_client = self.app.client_manager.compute
server = compute_client.find_server( server = compute_client.find_server(
parsed_args.server, ignore_missing=False parsed_args.server, ignore_missing=False
@ -691,7 +674,7 @@ class AddServerSecurityGroup(command.Command):
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
compute_client = self.app.client_manager.sdk_connection.compute compute_client = self.app.client_manager.compute
server = compute_client.find_server( server = compute_client.find_server(
parsed_args.server, ignore_missing=False parsed_args.server, ignore_missing=False
@ -714,7 +697,8 @@ class AddServerSecurityGroup(command.Command):
for security_group in security_groups: for security_group in security_groups:
try: try:
compute_client.add_security_group_to_server( compute_client.add_security_group_to_server(
server, security_group server,
{'name': security_group},
) )
except sdk_exceptions.HttpException as e: except sdk_exceptions.HttpException as e:
errors += 1 errors += 1
@ -792,7 +776,7 @@ with status ``SHELVED`` or ``SHELVED_OFFLOADED``."""
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
compute_client = self.app.client_manager.sdk_connection.compute compute_client = self.app.client_manager.compute
volume_client = self.app.client_manager.sdk_connection.volume volume_client = self.app.client_manager.sdk_connection.volume
server = compute_client.find_server( server = compute_client.find_server(
@ -841,8 +825,13 @@ with status ``SHELVED`` or ``SHELVED_OFFLOADED``."""
**kwargs, **kwargs,
) )
columns = ('id', 'server id', 'volume id', 'device') columns: tuple[str, ...] = ('id', 'server id', 'volume id', 'device')
column_headers = ('ID', 'Server ID', 'Volume ID', 'Device') column_headers: tuple[str, ...] = (
'ID',
'Server ID',
'Volume ID',
'Device',
)
if sdk_utils.supports_microversion(compute_client, '2.49'): if sdk_utils.supports_microversion(compute_client, '2.49'):
columns += ('tag',) columns += ('tag',)
column_headers += ('Tag',) column_headers += ('Tag',)
@ -1026,7 +1015,6 @@ class BDMLegacyAction(argparse.Action):
class BDMAction(parseractions.MultiKeyValueAction): class BDMAction(parseractions.MultiKeyValueAction):
def __init__(self, option_strings, dest, **kwargs): def __init__(self, option_strings, dest, **kwargs):
required_keys = []
optional_keys = [ optional_keys = [
'uuid', 'uuid',
'source_type', 'source_type',
@ -1044,7 +1032,7 @@ class BDMAction(parseractions.MultiKeyValueAction):
super().__init__( super().__init__(
option_strings, option_strings,
dest, dest,
required_keys=required_keys, required_keys=[],
optional_keys=optional_keys, optional_keys=optional_keys,
**kwargs, **kwargs,
) )
@ -1356,14 +1344,26 @@ class CreateServer(command.ShowOne):
'This option requires cloud support.' 'This option requires cloud support.'
), ),
) )
parser.add_argument( secgroups = parser.add_mutually_exclusive_group()
secgroups.add_argument(
'--no-security-group',
dest='security_groups',
action='store_const',
const=[],
help=_(
'Do not associate a security group with ports attached to '
'this server. This does not affect the security groups '
'associated with pre-existing ports.'
),
)
secgroups.add_argument(
'--security-group', '--security-group',
metavar='<security-group>', metavar='<security-group>',
action='append', action='append',
default=[],
dest='security_groups', dest='security_groups',
help=_( help=_(
'Security group to assign to this server (name or ID) ' 'Security group to associate with ports attached to this '
'server (name or ID) '
'(repeat option to set multiple groups)' '(repeat option to set multiple groups)'
), ),
) )
@ -1541,7 +1541,7 @@ class CreateServer(command.ShowOne):
self.app.stdout.write(f'\rProgress: {progress}') self.app.stdout.write(f'\rProgress: {progress}')
self.app.stdout.flush() self.app.stdout.flush()
compute_client = self.app.client_manager.sdk_connection.compute compute_client = self.app.client_manager.compute
volume_client = self.app.client_manager.volume volume_client = self.app.client_manager.volume
image_client = self.app.client_manager.image image_client = self.app.client_manager.image
@ -1604,8 +1604,7 @@ class CreateServer(command.ShowOne):
image = images[0] image = images[0]
else: else:
msg = _( msg = _(
'No images match the property expected by ' 'No images match the property expected by --image-property'
'--image-property'
) )
raise exceptions.CommandError(msg) raise exceptions.CommandError(msg)
@ -1860,7 +1859,7 @@ class CreateServer(command.ShowOne):
if 'delete_on_termination' in mapping: if 'delete_on_termination' in mapping:
try: try:
value = bool_from_str( value = envvars.bool_from_str(
mapping['delete_on_termination'], mapping['delete_on_termination'],
strict=True, strict=True,
) )
@ -1889,7 +1888,7 @@ class CreateServer(command.ShowOne):
# Default to empty list if nothing was specified and let nova # Default to empty list if nothing was specified and let nova
# decide the default behavior. # decide the default behavior.
networks: ty.Union[str, ty.List[ty.Dict[str, str]], None] = [] networks: str | list[dict[str, str]] | None = []
if 'auto' in parsed_args.nics or 'none' in parsed_args.nics: if 'auto' in parsed_args.nics or 'none' in parsed_args.nics:
if len(parsed_args.nics) > 1: if len(parsed_args.nics) > 1:
@ -1954,7 +1953,7 @@ class CreateServer(command.ShowOne):
# convert from the novaclient-derived "NIC" view to the actual # convert from the novaclient-derived "NIC" view to the actual
# "network" view # "network" view
network = {} network: dict[str, str] = {}
if nic['net-id']: if nic['net-id']:
network['uuid'] = nic['net-id'] network['uuid'] = nic['net-id']
@ -1963,14 +1962,14 @@ class CreateServer(command.ShowOne):
network['port'] = nic['port-id'] network['port'] = nic['port-id']
if nic['v4-fixed-ip']: if nic['v4-fixed-ip']:
network['fixed'] = nic['v4-fixed-ip'] network['fixed_ip'] = nic['v4-fixed-ip']
elif nic['v6-fixed-ip']: elif nic['v6-fixed-ip']:
network['fixed'] = nic['v6-fixed-ip'] network['fixed_ip'] = nic['v6-fixed-ip']
if nic.get('tag'): # tags are optional if nic.get('tag'): # tags are optional
network['tag'] = nic['tag'] network['tag'] = nic['tag']
networks.append(network) networks.append(network) # type: ignore[union-attr]
if not parsed_args.nics and sdk_utils.supports_microversion( if not parsed_args.nics and sdk_utils.supports_microversion(
compute_client, '2.37' compute_client, '2.37'
@ -1980,6 +1979,8 @@ class CreateServer(command.ShowOne):
networks = 'auto' networks = 'auto'
# Check security group(s) exist and convert ID to name # Check security group(s) exist and convert ID to name
security_groups = None
if parsed_args.security_groups is not None:
security_groups = [] security_groups = []
if self.app.client_manager.is_network_endpoint_enabled(): if self.app.client_manager.is_network_endpoint_enabled():
network_client = self.app.client_manager.network network_client = self.app.client_manager.network
@ -1987,8 +1988,8 @@ class CreateServer(command.ShowOne):
sg = network_client.find_security_group( sg = network_client.find_security_group(
security_group, ignore_missing=False security_group, ignore_missing=False
) )
# Use security group ID to avoid multiple security group have # Use security group ID to avoid multiple security group
# same name in neutron networking backend # have same name in neutron networking backend
security_groups.append({'name': sg.id}) security_groups.append({'name': sg.id})
else: # nova-network else: # nova-network
for security_group in parsed_args.security_groups: for security_group in parsed_args.security_groups:
@ -2058,7 +2059,7 @@ class CreateServer(command.ShowOne):
if files: if files:
kwargs['personality'] = files kwargs['personality'] = files
if security_groups: if security_groups is not None:
kwargs['security_groups'] = security_groups kwargs['security_groups'] = security_groups
if block_device_mapping_v2: if block_device_mapping_v2:
@ -2143,13 +2144,11 @@ class CreateServer(command.ShowOne):
f.close() f.close()
if parsed_args.wait: if parsed_args.wait:
if utils.wait_for_status( if not utils.wait_for_status(
compute_client.get_server, compute_client.get_server,
server.id, server.id,
callback=_show_progress, callback=_show_progress,
): ):
self.app.stdout.write('\n')
else:
msg = _('Error creating server: %s') % parsed_args.server_name msg = _('Error creating server: %s') % parsed_args.server_name
raise exceptions.CommandError(msg) raise exceptions.CommandError(msg)
@ -2180,9 +2179,11 @@ class CreateServerDump(command.Command):
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
compute_client = self.app.client_manager.sdk_connection.compute compute_client = self.app.client_manager.compute
for name_or_id in parsed_args.server: for name_or_id in parsed_args.server:
server = compute_client.find_server(name_or_id) server = compute_client.find_server(
name_or_id, ignore_missing=False
)
server.trigger_crash_dump(compute_client) server.trigger_crash_dump(compute_client)
@ -2205,7 +2206,7 @@ class DeleteServer(command.Command):
parser.add_argument( parser.add_argument(
'--all-projects', '--all-projects',
action='store_true', action='store_true',
default=boolenv('ALL_PROJECTS'), default=envvars.boolenv('ALL_PROJECTS'),
help=_( help=_(
'Delete server(s) in another project by name (admin only)' 'Delete server(s) in another project by name (admin only)'
'(can be specified using the ALL_PROJECTS envvar)' '(can be specified using the ALL_PROJECTS envvar)'
@ -2224,23 +2225,48 @@ class DeleteServer(command.Command):
self.app.stdout.write(f'\rProgress: {progress}') self.app.stdout.write(f'\rProgress: {progress}')
self.app.stdout.flush() self.app.stdout.flush()
compute_client = self.app.client_manager.sdk_connection.compute compute_client = self.app.client_manager.compute
deleted_servers = []
for server in parsed_args.server: for server in parsed_args.server:
try:
server_obj = compute_client.find_server( server_obj = compute_client.find_server(
server, server,
ignore_missing=False, ignore_missing=False,
all_projects=parsed_args.all_projects, all_projects=parsed_args.all_projects,
) )
compute_client.delete_server(server_obj, force=parsed_args.force) compute_client.delete_server(
server_obj, force=parsed_args.force
)
deleted_servers.append(server_obj)
except Exception as e:
LOG.error(
_(
"Failed to delete server with "
"name or ID '%(server)s': %(e)s"
),
{'server': server, 'e': e},
)
if parsed_args.wait: if parsed_args.wait:
for server_obj in deleted_servers:
try: try:
compute_client.wait_for_delete( compute_client.wait_for_delete(
server_obj, callback=_show_progress server_obj, callback=_show_progress
) )
except sdk_exceptions.ResourceTimeout: except sdk_exceptions.ResourceTimeout:
msg = _('Error deleting server: %s') % server_obj.id msg = _('Error deleting server: %s') % server_obj.id
deleted_servers.remove(server_obj)
raise exceptions.CommandError(msg)
fails = len(parsed_args.server) - len(deleted_servers)
if fails > 0:
total = len(parsed_args.server)
msg = _("%(fails)s of %(total)s servers failed to delete.") % {
'fails': fails,
'total': total,
}
raise exceptions.CommandError(msg) raise exceptions.CommandError(msg)
@ -2371,7 +2397,7 @@ class ListServer(command.Lister):
parser.add_argument( parser.add_argument(
'--all-projects', '--all-projects',
action='store_true', action='store_true',
default=boolenv('ALL_PROJECTS'), default=envvars.boolenv('ALL_PROJECTS'),
help=_( help=_(
'Include all projects (admin only) ' 'Include all projects (admin only) '
'(can be specified using the ALL_PROJECTS envvar)' '(can be specified using the ALL_PROJECTS envvar)'
@ -2409,8 +2435,7 @@ class ListServer(command.Lister):
parser.add_argument( parser.add_argument(
'--key-name', '--key-name',
help=_( help=_(
'Search by keypair name ' 'Search by keypair name (admin only before microversion 2.83)'
'(admin only before microversion 2.83)'
), ),
) )
config_drive_group = parser.add_mutually_exclusive_group() config_drive_group = parser.add_mutually_exclusive_group()
@ -2633,7 +2658,7 @@ class ListServer(command.Lister):
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
compute_client = self.app.client_manager.sdk_connection.compute compute_client = self.app.client_manager.compute
identity_client = self.app.client_manager.identity identity_client = self.app.client_manager.identity
image_client = self.app.client_manager.image image_client = self.app.client_manager.image
@ -2789,12 +2814,12 @@ class ListServer(command.Lister):
msg % search_opts['changes-since'] msg % search_opts['changes-since']
) )
columns = ( columns: tuple[str, ...] = (
'id', 'id',
'name', 'name',
'status', 'status',
) )
column_headers = ( column_headers: tuple[str, ...] = (
'ID', 'ID',
'Name', 'Name',
'Status', 'Status',
@ -2855,16 +2880,25 @@ class ListServer(command.Lister):
if parsed_args.long: if parsed_args.long:
columns += ( columns += (
'availability_zone', 'availability_zone',
'pinned_availability_zone',
'hypervisor_hostname', 'hypervisor_hostname',
'metadata', 'metadata',
) )
column_headers += ( column_headers += (
'Availability Zone', 'Availability Zone',
'Pinned Availability Zone',
'Host', 'Host',
'Properties', 'Properties',
) )
if sdk_utils.supports_microversion(compute_client, '2.96'):
columns += ('pinned_availability_zone',)
column_headers += ('Pinned Availability Zone',)
if sdk_utils.supports_microversion(compute_client, '2.100'):
columns += ('scheduler_hints',)
column_headers += ('Scheduler Hints',)
if parsed_args.all_projects:
columns += ('project_id',)
column_headers += ('Project ID',)
# support for additional columns # support for additional columns
if parsed_args.columns: if parsed_args.columns:
@ -2898,9 +2932,10 @@ class ListServer(command.Lister):
column_headers += ('Availability Zone',) column_headers += ('Availability Zone',)
if c in ( if c in (
'pinned_availability_zone', 'pinned_availability_zone',
"Pinned Availability Zone", 'Pinned Availability Zone',
): ):
columns += ('Pinned Availability Zone',) if sdk_utils.supports_microversion(compute_client, '2.96'):
columns += ('pinned_availability_zone',)
column_headers += ('Pinned Availability Zone',) column_headers += ('Pinned Availability Zone',)
if c in ('Host', "host"): if c in ('Host', "host"):
columns += ('hypervisor_hostname',) columns += ('hypervisor_hostname',)
@ -2908,6 +2943,15 @@ class ListServer(command.Lister):
if c in ('Properties', "properties"): if c in ('Properties', "properties"):
columns += ('Metadata',) columns += ('Metadata',)
column_headers += ('Properties',) column_headers += ('Properties',)
if c in (
'scheduler_hints',
"Scheduler Hints",
):
if sdk_utils.supports_microversion(
compute_client, '2.100'
):
columns += ('scheduler_hints',)
column_headers += ('Scheduler Hints',)
# remove duplicates # remove duplicates
column_headers = tuple(dict.fromkeys(column_headers)) column_headers = tuple(dict.fromkeys(column_headers))
@ -3010,7 +3054,7 @@ class ListServer(command.Lister):
# infrastructure failure situations. # infrastructure failure situations.
# For those servers with partial constructs we just skip the # For those servers with partial constructs we just skip the
# processing of the image and flavor information. # processing of the image and flavor information.
if not hasattr(s, 'image') or not hasattr(s, 'flavor'): if getattr(s, 'status') == 'UNKNOWN':
continue continue
if 'id' in s.image and s.image.id is not None: if 'id' in s.image and s.image.id is not None:
@ -3074,6 +3118,7 @@ class ListServer(command.Lister):
'metadata': format_columns.DictColumn, 'metadata': format_columns.DictColumn,
'security_groups_name': format_columns.ListColumn, 'security_groups_name': format_columns.ListColumn,
'hypervisor_hostname': HostColumn, 'hypervisor_hostname': HostColumn,
'scheduler_hints': format_columns.DictListColumn,
}, },
) )
for s in data for s in data
@ -3109,7 +3154,7 @@ A non-admin user will not be able to execute actions."""
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
compute_client = self.app.client_manager.sdk_connection.compute compute_client = self.app.client_manager.compute
kwargs = {} kwargs = {}
if parsed_args.reason: if parsed_args.reason:
@ -3233,7 +3278,7 @@ revert to release the new server and restart the old one."""
self.app.stdout.write(f'\rProgress: {progress}') self.app.stdout.write(f'\rProgress: {progress}')
self.app.stdout.flush() self.app.stdout.flush()
compute_client = self.app.client_manager.sdk_connection.compute compute_client = self.app.client_manager.compute
server = compute_client.find_server( server = compute_client.find_server(
parsed_args.server, ignore_missing=False parsed_args.server, ignore_missing=False
@ -3336,7 +3381,7 @@ class PauseServer(command.Command):
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
compute_client = self.app.client_manager.sdk_connection.compute compute_client = self.app.client_manager.compute
for server in parsed_args.server: for server in parsed_args.server:
server_id = compute_client.find_server( server_id = compute_client.find_server(
server, server,
@ -3385,7 +3430,7 @@ class RebootServer(command.Command):
self.app.stdout.write(f'\rProgress: {progress}') self.app.stdout.write(f'\rProgress: {progress}')
self.app.stdout.flush() self.app.stdout.flush()
compute_client = self.app.client_manager.sdk_connection.compute compute_client = self.app.client_manager.compute
server_id = compute_client.find_server( server_id = compute_client.find_server(
parsed_args.server, parsed_args.server,
ignore_missing=False, ignore_missing=False,
@ -3590,7 +3635,7 @@ class RebuildServer(command.ShowOne):
self.app.stdout.write(f'\rProgress: {progress}') self.app.stdout.write(f'\rProgress: {progress}')
self.app.stdout.flush() self.app.stdout.flush()
compute_client = self.app.client_manager.sdk_connection.compute compute_client = self.app.client_manager.compute
image_client = self.app.client_manager.image image_client = self.app.client_manager.image
server = compute_client.find_server( server = compute_client.find_server(
@ -3851,7 +3896,7 @@ host."""
self.app.stdout.write(f'\rProgress: {progress}') self.app.stdout.write(f'\rProgress: {progress}')
self.app.stdout.flush() self.app.stdout.flush()
compute_client = self.app.client_manager.sdk_connection.compute compute_client = self.app.client_manager.compute
image_client = self.app.client_manager.image image_client = self.app.client_manager.image
if parsed_args.host: if parsed_args.host:
@ -3884,9 +3929,15 @@ host."""
compute_client.evacuate_server(server, **kwargs) compute_client.evacuate_server(server, **kwargs)
if parsed_args.wait: if parsed_args.wait:
orig_status = server.status
success = ['ACTIVE']
if orig_status == 'SHUTOFF':
success.append('SHUTOFF')
if utils.wait_for_status( if utils.wait_for_status(
compute_client.get_server, compute_client.get_server,
server.id, server.id,
success_status=success,
callback=_show_progress, callback=_show_progress,
): ):
self.app.stdout.write(_('Complete\n')) self.app.stdout.write(_('Complete\n'))
@ -3916,7 +3967,7 @@ class RemoveFixedIP(command.Command):
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
compute_client = self.app.client_manager.sdk_connection.compute compute_client = self.app.client_manager.compute
server = compute_client.find_server( server = compute_client.find_server(
parsed_args.server, ignore_missing=False parsed_args.server, ignore_missing=False
@ -3945,14 +3996,12 @@ class RemoveFloatingIP(network_common.NetworkAndComputeCommand):
return parser return parser
def take_action_network(self, client, parsed_args): def take_action_network(self, client, parsed_args):
attrs = {}
obj = client.find_ip( obj = client.find_ip(
parsed_args.ip_address, parsed_args.ip_address,
ignore_missing=False, ignore_missing=False,
) )
attrs['port_id'] = None
client.update_ip(obj, **attrs) client.update_ip(obj, port_id=None)
def take_action_compute(self, client, parsed_args): def take_action_compute(self, client, parsed_args):
server = client.find_server(parsed_args.server, ignore_missing=False) server = client.find_server(parsed_args.server, ignore_missing=False)
@ -3977,7 +4026,7 @@ class RemovePort(command.Command):
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
compute_client = self.app.client_manager.sdk_connection.compute compute_client = self.app.client_manager.compute
server = compute_client.find_server( server = compute_client.find_server(
parsed_args.server, ignore_missing=False parsed_args.server, ignore_missing=False
@ -4016,7 +4065,7 @@ class RemoveNetwork(command.Command):
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
compute_client = self.app.client_manager.sdk_connection.compute compute_client = self.app.client_manager.compute
server = compute_client.find_server( server = compute_client.find_server(
parsed_args.server, ignore_missing=False parsed_args.server, ignore_missing=False
@ -4060,7 +4109,7 @@ class RemoveServerSecurityGroup(command.Command):
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
compute_client = self.app.client_manager.sdk_connection.compute compute_client = self.app.client_manager.compute
server = compute_client.find_server( server = compute_client.find_server(
parsed_args.server, ignore_missing=False parsed_args.server, ignore_missing=False
@ -4083,7 +4132,8 @@ class RemoveServerSecurityGroup(command.Command):
for security_group in security_groups: for security_group in security_groups:
try: try:
compute_client.remove_security_group_from_server( compute_client.remove_security_group_from_server(
server, security_group server,
{'name': security_group},
) )
except sdk_exceptions.HttpException as e: except sdk_exceptions.HttpException as e:
errors += 1 errors += 1
@ -4129,7 +4179,7 @@ volume from a server with status ``SHELVED`` or ``SHELVED_OFFLOADED``."""
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
compute_client = self.app.client_manager.sdk_connection.compute compute_client = self.app.client_manager.compute
volume_client = self.app.client_manager.sdk_connection.volume volume_client = self.app.client_manager.sdk_connection.volume
server = compute_client.find_server( server = compute_client.find_server(
@ -4182,7 +4232,7 @@ server booted from a volume."""
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
compute_client = self.app.client_manager.sdk_connection.compute compute_client = self.app.client_manager.compute
image_client = self.app.client_manager.image image_client = self.app.client_manager.image
image_ref = None image_ref = None
@ -4236,7 +4286,7 @@ release the new server and restart the old one."""
'--revert', '--revert',
action="store_true", action="store_true",
help=_( help=_(
'**Deprecated** Restore server state before resize' '**Deprecated** Restore server state before resize. '
"Replaced by the 'openstack server resize revert' and " "Replaced by the 'openstack server resize revert' and "
"'openstack server migration revert' commands" "'openstack server migration revert' commands"
), ),
@ -4254,7 +4304,7 @@ release the new server and restart the old one."""
self.app.stdout.write(f'\rProgress: {progress}') self.app.stdout.write(f'\rProgress: {progress}')
self.app.stdout.flush() self.app.stdout.flush()
compute_client = self.app.client_manager.sdk_connection.compute compute_client = self.app.client_manager.compute
server = compute_client.find_server( server = compute_client.find_server(
parsed_args.server, ignore_missing=False parsed_args.server, ignore_missing=False
) )
@ -4316,7 +4366,7 @@ Confirm (verify) success of resize operation and release the old server."""
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
compute_client = self.app.client_manager.sdk_connection.compute compute_client = self.app.client_manager.compute
server = compute_client.find_server( server = compute_client.find_server(
parsed_args.server, ignore_missing=False parsed_args.server, ignore_missing=False
) )
@ -4364,7 +4414,7 @@ one."""
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
compute_client = self.app.client_manager.sdk_connection.compute compute_client = self.app.client_manager.compute
server = compute_client.find_server( server = compute_client.find_server(
parsed_args.server, ignore_missing=False parsed_args.server, ignore_missing=False
) )
@ -4408,7 +4458,7 @@ class RestoreServer(command.Command):
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
compute_client = self.app.client_manager.sdk_connection.compute compute_client = self.app.client_manager.compute
for server in parsed_args.server: for server in parsed_args.server:
server_id = compute_client.find_server( server_id = compute_client.find_server(
server, server,
@ -4431,7 +4481,7 @@ class ResumeServer(command.Command):
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
compute_client = self.app.client_manager.sdk_connection.compute compute_client = self.app.client_manager.compute
for server in parsed_args.server: for server in parsed_args.server:
server_id = compute_client.find_server( server_id = compute_client.find_server(
server, server,
@ -4459,8 +4509,7 @@ class SetServer(command.Command):
password_group.add_argument( password_group.add_argument(
'--password', '--password',
help=_( help=_(
'Set the server password. ' 'Set the server password. This option requires cloud support.'
'This option requires cloud support.'
), ),
) )
password_group.add_argument( password_group.add_argument(
@ -4488,15 +4537,27 @@ class SetServer(command.Command):
'(repeat option to set multiple properties)' '(repeat option to set multiple properties)'
), ),
) )
parser.add_argument(
'--auto-approve',
action='store_true',
help=_(
"Allow server state override without asking for confirmation"
),
)
parser.add_argument( parser.add_argument(
'--state', '--state',
metavar='<state>', metavar='<state>',
choices=['active', 'error'], choices=['active', 'error'],
help=_( help=_(
'New server state ' 'New server state.'
'**WARNING** This can result in instances that are no longer ' '**WARNING** Resetting the state is intended to work around '
'usable and should be used with caution ' 'servers stuck in an intermediate state, such as deleting. '
'(admin only)' 'If the server is in an error state then this is almost '
'never the correct command to run and you should prefer hard '
'reboot where possible. In particular, if the server is in '
'an error state due to a move operation, setting the state '
'can result in instances that are no longer usable. Proceed '
'with caution. (admin only)'
), ),
) )
parser.add_argument( parser.add_argument(
@ -4531,8 +4592,22 @@ class SetServer(command.Command):
) )
return parser return parser
@staticmethod
def ask_user_yesno(msg):
"""Ask user Y/N question
:param str msg: question text
:return bool: User choice
"""
while True:
answer = getpass.getpass('{} [{}]: '.format(msg, 'y/n'))
if answer in ('y', 'Y', 'yes'):
return True
elif answer in ('n', 'N', 'no'):
return False
def take_action(self, parsed_args): def take_action(self, parsed_args):
compute_client = self.app.client_manager.sdk_connection.compute compute_client = self.app.client_manager.compute
server = compute_client.find_server( server = compute_client.find_server(
parsed_args.server, ignore_missing=False parsed_args.server, ignore_missing=False
) )
@ -4581,6 +4656,17 @@ class SetServer(command.Command):
) )
if parsed_args.state: if parsed_args.state:
if not parsed_args.auto_approve:
if not self.ask_user_yesno(
_(
"Resetting the server state can make it much harder "
"to recover a server from an error state. If the "
"server is in error status due to a failed move "
"operation then this is likely not the correct "
"approach to fix the problem. Do you wish to continue?"
)
):
return
compute_client.reset_server_state(server, state=parsed_args.state) compute_client.reset_server_state(server, state=parsed_args.state)
if parsed_args.root_password: if parsed_args.root_password:
@ -4650,7 +4736,7 @@ class ShelveServer(command.Command):
self.app.stdout.write(f'\rProgress: {progress}') self.app.stdout.write(f'\rProgress: {progress}')
self.app.stdout.flush() self.app.stdout.flush()
compute_client = self.app.client_manager.sdk_connection.compute compute_client = self.app.client_manager.compute
server_ids = [] server_ids = []
for server in parsed_args.servers: for server in parsed_args.servers:
@ -4745,7 +4831,7 @@ information for the server."""
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
compute_client = self.app.client_manager.sdk_connection.compute compute_client = self.app.client_manager.compute
image_client = self.app.client_manager.image image_client = self.app.client_manager.image
server = compute_client.find_server( server = compute_client.find_server(
@ -4875,7 +4961,7 @@ class SshServer(command.Command):
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
compute_client = self.app.client_manager.sdk_connection.compute compute_client = self.app.client_manager.compute
server = compute_client.find_server( server = compute_client.find_server(
parsed_args.server, ignore_missing=False parsed_args.server, ignore_missing=False
@ -4952,7 +5038,7 @@ class StartServer(command.Command):
parser.add_argument( parser.add_argument(
'--all-projects', '--all-projects',
action='store_true', action='store_true',
default=boolenv('ALL_PROJECTS'), default=envvars.boolenv('ALL_PROJECTS'),
help=_( help=_(
'Start server(s) in another project by name (admin only) ' 'Start server(s) in another project by name (admin only) '
'(can be specified using the ALL_PROJECTS envvar)' '(can be specified using the ALL_PROJECTS envvar)'
@ -4961,7 +5047,7 @@ class StartServer(command.Command):
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
compute_client = self.app.client_manager.sdk_connection.compute compute_client = self.app.client_manager.compute
for server in parsed_args.server: for server in parsed_args.server:
server_id = compute_client.find_server( server_id = compute_client.find_server(
server, server,
@ -4987,7 +5073,7 @@ class StopServer(command.Command):
parser.add_argument( parser.add_argument(
'--all-projects', '--all-projects',
action='store_true', action='store_true',
default=boolenv('ALL_PROJECTS'), default=envvars.boolenv('ALL_PROJECTS'),
help=_( help=_(
'Stop server(s) in another project by name (admin only) ' 'Stop server(s) in another project by name (admin only) '
'(can be specified using the ALL_PROJECTS envvar)' '(can be specified using the ALL_PROJECTS envvar)'
@ -4996,7 +5082,7 @@ class StopServer(command.Command):
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
compute_client = self.app.client_manager.sdk_connection.compute compute_client = self.app.client_manager.compute
for server in parsed_args.server: for server in parsed_args.server:
server_id = compute_client.find_server( server_id = compute_client.find_server(
server, server,
@ -5021,7 +5107,7 @@ class SuspendServer(command.Command):
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
compute_client = self.app.client_manager.sdk_connection.compute compute_client = self.app.client_manager.compute
for server in parsed_args.server: for server in parsed_args.server:
server_id = compute_client.find_server( server_id = compute_client.find_server(
server, server,
@ -5044,7 +5130,7 @@ class UnlockServer(command.Command):
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
compute_client = self.app.client_manager.sdk_connection.compute compute_client = self.app.client_manager.compute
for server in parsed_args.server: for server in parsed_args.server:
server_id = compute_client.find_server( server_id = compute_client.find_server(
server, server,
@ -5067,7 +5153,7 @@ class UnpauseServer(command.Command):
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
compute_client = self.app.client_manager.sdk_connection.compute compute_client = self.app.client_manager.compute
for server in parsed_args.server: for server in parsed_args.server:
server_id = compute_client.find_server( server_id = compute_client.find_server(
server, server,
@ -5089,7 +5175,7 @@ class UnrescueServer(command.Command):
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
compute_client = self.app.client_manager.sdk_connection.compute compute_client = self.app.client_manager.compute
server = compute_client.find_server( server = compute_client.find_server(
parsed_args.server, ignore_missing=False parsed_args.server, ignore_missing=False
) )
@ -5156,7 +5242,7 @@ class UnsetServer(command.Command):
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
compute_client = self.app.client_manager.sdk_connection.compute compute_client = self.app.client_manager.compute
server = compute_client.find_server( server = compute_client.find_server(
parsed_args.server, ignore_missing=False parsed_args.server, ignore_missing=False
@ -5247,7 +5333,7 @@ class UnshelveServer(command.Command):
self.app.stdout.write(f'\rProgress: {progress}') self.app.stdout.write(f'\rProgress: {progress}')
self.app.stdout.flush() self.app.stdout.flush()
compute_client = self.app.client_manager.sdk_connection.compute compute_client = self.app.client_manager.compute
kwargs = {} kwargs = {}
if parsed_args.availability_zone: if parsed_args.availability_zone:

View file

@ -17,10 +17,10 @@
import importlib import importlib
from osc_lib.command import command
from osc_lib import exceptions from osc_lib import exceptions
from osc_lib import utils from osc_lib import utils
from openstackclient import command
from openstackclient.i18n import _ from openstackclient.i18n import _
@ -71,7 +71,7 @@ class CreateServerBackup(command.ShowOne):
self.app.stderr.write(f'\rProgress: {progress}') self.app.stderr.write(f'\rProgress: {progress}')
self.app.stderr.flush() self.app.stderr.flush()
compute_client = self.app.client_manager.sdk_connection.compute compute_client = self.app.client_manager.compute
server = compute_client.find_server( server = compute_client.find_server(
parsed_args.server, ignore_missing=False parsed_args.server, ignore_missing=False

View file

@ -22,10 +22,10 @@ from cliff import columns
import iso8601 import iso8601
from openstack import exceptions as sdk_exceptions from openstack import exceptions as sdk_exceptions
from openstack import utils as sdk_utils from openstack import utils as sdk_utils
from osc_lib.command import command
from osc_lib import exceptions from osc_lib import exceptions
from osc_lib import utils from osc_lib import utils
from openstackclient import command
from openstackclient.common import pagination from openstackclient.common import pagination
from openstackclient.i18n import _ from openstackclient.i18n import _
@ -65,17 +65,11 @@ class ServerActionEventColumn(columns.FormattableColumn):
""" """
def _format_event(self, event): def _format_event(self, event):
column_map = {}
hidden_columns = ['id', 'name', 'location'] hidden_columns = ['id', 'name', 'location']
_, columns = utils.get_osc_show_columns_for_sdk_resource( _, columns = utils.get_osc_show_columns_for_sdk_resource(
event, event, {}, hidden_columns
column_map,
hidden_columns,
)
data = utils.get_item_properties(
event,
columns,
) )
data = utils.get_item_properties(event, columns)
return dict(zip(columns, data)) return dict(zip(columns, data))
def human_readable(self): def human_readable(self):
@ -88,17 +82,14 @@ class ServerActionEventColumn(columns.FormattableColumn):
def _get_server_event_columns(item, client): def _get_server_event_columns(item, client):
column_map = {} hidden_columns = ['name', 'server_id', 'links', 'location', 'finish_time']
hidden_columns = ['name', 'server_id', 'links', 'location']
if not sdk_utils.supports_microversion(client, '2.58'): if not sdk_utils.supports_microversion(client, '2.58'):
# updated_at was introduced in 2.58 # updated_at was introduced in 2.58
hidden_columns.append('updated_at') hidden_columns.append('updated_at')
return utils.get_osc_show_columns_for_sdk_resource( return utils.get_osc_show_columns_for_sdk_resource(
item, item, {}, hidden_columns
column_map,
hidden_columns,
) )
@ -148,7 +139,7 @@ class ListServerEvent(command.Lister):
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
compute_client = self.app.client_manager.sdk_connection.compute compute_client = self.app.client_manager.compute
kwargs = {} kwargs = {}
@ -220,13 +211,13 @@ class ListServerEvent(command.Lister):
data = compute_client.server_actions(server_id, **kwargs) data = compute_client.server_actions(server_id, **kwargs)
columns = ( columns: tuple[str, ...] = (
'request_id', 'request_id',
'server_id', 'server_id',
'action', 'action',
'start_time', 'start_time',
) )
column_headers = ( column_headers: tuple[str, ...] = (
'Request ID', 'Request ID',
'Server ID', 'Server ID',
'Action', 'Action',
@ -275,7 +266,7 @@ class ShowServerEvent(command.ShowOne):
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
compute_client = self.app.client_manager.sdk_connection.compute compute_client = self.app.client_manager.compute
try: try:
server_id = compute_client.find_server( server_id = compute_client.find_server(

View file

@ -16,21 +16,23 @@
"""Compute v2 Server Group action implementations""" """Compute v2 Server Group action implementations"""
import logging import logging
import typing as ty
from cliff import columns
from openstack import utils as sdk_utils from openstack import utils as sdk_utils
from osc_lib.cli import format_columns from osc_lib.cli import format_columns
from osc_lib.cli import parseractions from osc_lib.cli import parseractions
from osc_lib.command import command
from osc_lib import exceptions from osc_lib import exceptions
from osc_lib import utils from osc_lib import utils
from openstackclient import command
from openstackclient.common import pagination from openstackclient.common import pagination
from openstackclient.i18n import _ from openstackclient.i18n import _
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
_formatters = { _formatters: dict[str, type[columns.FormattableColumn[ty.Any]]] = {
'member_ids': format_columns.ListColumn, 'member_ids': format_columns.ListColumn,
'policies': format_columns.ListColumn, 'policies': format_columns.ListColumn,
'rules': format_columns.DictColumn, 'rules': format_columns.DictColumn,
@ -93,7 +95,7 @@ class CreateServerGroup(command.ShowOne):
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
compute_client = self.app.client_manager.sdk_connection.compute compute_client = self.app.client_manager.compute
if parsed_args.policy in ('soft-affinity', 'soft-anti-affinity'): if parsed_args.policy in ('soft-affinity', 'soft-anti-affinity'):
if not sdk_utils.supports_microversion(compute_client, '2.15'): if not sdk_utils.supports_microversion(compute_client, '2.15'):
@ -153,7 +155,7 @@ class DeleteServerGroup(command.Command):
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
compute_client = self.app.client_manager.sdk_connection.compute compute_client = self.app.client_manager.compute
result = 0 result = 0
for group in parsed_args.server_group: for group in parsed_args.server_group:
try: try:
@ -197,7 +199,7 @@ class ListServerGroup(command.Lister):
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
compute_client = self.app.client_manager.sdk_connection.compute compute_client = self.app.client_manager.compute
kwargs = {} kwargs = {}
@ -216,12 +218,12 @@ class ListServerGroup(command.Lister):
if sdk_utils.supports_microversion(compute_client, '2.64'): if sdk_utils.supports_microversion(compute_client, '2.64'):
policy_key = 'Policy' policy_key = 'Policy'
columns = ( columns: tuple[str, ...] = (
'id', 'id',
'name', 'name',
policy_key.lower(), policy_key.lower(),
) )
column_headers = ( column_headers: tuple[str, ...] = (
'ID', 'ID',
'Name', 'Name',
policy_key, policy_key,
@ -264,7 +266,7 @@ class ShowServerGroup(command.ShowOne):
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
compute_client = self.app.client_manager.sdk_connection.compute compute_client = self.app.client_manager.compute
group = compute_client.find_server_group( group = compute_client.find_server_group(
parsed_args.server_group, ignore_missing=False parsed_args.server_group, ignore_missing=False
) )

View file

@ -19,10 +19,10 @@ import importlib
import logging import logging
from osc_lib.cli import parseractions from osc_lib.cli import parseractions
from osc_lib.command import command
from osc_lib import exceptions from osc_lib import exceptions
from osc_lib import utils from osc_lib import utils
from openstackclient import command
from openstackclient.i18n import _ from openstackclient.i18n import _
@ -72,7 +72,7 @@ class CreateServerImage(command.ShowOne):
self.app.stdout.write(f'\rProgress: {progress}') self.app.stdout.write(f'\rProgress: {progress}')
self.app.stdout.flush() self.app.stdout.flush()
compute_client = self.app.client_manager.sdk_connection.compute compute_client = self.app.client_manager.compute
image_client = self.app.client_manager.image image_client = self.app.client_manager.image
server = compute_client.find_server( server = compute_client.find_server(

View file

@ -15,10 +15,10 @@
import uuid import uuid
from openstack import utils as sdk_utils from openstack import utils as sdk_utils
from osc_lib.command import command
from osc_lib import exceptions from osc_lib import exceptions
from osc_lib import utils from osc_lib import utils
from openstackclient import command
from openstackclient.common import pagination from openstackclient.common import pagination
from openstackclient.i18n import _ from openstackclient.i18n import _
from openstackclient.identity import common as identity_common from openstackclient.identity import common as identity_common
@ -154,7 +154,7 @@ class ListMigration(command.Lister):
) )
def take_action(self, parsed_args): def take_action(self, parsed_args):
compute_client = self.app.client_manager.sdk_connection.compute compute_client = self.app.client_manager.compute
identity_client = self.app.client_manager.identity identity_client = self.app.client_manager.identity
search_opts = {} search_opts = {}
@ -289,7 +289,7 @@ class ShowMigration(command.ShowOne):
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
compute_client = self.app.client_manager.sdk_connection.compute compute_client = self.app.client_manager.compute
if not sdk_utils.supports_microversion(compute_client, '2.24'): if not sdk_utils.supports_microversion(compute_client, '2.24'):
msg = _( msg = _(
@ -333,7 +333,7 @@ class ShowMigration(command.ShowOne):
ignore_missing=False, ignore_missing=False,
) )
column_headers = ( column_headers: tuple[str, ...] = (
'ID', 'ID',
'Server UUID', 'Server UUID',
'Status', 'Status',
@ -352,7 +352,7 @@ class ShowMigration(command.ShowOne):
'Updated At', 'Updated At',
) )
columns = ( columns: tuple[str, ...] = (
'id', 'id',
'server_id', 'server_id',
'status', 'status',
@ -404,7 +404,7 @@ class AbortMigration(command.Command):
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
compute_client = self.app.client_manager.sdk_connection.compute compute_client = self.app.client_manager.compute
if not sdk_utils.supports_microversion(compute_client, '2.24'): if not sdk_utils.supports_microversion(compute_client, '2.24'):
msg = _( msg = _(
@ -469,7 +469,7 @@ class ForceCompleteMigration(command.Command):
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
compute_client = self.app.client_manager.sdk_connection.compute compute_client = self.app.client_manager.compute
if not sdk_utils.supports_microversion(compute_client, '2.22'): if not sdk_utils.supports_microversion(compute_client, '2.22'):
msg = _( msg = _(

View file

@ -15,10 +15,10 @@
"""Compute v2 Server action implementations""" """Compute v2 Server action implementations"""
from openstack import utils as sdk_utils from openstack import utils as sdk_utils
from osc_lib.command import command
from osc_lib import exceptions from osc_lib import exceptions
from osc_lib import utils from osc_lib import utils
from openstackclient import command
from openstackclient.i18n import _ from openstackclient.i18n import _
@ -34,7 +34,7 @@ class ListServerVolume(command.Lister):
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
compute_client = self.app.client_manager.sdk_connection.compute compute_client = self.app.client_manager.compute
server = compute_client.find_server( server = compute_client.find_server(
parsed_args.server, parsed_args.server,
@ -42,8 +42,8 @@ class ListServerVolume(command.Lister):
) )
volumes = compute_client.volume_attachments(server) volumes = compute_client.volume_attachments(server)
columns = () columns: tuple[str, ...] = ()
column_headers = () column_headers: tuple[str, ...] = ()
if not sdk_utils.supports_microversion(compute_client, '2.89'): if not sdk_utils.supports_microversion(compute_client, '2.89'):
columns += ('id',) columns += ('id',)
@ -114,7 +114,7 @@ class SetServerVolume(command.Command):
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
compute_client = self.app.client_manager.sdk_connection.compute compute_client = self.app.client_manager.compute
volume_client = self.app.client_manager.sdk_connection.volume volume_client = self.app.client_manager.sdk_connection.volume
if parsed_args.delete_on_termination is not None: if parsed_args.delete_on_termination is not None:

View file

@ -18,10 +18,10 @@
import logging import logging
from openstack import utils as sdk_utils from openstack import utils as sdk_utils
from osc_lib.command import command
from osc_lib import exceptions from osc_lib import exceptions
from osc_lib import utils from osc_lib import utils
from openstackclient import command
from openstackclient.i18n import _ from openstackclient.i18n import _
@ -52,7 +52,7 @@ class DeleteService(command.Command):
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
compute_client = self.app.client_manager.sdk_connection.compute compute_client = self.app.client_manager.compute
result = 0 result = 0
for s in parsed_args.service: for s in parsed_args.service:
try: try:
@ -70,7 +70,7 @@ class DeleteService(command.Command):
if result > 0: if result > 0:
total = len(parsed_args.service) total = len(parsed_args.service)
msg = _( msg = _(
"%(result)s of %(total)s compute services failed " "to delete." "%(result)s of %(total)s compute services failed to delete."
) % {'result': result, 'total': total} ) % {'result': result, 'total': total}
raise exceptions.CommandError(msg) raise exceptions.CommandError(msg)
@ -108,8 +108,8 @@ deployment."""
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
compute_client = self.app.client_manager.sdk_connection.compute compute_client = self.app.client_manager.compute
columns = ( columns: tuple[str, ...] = (
"id", "id",
"binary", "binary",
"host", "host",
@ -118,7 +118,7 @@ deployment."""
"state", "state",
"updated_at", "updated_at",
) )
column_headers = ( column_headers: tuple[str, ...] = (
"ID", "ID",
"Binary", "Binary",
"Host", "Host",
@ -153,8 +153,7 @@ class SetService(command.Command):
"service", "service",
metavar="<service>", metavar="<service>",
help=_( help=_(
"Name of service (Binary name), for example " "Name of service (Binary name), for example ``nova-compute``"
"``nova-compute``"
), ),
) )
enabled_group = parser.add_mutually_exclusive_group() enabled_group = parser.add_mutually_exclusive_group()
@ -222,7 +221,7 @@ class SetService(command.Command):
return services[0] return services[0]
def take_action(self, parsed_args): def take_action(self, parsed_args):
compute_client = self.app.client_manager.sdk_connection.compute compute_client = self.app.client_manager.compute
if ( if (
parsed_args.enable or not parsed_args.disable parsed_args.enable or not parsed_args.disable
@ -281,9 +280,7 @@ class SetService(command.Command):
force_down = False force_down = False
if force_down is not None: if force_down is not None:
if not sdk_utils.supports_microversion(compute_client, '2.11'): if not sdk_utils.supports_microversion(compute_client, '2.11'):
msg = _( msg = _('--os-compute-api-version 2.11 or later is required')
'--os-compute-api-version 2.11 or later is ' 'required'
)
raise exceptions.CommandError(msg) raise exceptions.CommandError(msg)
try: try:
compute_client.update_service_forced_down( compute_client.update_service_forced_down(
@ -299,7 +296,6 @@ class SetService(command.Command):
if result > 0: if result > 0:
msg = _( msg = _(
"Compute service %(service)s of host %(host)s failed to " "Compute service %(service)s of host %(host)s failed to set."
"set."
) % {"service": parsed_args.service, "host": parsed_args.host} ) % {"service": parsed_args.service, "host": parsed_args.host}
raise exceptions.CommandError(msg) raise exceptions.CommandError(msg)

View file

@ -15,19 +15,21 @@
"""Usage action implementations""" """Usage action implementations"""
from collections.abc import Collection
import datetime import datetime
import functools import functools
import typing as ty
from cliff import columns as cliff_columns from cliff import columns as cliff_columns
from osc_lib.command import command
from osc_lib import utils from osc_lib import utils
from openstackclient import command
from openstackclient.i18n import _ from openstackclient.i18n import _
# TODO(stephenfin): This exists in a couple of places and should be moved to a # TODO(stephenfin): This exists in a couple of places and should be moved to a
# common module # common module
class ProjectColumn(cliff_columns.FormattableColumn): class ProjectColumn(cliff_columns.FormattableColumn[str]):
"""Formattable column for project column. """Formattable column for project column.
Unlike the parent FormattableColumn class, the initializer of the class Unlike the parent FormattableColumn class, the initializer of the class
@ -53,12 +55,12 @@ class ProjectColumn(cliff_columns.FormattableColumn):
return project return project
class CountColumn(cliff_columns.FormattableColumn): class CountColumn(cliff_columns.FormattableColumn[Collection[ty.Any]]):
def human_readable(self): def human_readable(self):
return len(self._value) if self._value is not None else None return len(self._value) if self._value is not None else None
class FloatColumn(cliff_columns.FormattableColumn): class FloatColumn(cliff_columns.FormattableColumn[float]):
def human_readable(self): def human_readable(self):
return float(f"{self._value:.2f}") return float(f"{self._value:.2f}")
@ -115,8 +117,7 @@ class ListUsage(command.Lister):
metavar="<start>", metavar="<start>",
default=None, default=None,
help=_( help=_(
"Usage range start date, ex 2012-01-20" "Usage range start date, ex 2012-01-20 (default: 4 weeks ago)"
" (default: 4 weeks ago)"
), ),
) )
parser.add_argument( parser.add_argument(
@ -136,7 +137,7 @@ class ListUsage(command.Lister):
else: else:
return project return project
compute_client = self.app.client_manager.sdk_connection.compute compute_client = self.app.client_manager.compute
columns = ( columns = (
"project_id", "project_id",
"server_usages", "server_usages",
@ -153,7 +154,7 @@ class ListUsage(command.Lister):
) )
date_cli_format = "%Y-%m-%d" date_cli_format = "%Y-%m-%d"
now = datetime.datetime.utcnow() now = datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None)
if parsed_args.start: if parsed_args.start:
start = datetime.datetime.strptime( start = datetime.datetime.strptime(
@ -222,8 +223,7 @@ class ShowUsage(command.ShowOne):
metavar="<start>", metavar="<start>",
default=None, default=None,
help=_( help=_(
"Usage range start date, ex 2012-01-20" "Usage range start date, ex 2012-01-20 (default: 4 weeks ago)"
" (default: 4 weeks ago)"
), ),
) )
parser.add_argument( parser.add_argument(
@ -236,9 +236,9 @@ class ShowUsage(command.ShowOne):
def take_action(self, parsed_args): def take_action(self, parsed_args):
identity_client = self.app.client_manager.identity identity_client = self.app.client_manager.identity
compute_client = self.app.client_manager.sdk_connection.compute compute_client = self.app.client_manager.compute
date_cli_format = "%Y-%m-%d" date_cli_format = "%Y-%m-%d"
now = datetime.datetime.utcnow() now = datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None)
if parsed_args.start: if parsed_args.start:
start = datetime.datetime.strptime( start = datetime.datetime.strptime(

View file

@ -11,7 +11,6 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
#
import logging import logging
@ -20,9 +19,9 @@ from osc_lib import utils
from openstackclient.i18n import _ from openstackclient.i18n import _
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
# global variables used when building the shell
DEFAULT_API_VERSION = '3' DEFAULT_API_VERSION = '3'
API_VERSION_OPTION = 'os_identity_api_version' API_VERSION_OPTION = 'os_identity_api_version'
API_NAME = 'identity' API_NAME = 'identity'
@ -64,8 +63,7 @@ def build_option_parser(parser):
metavar='<identity-api-version>', metavar='<identity-api-version>',
default=utils.env('OS_IDENTITY_API_VERSION'), default=utils.env('OS_IDENTITY_API_VERSION'),
help=_( help=_(
'Identity API version, default=%s ' 'Identity API version, default=%s (Env: OS_IDENTITY_API_VERSION)'
'(Env: OS_IDENTITY_API_VERSION)'
) )
% DEFAULT_API_VERSION, % DEFAULT_API_VERSION,
) )

View file

@ -88,23 +88,23 @@ def find_service_sdk(identity_client, name_type_or_id):
raise exceptions.CommandError(e.message) raise exceptions.CommandError(e.message)
# search for service type # search for service type
services = identity_client.services() services = identity_client.services(type=name_type_or_id)
result = None try:
for service in services: service = next(services)
if name_type_or_id == service.type: except StopIteration:
if result:
msg = _( msg = _(
"Multiple service matches found for '%s', " "No service with a type, name or ID of '%(query)s' exists."
"use an ID or name to be more specific." ) % {"query": name_type_or_id}
) raise exceptions.CommandError(msg)
raise exceptions.CommandError(msg % name_type_or_id)
result = service
if result is None: if next(services, None):
msg = _("No service with a type, name or ID of '%s' exists.") msg = _(
raise exceptions.CommandError(msg % name_type_or_id) "Multiple service matches found for '%(query)s', "
"use an ID to be more specific."
) % {"query": name_type_or_id}
raise exceptions.CommandError(msg)
return result return service
def get_resource(manager, name_type_or_id): def get_resource(manager, name_type_or_id):
@ -184,27 +184,29 @@ def _get_token_resource(client, resource, parsed_name, parsed_domain=None):
return parsed_name return parsed_name
def _get_domain_id_if_requested(identity_client, domain_name_or_id):
if not domain_name_or_id:
return None
domain = find_domain(identity_client, domain_name_or_id)
return domain.id
def find_domain(identity_client, name_or_id): def find_domain(identity_client, name_or_id):
return _find_identity_resource( return _find_identity_resource(
identity_client.domains, name_or_id, domains.Domain identity_client.domains, name_or_id, domains.Domain
) )
def find_domain_id_sdk(
identity_client, name_or_id, *, validate_actor_existence=True
):
return _find_sdk_id(
identity_client.find_domain,
name_or_id=name_or_id,
validate_actor_existence=validate_actor_existence,
)
def find_group(identity_client, name_or_id, domain_name_or_id=None): def find_group(identity_client, name_or_id, domain_name_or_id=None):
domain_id = _get_domain_id_if_requested(identity_client, domain_name_or_id) if domain_name_or_id is None:
if not domain_id:
return _find_identity_resource( return _find_identity_resource(
identity_client.groups, name_or_id, groups.Group identity_client.groups, name_or_id, groups.Group
) )
else:
domain_id = find_domain(identity_client, domain_id).id domain_id = find_domain(identity_client, domain_name_or_id).id
return _find_identity_resource( return _find_identity_resource(
identity_client.groups, identity_client.groups,
name_or_id, name_or_id,
@ -213,14 +215,39 @@ def find_group(identity_client, name_or_id, domain_name_or_id=None):
) )
def find_group_id_sdk(
identity_client,
name_or_id,
domain_name_or_id=None,
*,
validate_actor_existence=True,
):
if domain_name_or_id is None:
return _find_sdk_id(
identity_client.find_group,
name_or_id=name_or_id,
validate_actor_existence=validate_actor_existence,
)
domain_id = find_domain_id_sdk(
identity_client,
name_or_id=domain_name_or_id,
validate_actor_existence=validate_actor_existence,
)
return _find_sdk_id(
identity_client.find_group,
name_or_id=name_or_id,
validate_actor_existence=validate_actor_existence,
domain_id=domain_id,
)
def find_project(identity_client, name_or_id, domain_name_or_id=None): def find_project(identity_client, name_or_id, domain_name_or_id=None):
domain_id = _get_domain_id_if_requested(identity_client, domain_name_or_id) if domain_name_or_id is None:
if not domain_id:
return _find_identity_resource( return _find_identity_resource(
identity_client.projects, name_or_id, projects.Project identity_client.projects, name_or_id, projects.Project
) )
else: domain_id = find_domain(identity_client, domain_name_or_id).id
domain_id = find_domain(identity_client, domain_id).id
return _find_identity_resource( return _find_identity_resource(
identity_client.projects, identity_client.projects,
name_or_id, name_or_id,
@ -230,18 +257,42 @@ def find_project(identity_client, name_or_id, domain_name_or_id=None):
def find_user(identity_client, name_or_id, domain_name_or_id=None): def find_user(identity_client, name_or_id, domain_name_or_id=None):
domain_id = _get_domain_id_if_requested(identity_client, domain_name_or_id) if domain_name_or_id is None:
if not domain_id:
return _find_identity_resource( return _find_identity_resource(
identity_client.users, name_or_id, users.User identity_client.users, name_or_id, users.User
) )
else: domain_id = find_domain(identity_client, domain_name_or_id).id
domain_id = find_domain(identity_client, domain_id).id
return _find_identity_resource( return _find_identity_resource(
identity_client.users, name_or_id, users.User, domain_id=domain_id identity_client.users, name_or_id, users.User, domain_id=domain_id
) )
def find_user_id_sdk(
identity_client,
name_or_id,
domain_name_or_id=None,
*,
validate_actor_existence=True,
):
if domain_name_or_id is None:
return _find_sdk_id(
identity_client.find_user,
name_or_id=name_or_id,
validate_actor_existence=validate_actor_existence,
)
domain_id = find_domain_id_sdk(
identity_client,
name_or_id=domain_name_or_id,
validate_actor_existence=validate_actor_existence,
)
return _find_sdk_id(
identity_client.find_user,
name_or_id=name_or_id,
validate_actor_existence=validate_actor_existence,
domain_id=domain_id,
)
def _find_identity_resource( def _find_identity_resource(
identity_client_manager, name_or_id, resource_type, **kwargs identity_client_manager, name_or_id, resource_type, **kwargs
): ):
@ -282,13 +333,20 @@ def _find_identity_resource(
return resource_type(None, {'id': name_or_id, 'name': name_or_id}) return resource_type(None, {'id': name_or_id, 'name': name_or_id})
def get_immutable_options(parsed_args): def _find_sdk_id(
options = {} find_command, name_or_id, *, validate_actor_existence=True, **kwargs
if parsed_args.immutable: ):
options['immutable'] = True try:
if parsed_args.no_immutable: resource = find_command(
options['immutable'] = False name_or_id=name_or_id, ignore_missing=False, **kwargs
return options )
except sdk_exceptions.ForbiddenException:
return name_or_id
except sdk_exceptions.ResourceNotFound as exc:
if not validate_actor_existence:
return name_or_id
raise exceptions.CommandError from exc
return resource.id
def add_user_domain_option_to_parser(parser): def add_user_domain_option_to_parser(parser):
@ -347,23 +405,27 @@ def add_inherited_option_to_parser(parser):
action='store_true', action='store_true',
default=False, default=False,
help=_( help=_(
'Specifies if the role grant is inheritable to the sub ' 'projects' 'Specifies if the role grant is inheritable to the sub projects'
), ),
) )
def add_resource_option_to_parser(parser): def add_resource_option_to_parser(parser):
enable_group = parser.add_mutually_exclusive_group() immutable_group = parser.add_mutually_exclusive_group()
enable_group.add_argument( immutable_group.add_argument(
'--immutable', '--immutable',
action='store_true', action='store_true',
dest='immutable',
default=None,
help=_( help=_(
'Make resource immutable. An immutable project may not ' 'Make resource immutable. An immutable project may not '
'be deleted or modified except to remove the immutable flag' 'be deleted or modified except to remove the immutable flag'
), ),
) )
enable_group.add_argument( immutable_group.add_argument(
'--no-immutable', '--no-immutable',
action='store_true', action='store_false',
dest='immutable',
default=None,
help=_('Make resource mutable (default)'), help=_('Make resource mutable (default)'),
) )

View file

@ -14,19 +14,20 @@
"""Identity v2 Service Catalog action implementations""" """Identity v2 Service Catalog action implementations"""
import logging import logging
import typing as ty
from cliff import columns as cliff_columns from cliff import columns as cliff_columns
from osc_lib.command import command
from osc_lib import exceptions from osc_lib import exceptions
from osc_lib import utils from osc_lib import utils
from openstackclient import command
from openstackclient.i18n import _ from openstackclient.i18n import _
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
class EndpointsColumn(cliff_columns.FormattableColumn): class EndpointsColumn(cliff_columns.FormattableColumn[ty.Any]):
def human_readable(self): def human_readable(self):
if not self._value: if not self._value:
return "" return ""

View file

@ -18,10 +18,10 @@
import logging import logging
from osc_lib.command import command
from osc_lib import exceptions from osc_lib import exceptions
from osc_lib import utils from osc_lib import utils
from openstackclient import command
from openstackclient.i18n import _ from openstackclient.i18n import _
@ -128,9 +128,10 @@ class DeleteEC2Creds(command.Command):
if result > 0: if result > 0:
total = len(parsed_args.access_keys) total = len(parsed_args.access_keys)
msg = _( msg = _("%(result)s of %(total)s EC2 keys failed to delete.") % {
"%(result)s of %(total)s EC2 keys failed " "to delete." 'result': result,
) % {'result': result, 'total': total} 'total': total,
}
raise exceptions.CommandError(msg) raise exceptions.CommandError(msg)

View file

@ -17,10 +17,10 @@
import logging import logging
from osc_lib.command import command
from osc_lib import exceptions from osc_lib import exceptions
from osc_lib import utils from osc_lib import utils
from openstackclient import command
from openstackclient.i18n import _ from openstackclient.i18n import _
from openstackclient.identity import common from openstackclient.identity import common
@ -111,9 +111,10 @@ class DeleteEndpoint(command.Command):
if result > 0: if result > 0:
total = len(parsed_args.endpoints) total = len(parsed_args.endpoints)
msg = _( msg = _("%(result)s of %(total)s endpoints failed to delete.") % {
"%(result)s of %(total)s endpoints failed " "to delete." 'result': result,
) % {'result': result, 'total': total} 'total': total,
}
raise exceptions.CommandError(msg) raise exceptions.CommandError(msg)
@ -132,18 +133,19 @@ class ListEndpoint(command.Lister):
def take_action(self, parsed_args): def take_action(self, parsed_args):
identity_client = self.app.client_manager.identity identity_client = self.app.client_manager.identity
if parsed_args.long:
columns = ( columns: tuple[str, ...] = (
'ID', 'ID',
'Region', 'Region',
'Service Name', 'Service Name',
'Service Type', 'Service Type',
)
if parsed_args.long:
columns += (
'PublicURL', 'PublicURL',
'AdminURL', 'AdminURL',
'InternalURL', 'InternalURL',
) )
else:
columns = ('ID', 'Region', 'Service Name', 'Service Type')
data = identity_client.endpoints.list() data = identity_client.endpoints.list()
for ep in data: for ep in data:

View file

@ -20,10 +20,10 @@ import logging
from keystoneauth1 import exceptions as ks_exc from keystoneauth1 import exceptions as ks_exc
from osc_lib.cli import format_columns from osc_lib.cli import format_columns
from osc_lib.cli import parseractions from osc_lib.cli import parseractions
from osc_lib.command import command
from osc_lib import exceptions from osc_lib import exceptions
from osc_lib import utils from osc_lib import utils
from openstackclient import command
from openstackclient.i18n import _ from openstackclient.i18n import _
@ -59,6 +59,7 @@ class CreateProject(command.ShowOne):
parser.add_argument( parser.add_argument(
'--property', '--property',
metavar='<key=value>', metavar='<key=value>',
dest='properties',
action=parseractions.KeyValueAction, action=parseractions.KeyValueAction,
help=_( help=_(
'Add a property to <name> ' 'Add a property to <name> '
@ -79,8 +80,8 @@ class CreateProject(command.ShowOne):
if parsed_args.disable: if parsed_args.disable:
enabled = False enabled = False
kwargs = {} kwargs = {}
if parsed_args.property: if parsed_args.properties:
kwargs = parsed_args.property.copy() kwargs.update(parsed_args.properties)
try: try:
project = identity_client.tenants.create( project = identity_client.tenants.create(
@ -105,7 +106,14 @@ class CreateProject(command.ShowOne):
class DeleteProject(command.Command): class DeleteProject(command.Command):
_description = _("Delete project(s)") _description = _(
"Delete project(s). This command will remove specified "
"existing project(s) if an active user is authorized to do "
"this. If there are resources managed by other services "
"(for example, Nova, Neutron, Cinder) associated with "
"specified project(s), delete operation will proceed "
"regardless."
)
def get_parser(self, prog_name): def get_parser(self, prog_name):
parser = super().get_parser(prog_name) parser = super().get_parser(prog_name)
@ -140,9 +148,10 @@ class DeleteProject(command.Command):
if errors > 0: if errors > 0:
total = len(parsed_args.projects) total = len(parsed_args.projects)
msg = _( msg = _("%(errors)s of %(total)s projects failed to delete.") % {
"%(errors)s of %(total)s projects failed " "to delete." 'errors': errors,
) % {'errors': errors, 'total': total} 'total': total,
}
raise exceptions.CommandError(msg) raise exceptions.CommandError(msg)
@ -169,10 +178,9 @@ class ListProject(command.Lister):
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
columns: tuple[str, ...] = ('ID', 'Name')
if parsed_args.long: if parsed_args.long:
columns = ('ID', 'Name', 'Description', 'Enabled') columns += ('Description', 'Enabled')
else:
columns = ('ID', 'Name')
data = self.app.client_manager.identity.tenants.list() data = self.app.client_manager.identity.tenants.list()
if parsed_args.sort: if parsed_args.sort:
data = utils.sort_items(data, parsed_args.sort) data = utils.sort_items(data, parsed_args.sort)
@ -223,6 +231,7 @@ class SetProject(command.Command):
parser.add_argument( parser.add_argument(
'--property', '--property',
metavar='<key=value>', metavar='<key=value>',
dest='properties',
action=parseractions.KeyValueAction, action=parseractions.KeyValueAction,
help=_( help=_(
'Set a project property ' 'Set a project property '
@ -248,8 +257,8 @@ class SetProject(command.Command):
kwargs['enabled'] = True kwargs['enabled'] = True
if parsed_args.disable: if parsed_args.disable:
kwargs['enabled'] = False kwargs['enabled'] = False
if parsed_args.property: if parsed_args.properties:
kwargs.update(parsed_args.property) kwargs.update(parsed_args.properties)
if 'id' in kwargs: if 'id' in kwargs:
del kwargs['id'] del kwargs['id']
if 'name' in kwargs: if 'name' in kwargs:
@ -331,6 +340,7 @@ class UnsetProject(command.Command):
parser.add_argument( parser.add_argument(
'--property', '--property',
metavar='<key>', metavar='<key>',
dest='properties',
action='append', action='append',
default=[], default=[],
help=_( help=_(
@ -347,7 +357,7 @@ class UnsetProject(command.Command):
parsed_args.project, parsed_args.project,
) )
kwargs = project._info kwargs = project._info
for key in parsed_args.property: for key in parsed_args.properties:
if key in kwargs: if key in kwargs:
kwargs[key] = None kwargs[key] = None
identity_client.tenants.update(project.id, **kwargs) identity_client.tenants.update(project.id, **kwargs)

View file

@ -18,10 +18,10 @@
import logging import logging
from keystoneauth1 import exceptions as ks_exc from keystoneauth1 import exceptions as ks_exc
from osc_lib.command import command
from osc_lib import exceptions from osc_lib import exceptions
from osc_lib import utils from osc_lib import utils
from openstackclient import command
from openstackclient.i18n import _ from openstackclient.i18n import _
@ -143,7 +143,7 @@ class DeleteRole(command.Command):
if errors > 0: if errors > 0:
total = len(parsed_args.roles) total = len(parsed_args.roles)
msg = _("%(errors)s of %(total)s roles failed " "to delete.") % { msg = _("%(errors)s of %(total)s roles failed to delete.") % {
'errors': errors, 'errors': errors,
'total': total, 'total': total,
} }

View file

@ -13,10 +13,10 @@
"""Identity v2 Assignment action implementations""" """Identity v2 Assignment action implementations"""
from osc_lib.command import command
from osc_lib import exceptions from osc_lib import exceptions
from osc_lib import utils from osc_lib import utils
from openstackclient import command
from openstackclient.i18n import _ # noqa from openstackclient.i18n import _ # noqa
@ -68,7 +68,7 @@ class ListRoleAssignment(command.Lister):
parsed_args.user, parsed_args.user,
) )
elif parsed_args.authuser: elif parsed_args.authuser:
if auth_ref: if auth_ref and auth_ref.user_id:
user = utils.find_resource( user = utils.find_resource(
identity_client.users, auth_ref.user_id identity_client.users, auth_ref.user_id
) )
@ -80,7 +80,7 @@ class ListRoleAssignment(command.Lister):
parsed_args.project, parsed_args.project,
) )
elif parsed_args.authproject: elif parsed_args.authproject:
if auth_ref: if auth_ref and auth_ref.project_id:
project = utils.find_resource( project = utils.find_resource(
identity_client.projects, auth_ref.project_id identity_client.projects, auth_ref.project_id
) )

View file

@ -17,10 +17,10 @@
import logging import logging
from osc_lib.command import command
from osc_lib import exceptions from osc_lib import exceptions
from osc_lib import utils from osc_lib import utils
from openstackclient import command
from openstackclient.i18n import _ from openstackclient.i18n import _
from openstackclient.identity import common from openstackclient.identity import common
@ -100,9 +100,10 @@ class DeleteService(command.Command):
if result > 0: if result > 0:
total = len(parsed_args.services) total = len(parsed_args.services)
msg = _( msg = _("%(result)s of %(total)s services failed to delete.") % {
"%(result)s of %(total)s services failed " "to delete." 'result': result,
) % {'result': result, 'total': total} 'total': total,
}
raise exceptions.CommandError(msg) raise exceptions.CommandError(msg)
@ -120,10 +121,9 @@ class ListService(command.Lister):
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
columns: tuple[str, ...] = ('ID', 'Name', 'Type')
if parsed_args.long: if parsed_args.long:
columns = ('ID', 'Name', 'Type', 'Description') columns += ('Description',)
else:
columns = ('ID', 'Name', 'Type')
data = self.app.client_manager.identity.services.list() data = self.app.client_manager.identity.services.list()
return ( return (
columns, columns,
@ -164,7 +164,7 @@ class ShowService(command.ShowOne):
return zip(*sorted(info.items())) return zip(*sorted(info.items()))
msg = _( msg = _(
"No service catalog with a type, name or ID of '%s' " "exists." "No service catalog with a type, name or ID of '%s' exists."
) % (parsed_args.service) ) % (parsed_args.service)
raise exceptions.CommandError(msg) raise exceptions.CommandError(msg)
else: else:

View file

@ -15,9 +15,9 @@
"""Identity v2 Token action implementations""" """Identity v2 Token action implementations"""
from osc_lib.command import command
from osc_lib import exceptions from osc_lib import exceptions
from openstackclient import command
from openstackclient.i18n import _ from openstackclient.i18n import _

View file

@ -20,17 +20,17 @@ import logging
from cliff import columns as cliff_columns from cliff import columns as cliff_columns
from keystoneauth1 import exceptions as ks_exc from keystoneauth1 import exceptions as ks_exc
from osc_lib.command import command
from osc_lib import exceptions from osc_lib import exceptions
from osc_lib import utils from osc_lib import utils
from openstackclient import command
from openstackclient.i18n import _ from openstackclient.i18n import _
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
class ProjectColumn(cliff_columns.FormattableColumn): class ProjectColumn(cliff_columns.FormattableColumn[str]):
"""Formattable column for project column. """Formattable column for project column.
Unlike the parent FormattableColumn class, the initializer of the Unlike the parent FormattableColumn class, the initializer of the
@ -194,7 +194,7 @@ class DeleteUser(command.Command):
if errors > 0: if errors > 0:
total = len(parsed_args.users) total = len(parsed_args.users)
msg = _("%(errors)s of %(total)s users failed " "to delete.") % { msg = _("%(errors)s of %(total)s users failed to delete.") % {
'errors': errors, 'errors': errors,
'total': total, 'total': total,
} }
@ -230,21 +230,11 @@ class ListUser(command.Lister):
) )
project = project.id project = project.id
columns: tuple[str, ...] = ('id', 'name')
column_headers: tuple[str, ...] = ('ID', 'Name')
if parsed_args.long: if parsed_args.long:
columns = ( columns += ('tenantId', 'email', 'enabled')
'ID', column_headers += ('Project', 'Email', 'Enabled')
'Name',
'tenantId',
'Email',
'Enabled',
)
column_headers = (
'ID',
'Name',
'Project',
'Email',
'Enabled',
)
# Cache the project list # Cache the project list
project_cache = {} project_cache = {}
try: try:
@ -256,15 +246,10 @@ class ListUser(command.Lister):
formatters['tenantId'] = functools.partial( formatters['tenantId'] = functools.partial(
ProjectColumn, project_cache=project_cache ProjectColumn, project_cache=project_cache
) )
else:
columns = column_headers = ('ID', 'Name')
data = identity_client.users.list(tenant_id=project) data = identity_client.users.list(tenant_id=project)
if parsed_args.project: if parsed_args.project:
d = {} data = {s.id: s for s in data}.values()
for s in data:
d[s.id] = s
data = d.values()
if parsed_args.long: if parsed_args.long:
# FIXME(dtroyer): Sometimes user objects have 'tenant_id' instead # FIXME(dtroyer): Sometimes user objects have 'tenant_id' instead

View file

@ -17,10 +17,10 @@
import logging import logging
from osc_lib.command import command
from osc_lib import exceptions from osc_lib import exceptions
from osc_lib import utils from osc_lib import utils
from openstackclient import command
from openstackclient.i18n import _ from openstackclient.i18n import _
from openstackclient.identity import common from openstackclient.identity import common
@ -44,7 +44,11 @@ class DeleteAccessRule(command.Command):
def take_action(self, parsed_args): def take_action(self, parsed_args):
identity_client = self.app.client_manager.sdk_connection.identity identity_client = self.app.client_manager.sdk_connection.identity
conn = self.app.client_manager.sdk_connection conn = self.app.client_manager.sdk_connection
user_id = conn.config.get_auth().get_user_id(conn.identity) auth = conn.config.get_auth()
if auth is None:
# this will never happen
raise exceptions.CommandError('invalid authentication info')
user_id = auth.get_user_id(conn.identity)
errors = 0 errors = 0
for ac in parsed_args.access_rule: for ac in parsed_args.access_rule:
@ -54,17 +58,14 @@ class DeleteAccessRule(command.Command):
except Exception as e: except Exception as e:
errors += 1 errors += 1
LOG.error( LOG.error(
_( _("Failed to delete access rule with ID '%(ac)s': %(e)s"),
"Failed to delete access rule with "
"ID '%(ac)s': %(e)s"
),
{'ac': ac, 'e': e}, {'ac': ac, 'e': e},
) )
if errors > 0: if errors > 0:
total = len(parsed_args.access_rule) total = len(parsed_args.access_rule)
msg = _( msg = _(
"%(errors)s of %(total)s access rules failed " "to delete." "%(errors)s of %(total)s access rules failed to delete."
) % {'errors': errors, 'total': total} ) % {'errors': errors, 'total': total}
raise exceptions.CommandError(msg) raise exceptions.CommandError(msg)
@ -90,7 +91,11 @@ class ListAccessRule(command.Lister):
).id ).id
else: else:
conn = self.app.client_manager.sdk_connection conn = self.app.client_manager.sdk_connection
user_id = conn.config.get_auth().get_user_id(conn.identity) auth = conn.config.get_auth()
if auth is None:
# this will never happen
raise exceptions.CommandError('invalid authentication info')
user_id = auth.get_user_id(conn.identity)
columns = ('ID', 'Service', 'Method', 'Path') columns = ('ID', 'Service', 'Method', 'Path')
data = identity_client.access_rules(user=user_id) data = identity_client.access_rules(user=user_id)
@ -122,7 +127,11 @@ class ShowAccessRule(command.ShowOne):
def take_action(self, parsed_args): def take_action(self, parsed_args):
identity_client = self.app.client_manager.sdk_connection.identity identity_client = self.app.client_manager.sdk_connection.identity
conn = self.app.client_manager.sdk_connection conn = self.app.client_manager.sdk_connection
user_id = conn.config.get_auth().get_user_id(conn.identity) auth = conn.config.get_auth()
if auth is None:
# this will never happen
raise exceptions.CommandError('invalid authentication info')
user_id = auth.get_user_id(conn.identity)
access_rule = identity_client.get_access_rule( access_rule = identity_client.get_access_rule(
user_id, parsed_args.access_rule user_id, parsed_args.access_rule

View file

@ -18,19 +18,95 @@
import datetime import datetime
import json import json
import logging import logging
import typing as ty
import uuid import uuid
from osc_lib.command import command from cliff import columns as cliff_columns
from osc_lib import exceptions from osc_lib import exceptions
from osc_lib import utils from osc_lib import utils
from openstackclient import command
from openstackclient.i18n import _ from openstackclient.i18n import _
from openstackclient.identity import common from openstackclient.identity import common
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
class RolesColumn(cliff_columns.FormattableColumn[ty.Any]):
"""Generate a formatted string of role names."""
def human_readable(self):
return utils.format_list(list(r['name'] for r in self._value))
def _format_application_credential(
application_credential, *, include_secret=False
):
column_headers: tuple[str, ...] = (
'ID',
'Name',
'Description',
'Project ID',
'Roles',
'Unrestricted',
'Access Rules',
'Expires At',
)
columns: tuple[str, ...] = (
'id',
'name',
'description',
'project_id',
'roles',
'unrestricted',
'access_rules',
'expires_at',
)
if include_secret:
column_headers += ('Secret',)
columns += ('secret',)
return (
column_headers,
utils.get_item_properties(
application_credential, columns, formatters={'roles': RolesColumn}
),
)
def _format_application_credentials(application_credentials):
column_headers = (
'ID',
'Name',
'Description',
'Project ID',
'Roles',
'Unrestricted',
'Access Rules',
'Expires At',
)
columns = (
'id',
'name',
'description',
'project_id',
'roles',
'unrestricted',
'access_rules',
'expires_at',
)
return (
column_headers,
(
utils.get_item_properties(
x, columns, formatters={'roles': RolesColumn}
)
for x in application_credentials
),
)
# TODO(stephenfin): Move this to osc_lib since it's useful elsewhere # TODO(stephenfin): Move this to osc_lib since it's useful elsewhere
def is_uuid_like(value) -> bool: def is_uuid_like(value) -> bool:
"""Returns validation of a value as a UUID. """Returns validation of a value as a UUID.
@ -38,9 +114,6 @@ def is_uuid_like(value) -> bool:
:param val: Value to verify :param val: Value to verify
:type val: string :type val: string
:returns: bool :returns: bool
.. versionchanged:: 1.1.1
Support non-lowercase UUIDs.
""" """
try: try:
formatted_value = ( formatted_value = (
@ -76,6 +149,7 @@ class CreateApplicationCredential(command.ShowOne):
parser.add_argument( parser.add_argument(
'--role', '--role',
metavar='<role>', metavar='<role>',
dest='roles',
action='append', action='append',
default=[], default=[],
help=_( help=_(
@ -132,10 +206,15 @@ class CreateApplicationCredential(command.ShowOne):
def take_action(self, parsed_args): def take_action(self, parsed_args):
identity_client = self.app.client_manager.sdk_connection.identity identity_client = self.app.client_manager.sdk_connection.identity
conn = self.app.client_manager.sdk_connection conn = self.app.client_manager.sdk_connection
user_id = conn.config.get_auth().get_user_id(conn.identity) auth = conn.config.get_auth()
if auth is None:
# this will never happen
raise exceptions.CommandError('invalid authentication info')
user_id = auth.get_user_id(conn.identity)
role_ids = [] role_ids = []
for role in parsed_args.role: for role in parsed_args.roles:
if is_uuid_like(role): if is_uuid_like(role):
role_ids.append({'id': role}) role_ids.append({'id': role})
else: else:
@ -179,31 +258,8 @@ class CreateApplicationCredential(command.ShowOne):
access_rules=access_rules, access_rules=access_rules,
) )
# Format roles into something sensible return _format_application_credential(
if application_credential['roles']: application_credential, include_secret=True
roles = application_credential['roles']
msg = ' '.join(r['name'] for r in roles)
application_credential['roles'] = msg
columns = (
'id',
'name',
'description',
'project_id',
'roles',
'unrestricted',
'access_rules',
'expires_at',
'secret',
)
return (
columns,
(
utils.get_dict_properties(
application_credential,
columns,
)
),
) )
@ -223,13 +279,18 @@ class DeleteApplicationCredential(command.Command):
def take_action(self, parsed_args): def take_action(self, parsed_args):
identity_client = self.app.client_manager.sdk_connection.identity identity_client = self.app.client_manager.sdk_connection.identity
conn = self.app.client_manager.sdk_connection conn = self.app.client_manager.sdk_connection
user_id = conn.config.get_auth().get_user_id(conn.identity) auth = conn.config.get_auth()
if auth is None:
# this will never happen
raise exceptions.CommandError('invalid authentication info')
user_id = auth.get_user_id(conn.identity)
errors = 0 errors = 0
for ac in parsed_args.application_credential: for ac in parsed_args.application_credential:
try: try:
app_cred = identity_client.find_application_credential( app_cred = identity_client.find_application_credential(
user_id, ac user_id, ac, ignore_missing=False
) )
identity_client.delete_application_credential( identity_client.delete_application_credential(
user_id, app_cred.id user_id, app_cred.id
@ -252,6 +313,8 @@ class DeleteApplicationCredential(command.Command):
) % {'errors': errors, 'total': total} ) % {'errors': errors, 'total': total}
raise exceptions.CommandError(msg) raise exceptions.CommandError(msg)
return None
class ListApplicationCredential(command.Lister): class ListApplicationCredential(command.Lister):
_description = _("List application credentials") _description = _("List application credentials")
@ -269,46 +332,23 @@ class ListApplicationCredential(command.Lister):
def take_action(self, parsed_args): def take_action(self, parsed_args):
identity_client = self.app.client_manager.sdk_connection.identity identity_client = self.app.client_manager.sdk_connection.identity
if parsed_args.user: if parsed_args.user:
user_id = common.find_user( user_id = common.find_user_id_sdk(
identity_client, parsed_args.user, parsed_args.user_domain identity_client, parsed_args.user, parsed_args.user_domain
).id )
else: else:
conn = self.app.client_manager.sdk_connection conn = self.app.client_manager.sdk_connection
user_id = conn.config.get_auth().get_user_id(conn.identity) auth = conn.config.get_auth()
if auth is None:
# this will never happen
raise exceptions.CommandError('invalid authentication info')
user_id = auth.get_user_id(conn.identity)
data = identity_client.application_credentials(user=user_id) application_credentials = identity_client.application_credentials(
user=user_id
data_formatted = []
for ac in data:
# Format roles into something sensible
roles = ac['roles']
msg = ' '.join(r['name'] for r in roles)
ac['roles'] = msg
data_formatted.append(ac)
columns = (
'ID',
'Name',
'Description',
'Project ID',
'Roles',
'Unrestricted',
'Access Rules',
'Expires At',
)
return (
columns,
(
utils.get_item_properties(
s,
columns,
formatters={},
)
for s in data_formatted
),
) )
return _format_application_credentials(application_credentials)
class ShowApplicationCredential(command.ShowOne): class ShowApplicationCredential(command.ShowOne):
_description = _("Display application credential details") _description = _("Display application credential details")
@ -325,33 +365,14 @@ class ShowApplicationCredential(command.ShowOne):
def take_action(self, parsed_args): def take_action(self, parsed_args):
identity_client = self.app.client_manager.sdk_connection.identity identity_client = self.app.client_manager.sdk_connection.identity
conn = self.app.client_manager.sdk_connection conn = self.app.client_manager.sdk_connection
user_id = conn.config.get_auth().get_user_id(conn.identity) auth = conn.config.get_auth()
if auth is None:
# this will never happen
raise exceptions.CommandError('invalid authentication info')
user_id = auth.get_user_id(conn.identity)
app_cred = identity_client.find_application_credential( application_credential = identity_client.find_application_credential(
user_id, parsed_args.application_credential user_id, parsed_args.application_credential, ignore_missing=False
) )
# Format roles into something sensible return _format_application_credential(application_credential)
roles = app_cred['roles']
msg = ' '.join(r['name'] for r in roles)
app_cred['roles'] = msg
columns = (
'id',
'name',
'description',
'project_id',
'roles',
'unrestricted',
'access_rules',
'expires_at',
)
return (
columns,
(
utils.get_dict_properties(
app_cred,
columns,
)
),
)

View file

@ -9,24 +9,24 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
#
"""Identity v3 Service Catalog action implementations""" """Identity v3 Service Catalog action implementations"""
import logging import logging
import typing as ty
from cliff import columns as cliff_columns from cliff import columns as cliff_columns
from osc_lib.command import command
from osc_lib import exceptions from osc_lib import exceptions
from osc_lib import utils from osc_lib import utils
from openstackclient import command
from openstackclient.i18n import _ from openstackclient.i18n import _
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
class EndpointsColumn(cliff_columns.FormattableColumn): class EndpointsColumn(cliff_columns.FormattableColumn[ty.Any]):
def human_readable(self): def human_readable(self):
if not self._value: if not self._value:
return "" return ""

View file

@ -17,10 +17,10 @@
import logging import logging
from osc_lib.command import command
from osc_lib import exceptions from osc_lib import exceptions
from osc_lib import utils from osc_lib import utils
from openstackclient import command
from openstackclient.i18n import _ from openstackclient.i18n import _
@ -82,9 +82,10 @@ class DeleteConsumer(command.Command):
if result > 0: if result > 0:
total = len(parsed_args.consumer) total = len(parsed_args.consumer)
msg = _( msg = _("%(result)s of %(total)s consumers failed to delete.") % {
"%(result)s of %(total)s consumers failed " "to delete." 'result': result,
) % {'result': result, 'total': total} 'total': total,
}
raise exceptions.CommandError(msg) raise exceptions.CommandError(msg)

View file

@ -17,10 +17,10 @@
import logging import logging
from osc_lib.command import command
from osc_lib import exceptions from osc_lib import exceptions
from osc_lib import utils from osc_lib import utils
from openstackclient import command
from openstackclient.i18n import _ from openstackclient.i18n import _
from openstackclient.identity import common from openstackclient.identity import common
@ -28,6 +28,23 @@ from openstackclient.identity import common
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
def _format_credential(credential):
columns = (
'blob',
'id',
'project_id',
'type',
'user_id',
)
return (
columns,
utils.get_item_properties(
credential,
columns,
),
)
class CreateCredential(command.ShowOne): class CreateCredential(command.ShowOne):
_description = _("Create new credential") _description = _("Create new credential")
@ -53,32 +70,30 @@ class CreateCredential(command.ShowOne):
'--project', '--project',
metavar='<project>', metavar='<project>',
help=_( help=_(
'Project which limits the scope of ' 'Project which limits the scope of the credential (name or ID)'
'the credential (name or ID)'
), ),
) )
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
identity_client = self.app.client_manager.identity identity_client = self.app.client_manager.sdk_connection.identity
user_id = utils.find_resource( user_id = identity_client.find_user(
identity_client.users, parsed_args.user parsed_args.user, ignore_missing=False
).id ).id
if parsed_args.project: if parsed_args.project:
project = utils.find_resource( project = identity_client.find_project(
identity_client.projects, parsed_args.project parsed_args.project, ignore_missing=False
).id ).id
else: else:
project = None project = None
credential = identity_client.credentials.create( credential = identity_client.create_credential(
user=user_id, user_id=user_id,
type=parsed_args.type, type=parsed_args.type,
blob=parsed_args.data, blob=parsed_args.data,
project=project, project_id=project,
) )
credential._info.pop('links') return _format_credential(credential)
return zip(*sorted(credential._info.items()))
class DeleteCredential(command.Command): class DeleteCredential(command.Command):
@ -95,11 +110,11 @@ class DeleteCredential(command.Command):
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
identity_client = self.app.client_manager.identity identity_client = self.app.client_manager.sdk_connection.identity
result = 0 result = 0
for i in parsed_args.credential: for i in parsed_args.credential:
try: try:
identity_client.credentials.delete(i) identity_client.delete_credential(i)
except Exception as e: except Exception as e:
result += 1 result += 1
LOG.error( LOG.error(
@ -112,9 +127,10 @@ class DeleteCredential(command.Command):
if result > 0: if result > 0:
total = len(parsed_args.credential) total = len(parsed_args.credential)
msg = _( msg = _("%(result)s of %(total)s credential failed to delete.") % {
"%(result)s of %(total)s credential failed " "to delete." 'result': result,
) % {'result': result, 'total': total} 'total': total,
}
raise exceptions.CommandError(msg) raise exceptions.CommandError(msg)
@ -137,14 +153,17 @@ class ListCredential(command.Lister):
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
identity_client = self.app.client_manager.identity identity_client = self.app.client_manager.sdk_connection.identity
kwargs = {} kwargs = {}
if parsed_args.user: if parsed_args.user:
user_id = common.find_user( domain_id = None
identity_client, if parsed_args.user_domain:
parsed_args.user, domain_id = identity_client.find_domain(
parsed_args.user_domain, parsed_args.user_domain, ignore_missing=False
)
user_id = identity_client.find_user(
parsed_args.user, domain_id=domain_id, ignore_missing=False
).id ).id
kwargs["user_id"] = user_id kwargs["user_id"] = user_id
@ -153,7 +172,8 @@ class ListCredential(command.Lister):
columns = ('ID', 'Type', 'User ID', 'Blob', 'Project ID') columns = ('ID', 'Type', 'User ID', 'Blob', 'Project ID')
column_headers = ('ID', 'Type', 'User ID', 'Data', 'Project ID') column_headers = ('ID', 'Type', 'User ID', 'Data', 'Project ID')
data = self.app.client_manager.identity.credentials.list(**kwargs) data = identity_client.credentials(**kwargs)
return ( return (
column_headers, column_headers,
( (
@ -199,27 +219,26 @@ class SetCredential(command.Command):
'--project', '--project',
metavar='<project>', metavar='<project>',
help=_( help=_(
'Project which limits the scope of ' 'Project which limits the scope of the credential (name or ID)'
'the credential (name or ID)'
), ),
) )
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
identity_client = self.app.client_manager.identity identity_client = self.app.client_manager.sdk_connection.identity
user_id = utils.find_resource( user_id = identity_client.find_user(
identity_client.users, parsed_args.user parsed_args.user, ignore_missing=False
).id ).id
if parsed_args.project: if parsed_args.project:
project = utils.find_resource( project = identity_client.find_project(
identity_client.projects, parsed_args.project parsed_args.project, ignore_missing=False
).id ).id
else: else:
project = None project = None
identity_client.credentials.update( identity_client.update_credential(
parsed_args.credential, parsed_args.credential,
user=user_id, user=user_id,
type=parsed_args.type, type=parsed_args.type,
@ -241,10 +260,7 @@ class ShowCredential(command.ShowOne):
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
identity_client = self.app.client_manager.identity identity_client = self.app.client_manager.sdk_connection.identity
credential = utils.find_resource( credential = identity_client.get_credential(parsed_args.credential)
identity_client.credentials, parsed_args.credential
)
credential._info.pop('links') return _format_credential(credential)
return zip(*sorted(credential._info.items()))

View file

@ -17,11 +17,11 @@
import logging import logging
from keystoneauth1 import exceptions as ks_exc from openstack import exceptions as sdk_exceptions
from osc_lib.command import command
from osc_lib import exceptions from osc_lib import exceptions
from osc_lib import utils from osc_lib import utils
from openstackclient import command
from openstackclient.i18n import _ from openstackclient.i18n import _
from openstackclient.identity import common from openstackclient.identity import common
@ -29,6 +29,31 @@ from openstackclient.identity import common
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
def _format_domain(domain):
columns = (
'id',
'name',
'is_enabled',
'description',
'options',
)
column_headers = (
'id',
'name',
'enabled',
'description',
'options',
)
return (
column_headers,
utils.get_item_properties(
domain,
columns,
),
)
class CreateDomain(command.ShowOne): class CreateDomain(command.ShowOne):
_description = _("Create new domain") _description = _("Create new domain")
@ -47,12 +72,15 @@ class CreateDomain(command.ShowOne):
enable_group = parser.add_mutually_exclusive_group() enable_group = parser.add_mutually_exclusive_group()
enable_group.add_argument( enable_group.add_argument(
'--enable', '--enable',
dest='is_enabled',
action='store_true', action='store_true',
default=True,
help=_('Enable domain (default)'), help=_('Enable domain (default)'),
) )
enable_group.add_argument( enable_group.add_argument(
'--disable', '--disable',
action='store_true', dest='is_enabled',
action='store_false',
help=_('Disable domain'), help=_('Disable domain'),
) )
parser.add_argument( parser.add_argument(
@ -64,32 +92,29 @@ class CreateDomain(command.ShowOne):
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
identity_client = self.app.client_manager.identity identity_client = self.app.client_manager.sdk_connection.identity
enabled = True options = {}
if parsed_args.disable: if parsed_args.immutable is not None:
enabled = False options['immutable'] = parsed_args.immutable
options = common.get_immutable_options(parsed_args)
try: try:
domain = identity_client.domains.create( domain = identity_client.create_domain(
name=parsed_args.name, name=parsed_args.name,
description=parsed_args.description, description=parsed_args.description,
options=options, options=options,
enabled=enabled, is_enabled=parsed_args.is_enabled,
) )
except ks_exc.Conflict: except sdk_exceptions.ConflictException:
if parsed_args.or_show: if parsed_args.or_show:
domain = utils.find_resource( domain = identity_client.find_domain(
identity_client.domains, parsed_args.name parsed_args.name, ignore_missing=False
) )
LOG.info(_('Returning existing domain %s'), domain.name) LOG.info(_('Returning existing domain %s'), domain.name)
else: else:
raise raise
domain._info.pop('links') return _format_domain(domain)
return zip(*sorted(domain._info.items()))
class DeleteDomain(command.Command): class DeleteDomain(command.Command):
@ -106,12 +131,12 @@ class DeleteDomain(command.Command):
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
identity_client = self.app.client_manager.identity identity_client = self.app.client_manager.sdk_connection.identity
result = 0 result = 0
for i in parsed_args.domain: for i in parsed_args.domain:
try: try:
domain = utils.find_resource(identity_client.domains, i) domain = identity_client.find_domain(i, ignore_missing=False)
identity_client.domains.delete(domain.id) identity_client.delete_domain(domain.id)
except Exception as e: except Exception as e:
result += 1 result += 1
LOG.error( LOG.error(
@ -124,7 +149,7 @@ class DeleteDomain(command.Command):
if result > 0: if result > 0:
total = len(parsed_args.domain) total = len(parsed_args.domain)
msg = _("%(result)s of %(total)s domains failed " "to delete.") % { msg = _("%(result)s of %(total)s domains failed to delete.") % {
'result': result, 'result': result,
'total': total, 'total': total,
} }
@ -143,7 +168,7 @@ class ListDomain(command.Lister):
) )
parser.add_argument( parser.add_argument(
'--enabled', '--enabled',
dest='enabled', dest='is_enabled',
action='store_true', action='store_true',
help=_('The domains that are enabled will be returned'), help=_('The domains that are enabled will be returned'),
) )
@ -153,13 +178,17 @@ class ListDomain(command.Lister):
kwargs = {} kwargs = {}
if parsed_args.name: if parsed_args.name:
kwargs['name'] = parsed_args.name kwargs['name'] = parsed_args.name
if parsed_args.enabled: if parsed_args.is_enabled:
kwargs['enabled'] = True kwargs['is_enabled'] = True
columns = ('id', 'name', 'is_enabled', 'description')
column_headers = ('ID', 'Name', 'Enabled', 'Description')
data = self.app.client_manager.sdk_connection.identity.domains(
**kwargs
)
columns = ('ID', 'Name', 'Enabled', 'Description')
data = self.app.client_manager.identity.domains.list(**kwargs)
return ( return (
columns, column_headers,
( (
utils.get_item_properties( utils.get_item_properties(
s, s,
@ -194,38 +223,37 @@ class SetDomain(command.Command):
enable_group = parser.add_mutually_exclusive_group() enable_group = parser.add_mutually_exclusive_group()
enable_group.add_argument( enable_group.add_argument(
'--enable', '--enable',
dest='is_enabled',
action='store_true', action='store_true',
default=None,
help=_('Enable domain'), help=_('Enable domain'),
) )
enable_group.add_argument( enable_group.add_argument(
'--disable', '--disable',
action='store_true', dest='is_enabled',
action='store_false',
default=None,
help=_('Disable domain'), help=_('Disable domain'),
) )
common.add_resource_option_to_parser(parser) common.add_resource_option_to_parser(parser)
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
identity_client = self.app.client_manager.identity identity_client = self.app.client_manager.sdk_connection.identity
domain = utils.find_resource( domain = identity_client.find_domain(
identity_client.domains, parsed_args.domain parsed_args.domain, ignore_missing=False
) )
kwargs = {} kwargs = {}
if parsed_args.name: if parsed_args.name:
kwargs['name'] = parsed_args.name kwargs['name'] = parsed_args.name
if parsed_args.description: if parsed_args.description:
kwargs['description'] = parsed_args.description kwargs['description'] = parsed_args.description
if parsed_args.is_enabled is not None:
kwargs['is_enabled'] = parsed_args.is_enabled
if parsed_args.immutable is not None:
kwargs['options'] = {'immutable': parsed_args.immutable}
if parsed_args.enable: identity_client.update_domain(domain.id, **kwargs)
kwargs['enabled'] = True
if parsed_args.disable:
kwargs['enabled'] = False
options = common.get_immutable_options(parsed_args)
if options:
kwargs['options'] = options
identity_client.domains.update(domain.id, **kwargs)
class ShowDomain(command.ShowOne): class ShowDomain(command.ShowOne):
@ -241,13 +269,9 @@ class ShowDomain(command.ShowOne):
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
identity_client = self.app.client_manager.identity identity_client = self.app.client_manager.sdk_connection.identity
domain = identity_client.find_domain(
domain_str = common._get_token_resource( parsed_args.domain, ignore_missing=False
identity_client, 'domain', parsed_args.domain
) )
domain = utils.find_resource(identity_client.domains, domain_str) return _format_domain(domain)
domain._info.pop('links')
return zip(*sorted(domain._info.items()))

View file

@ -14,10 +14,10 @@
import logging import logging
from osc_lib.command import command
from osc_lib import exceptions from osc_lib import exceptions
from osc_lib import utils from osc_lib import utils
from openstackclient import command
from openstackclient.i18n import _ from openstackclient.i18n import _
from openstackclient.identity import common from openstackclient.identity import common
@ -156,9 +156,10 @@ class DeleteEC2Creds(command.Command):
if result > 0: if result > 0:
total = len(parsed_args.access_key) total = len(parsed_args.access_key)
msg = _( msg = _("%(result)s of %(total)s EC2 keys failed to delete.") % {
"%(result)s of %(total)s EC2 keys failed " "to delete." 'result': result,
) % {'result': result, 'total': total} 'total': total,
}
raise exceptions.CommandError(msg) raise exceptions.CommandError(msg)

View file

@ -17,10 +17,10 @@
import logging import logging
from osc_lib.command import command
from osc_lib import exceptions from osc_lib import exceptions
from osc_lib import utils from osc_lib import utils
from openstackclient import command
from openstackclient.i18n import _ from openstackclient.i18n import _
from openstackclient.identity import common from openstackclient.identity import common
@ -28,11 +28,31 @@ from openstackclient.identity import common
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
def get_service_name(service): def _format_endpoint(endpoint, service):
if hasattr(service, 'name'): columns = (
return service.name 'is_enabled',
else: 'id',
return '' 'interface',
'region_id',
'region_id',
'service_id',
'url',
)
column_headers = (
'enabled',
'id',
'interface',
'region',
'region_id',
'service_id',
'url',
'service_name',
'service_type',
)
data = utils.get_item_properties(endpoint, columns)
data += (getattr(service, 'name', ''), service.type)
return column_headers, data
class AddProjectToEndpoint(command.Command): class AddProjectToEndpoint(command.Command):
@ -44,15 +64,13 @@ class AddProjectToEndpoint(command.Command):
'endpoint', 'endpoint',
metavar='<endpoint>', metavar='<endpoint>',
help=_( help=_(
'Endpoint to associate with ' 'specified project (name or ID)' 'Endpoint to associate with specified project (name or ID)'
), ),
) )
parser.add_argument( parser.add_argument(
'project', 'project',
metavar='<project>', metavar='<project>',
help=_( help=_('Project to associate with specified endpoint name or ID)'),
'Project to associate with ' 'specified endpoint name or ID)'
),
) )
common.add_project_domain_option_to_parser(parser) common.add_project_domain_option_to_parser(parser)
return parser return parser
@ -114,23 +132,23 @@ class CreateEndpoint(command.ShowOne):
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
identity_client = self.app.client_manager.identity identity_client = self.app.client_manager.sdk_connection.identity
service = common.find_service(identity_client, parsed_args.service) service = common.find_service_sdk(identity_client, parsed_args.service)
endpoint = identity_client.endpoints.create( kwargs = {}
service=service.id,
url=parsed_args.url,
interface=parsed_args.interface,
region=parsed_args.region,
enabled=parsed_args.enabled,
)
info = {} kwargs['service_id'] = service.id
endpoint._info.pop('links') kwargs['url'] = parsed_args.url
info.update(endpoint._info) kwargs['interface'] = parsed_args.interface
info['service_name'] = get_service_name(service) kwargs['is_enabled'] = parsed_args.enabled
info['service_type'] = service.type
return zip(*sorted(info.items())) if parsed_args.region:
region = identity_client.get_region(parsed_args.region)
kwargs['region_id'] = region.id
endpoint = identity_client.create_endpoint(**kwargs)
return _format_endpoint(endpoint, service=service)
class DeleteEndpoint(command.Command): class DeleteEndpoint(command.Command):
@ -147,14 +165,14 @@ class DeleteEndpoint(command.Command):
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
identity_client = self.app.client_manager.identity identity_client = self.app.client_manager.sdk_connection.identity
result = 0 result = 0
for i in parsed_args.endpoint: for i in parsed_args.endpoint:
try: try:
endpoint_id = utils.find_resource( endpoint_id = identity_client.find_endpoint(
identity_client.endpoints, i i, ignore_missing=False
).id ).id
identity_client.endpoints.delete(endpoint_id) identity_client.delete_endpoint(endpoint_id)
except Exception as e: except Exception as e:
result += 1 result += 1
LOG.error( LOG.error(
@ -167,9 +185,10 @@ class DeleteEndpoint(command.Command):
if result > 0: if result > 0:
total = len(parsed_args.endpoint) total = len(parsed_args.endpoint)
msg = _( msg = _("%(result)s of %(total)s endpoints failed to delete.") % {
"%(result)s of %(total)s endpoints failed " "to delete." 'result': result,
) % {'result': result, 'total': total} 'total': total,
}
raise exceptions.CommandError(msg) raise exceptions.CommandError(msg)
@ -209,28 +228,37 @@ class ListEndpoint(command.Lister):
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
identity_client = self.app.client_manager.identity identity_client = self.app.client_manager.sdk_connection.identity
endpoint = None endpoint = None
if parsed_args.endpoint: if parsed_args.endpoint:
endpoint = utils.find_resource( endpoint = identity_client.find_endpoint(
identity_client.endpoints, parsed_args.endpoint parsed_args.endpoint, ignore_missing=False
) )
project = None
project_domain_id = None
if parsed_args.project_domain:
project_domain_id = common._find_sdk_id(
identity_client.find_domain,
name_or_id=parsed_args.project_domain,
)
project_id = None
if parsed_args.project: if parsed_args.project:
project = common.find_project( project_id = common._find_sdk_id(
identity_client, identity_client.find_project,
parsed_args.project, name_or_id=common._get_token_resource(
parsed_args.project_domain, identity_client, 'project', parsed_args.project
),
domain_id=project_domain_id,
) )
if endpoint: if endpoint:
columns = ('ID', 'Name') column_headers: tuple[str, ...] = ('ID', 'Name')
data = identity_client.endpoint_filter.list_projects_for_endpoint( columns: tuple[str, ...] = ('id', 'name')
endpoint=endpoint.id data = identity_client.endpoint_projects(endpoint=endpoint.id)
)
else: else:
columns = ( column_headers = (
'ID', 'ID',
'Region', 'Region',
'Service Name', 'Service Name',
@ -239,37 +267,43 @@ class ListEndpoint(command.Lister):
'Interface', 'Interface',
'URL', 'URL',
) )
columns = (
'id',
'region_id',
'service_name',
'service_type',
'is_enabled',
'interface',
'url',
)
kwargs = {} kwargs = {}
if parsed_args.service: if parsed_args.service:
service = common.find_service( service = common.find_service_sdk(
identity_client, parsed_args.service identity_client, parsed_args.service
) )
kwargs['service'] = service.id kwargs['service_id'] = service.id
if parsed_args.interface: if parsed_args.interface:
kwargs['interface'] = parsed_args.interface kwargs['interface'] = parsed_args.interface
if parsed_args.region: if parsed_args.region:
kwargs['region'] = parsed_args.region region = identity_client.get_region(parsed_args.region)
kwargs['region_id'] = region.id
if project: if project_id:
data = ( data = list(
identity_client.endpoint_filter.list_endpoints_for_project( identity_client.project_endpoints(project=project_id)
project=project.id
)
) )
else: else:
data = identity_client.endpoints.list(**kwargs) data = list(identity_client.endpoints(**kwargs))
service_list = identity_client.services.list()
for ep in data: for ep in data:
service = common.find_service_in_list( service = identity_client.find_service(
service_list, ep.service_id ep.service_id, ignore_missing=False
) )
ep.service_name = get_service_name(service) ep.service_name = getattr(service, 'name', '')
ep.service_type = service.type ep.service_type = service.type
return ( return (
columns, column_headers,
( (
utils.get_item_properties( utils.get_item_properties(
s, s,
@ -290,14 +324,14 @@ class RemoveProjectFromEndpoint(command.Command):
'endpoint', 'endpoint',
metavar='<endpoint>', metavar='<endpoint>',
help=_( help=_(
'Endpoint to dissociate from ' 'specified project (name or ID)' 'Endpoint to dissociate from specified project (name or ID)'
), ),
) )
parser.add_argument( parser.add_argument(
'project', 'project',
metavar='<project>', metavar='<project>',
help=_( help=_(
'Project to dissociate from ' 'specified endpoint name or ID)' 'Project to dissociate from specified endpoint name or ID)'
), ),
) )
common.add_project_domain_option_to_parser(parser) common.add_project_domain_option_to_parser(parser)
@ -364,28 +398,36 @@ class SetEndpoint(command.Command):
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
identity_client = self.app.client_manager.identity identity_client = self.app.client_manager.sdk_connection.identity
endpoint = utils.find_resource( endpoint = identity_client.find_endpoint(
identity_client.endpoints, parsed_args.endpoint parsed_args.endpoint, ignore_missing=False
) )
service_id = None kwargs = {}
if parsed_args.service:
service = common.find_service(identity_client, parsed_args.service)
service_id = service.id
enabled = None
if parsed_args.enabled:
enabled = True
if parsed_args.disabled:
enabled = False
identity_client.endpoints.update( if parsed_args.service:
service = common.find_service_sdk(
identity_client, parsed_args.service
)
kwargs['service_id'] = service.id
if parsed_args.enabled:
kwargs['is_enabled'] = True
if parsed_args.disabled:
kwargs['is_enabled'] = False
if parsed_args.url:
kwargs['url'] = parsed_args.url
if parsed_args.interface:
kwargs['interface'] = parsed_args.interface
if parsed_args.region:
kwargs['region_id'] = parsed_args.region
identity_client.update_endpoint(
endpoint.id, endpoint.id,
service=service_id, **kwargs,
url=parsed_args.url,
interface=parsed_args.interface,
region=parsed_args.region,
enabled=enabled,
) )
@ -405,16 +447,11 @@ class ShowEndpoint(command.ShowOne):
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
identity_client = self.app.client_manager.identity identity_client = self.app.client_manager.sdk_connection.identity
endpoint = utils.find_resource( endpoint = identity_client.find_endpoint(
identity_client.endpoints, parsed_args.endpoint parsed_args.endpoint, ignore_missing=False
) )
service = common.find_service(identity_client, endpoint.service_id) service = common.find_service_sdk(identity_client, endpoint.service_id)
info = {} return _format_endpoint(endpoint, service)
endpoint._info.pop('links')
info.update(endpoint._info)
info['service_name'] = get_service_name(service)
info['service_type'] = service.type
return zip(*sorted(info.items()))

View file

@ -16,10 +16,10 @@
import json import json
import logging import logging
from osc_lib.command import command
from osc_lib import exceptions from osc_lib import exceptions
from osc_lib import utils from osc_lib import utils
from openstackclient import command
from openstackclient.i18n import _ from openstackclient.i18n import _
from openstackclient.identity import common from openstackclient.identity import common
@ -168,7 +168,7 @@ class DeleteEndpointGroup(command.Command):
if result > 0: if result > 0:
total = len(parsed_args.endpointgroup) total = len(parsed_args.endpointgroup)
msg = _( msg = _(
"%(result)s of %(total)s endpointgroups failed " "to delete." "%(result)s of %(total)s endpointgroups failed to delete."
) % {'result': result, 'total': total} ) % {'result': result, 'total': total}
raise exceptions.CommandError(msg) raise exceptions.CommandError(msg)

View file

@ -16,10 +16,10 @@
import logging import logging
from osc_lib.command import command
from osc_lib import exceptions from osc_lib import exceptions
from osc_lib import utils from osc_lib import utils
from openstackclient import command
from openstackclient.i18n import _ from openstackclient.i18n import _

View file

@ -17,11 +17,11 @@
import logging import logging
from keystoneauth1 import exceptions as ks_exc from openstack import exceptions as sdk_exc
from osc_lib.command import command
from osc_lib import exceptions from osc_lib import exceptions
from osc_lib import utils from osc_lib import utils
from openstackclient import command
from openstackclient.i18n import _ from openstackclient.i18n import _
from openstackclient.identity import common from openstackclient.identity import common
@ -29,6 +29,25 @@ from openstackclient.identity import common
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
def _format_group(group):
columns = (
'description',
'domain_id',
'id',
'name',
)
column_headers = (
'description',
'domain_id',
'id',
'name',
)
return (
column_headers,
utils.get_item_properties(group, columns),
)
class AddUserToGroup(command.Command): class AddUserToGroup(command.Command):
_description = _("Add user to group") _description = _("Add user to group")
@ -53,19 +72,19 @@ class AddUserToGroup(command.Command):
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
identity_client = self.app.client_manager.identity identity_client = self.app.client_manager.sdk_connection.identity
group_id = common.find_group( group_id = common.find_group_id_sdk(
identity_client, parsed_args.group, parsed_args.group_domain identity_client, parsed_args.group, parsed_args.group_domain
).id )
result = 0 result = 0
for i in parsed_args.user: for i in parsed_args.user:
try: try:
user_id = common.find_user( user_id = common.find_user_id_sdk(
identity_client, i, parsed_args.user_domain identity_client, i, parsed_args.user_domain
).id )
identity_client.users.add_to_group(user_id, group_id) identity_client.add_user_to_group(user_id, group_id)
except Exception as e: except Exception as e:
result += 1 result += 1
msg = _("%(user)s not added to group %(group)s: %(e)s") % { msg = _("%(user)s not added to group %(group)s: %(e)s") % {
@ -109,32 +128,41 @@ class CheckUserInGroup(command.Command):
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
identity_client = self.app.client_manager.identity identity_client = self.app.client_manager.sdk_connection.identity
user_id = common.find_user( user_id = common.find_user_id_sdk(
identity_client, parsed_args.user, parsed_args.user_domain identity_client,
).id parsed_args.user,
group_id = common.find_group( parsed_args.user_domain,
identity_client, parsed_args.group, parsed_args.group_domain validate_actor_existence=False,
).id )
group_id = common.find_group_id_sdk(
identity_client,
parsed_args.group,
parsed_args.group_domain,
validate_actor_existence=False,
)
user_in_group = False
try: try:
identity_client.users.check_in_group(user_id, group_id) user_in_group = identity_client.check_user_in_group(
except ks_exc.http.HTTPClientError as e: user_id, group_id
if e.http_status == 403 or e.http_status == 404: )
msg = _("%(user)s not in group %(group)s\n") % { except sdk_exc.ForbiddenException:
'user': parsed_args.user, # Assume False if forbidden
'group': parsed_args.group, pass
} if user_in_group:
self.app.stderr.write(msg)
else:
raise e
else:
msg = _("%(user)s in group %(group)s\n") % { msg = _("%(user)s in group %(group)s\n") % {
'user': parsed_args.user, 'user': parsed_args.user,
'group': parsed_args.group, 'group': parsed_args.group,
} }
self.app.stdout.write(msg) self.app.stdout.write(msg)
else:
msg = _("%(user)s not in group %(group)s\n") % {
'user': parsed_args.user,
'group': parsed_args.group,
}
self.app.stderr.write(msg)
class CreateGroup(command.ShowOne): class CreateGroup(command.ShowOne):
@ -165,29 +193,38 @@ class CreateGroup(command.ShowOne):
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
identity_client = self.app.client_manager.identity identity_client = self.app.client_manager.sdk_connection.identity
domain = None kwargs = {}
if parsed_args.name:
kwargs['name'] = parsed_args.name
if parsed_args.description:
kwargs['description'] = parsed_args.description
if parsed_args.domain: if parsed_args.domain:
domain = common.find_domain(identity_client, parsed_args.domain).id kwargs['domain_id'] = common.find_domain_id_sdk(
identity_client, parsed_args.domain
)
try: try:
group = identity_client.groups.create( group = identity_client.create_group(**kwargs)
name=parsed_args.name, except sdk_exc.ConflictException:
domain=domain,
description=parsed_args.description,
)
except ks_exc.Conflict:
if parsed_args.or_show: if parsed_args.or_show:
group = utils.find_resource( if parsed_args.domain:
identity_client.groups, parsed_args.name, domain_id=domain group = identity_client.find_group(
parsed_args.name,
domain_id=parsed_args.domain,
ignore_missing=False,
)
else:
group = identity_client.find_group(
parsed_args.name,
ignore_missing=False,
) )
LOG.info(_('Returning existing group %s'), group.name) LOG.info(_('Returning existing group %s'), group.name)
else: else:
raise raise
group._info.pop('links') return _format_group(group)
return zip(*sorted(group._info.items()))
class DeleteGroup(command.Command): class DeleteGroup(command.Command):
@ -209,15 +246,15 @@ class DeleteGroup(command.Command):
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
identity_client = self.app.client_manager.identity identity_client = self.app.client_manager.sdk_connection.identity
errors = 0 errors = 0
for group in parsed_args.groups: for group in parsed_args.groups:
try: try:
group_obj = common.find_group( group_id = common.find_group_id_sdk(
identity_client, group, parsed_args.domain identity_client, group, parsed_args.domain
) )
identity_client.groups.delete(group_obj.id) identity_client.delete_group(group_id)
except Exception as e: except Exception as e:
errors += 1 errors += 1
LOG.error( LOG.error(
@ -230,7 +267,7 @@ class DeleteGroup(command.Command):
if errors > 0: if errors > 0:
total = len(parsed_args.groups) total = len(parsed_args.groups)
msg = _("%(errors)s of %(total)s groups failed " "to delete.") % { msg = _("%(errors)s of %(total)s groups failed to delete.") % {
'errors': errors, 'errors': errors,
'total': total, 'total': total,
} }
@ -262,30 +299,38 @@ class ListGroup(command.Lister):
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
identity_client = self.app.client_manager.identity identity_client = self.app.client_manager.sdk_connection.identity
domain = None domain = None
if parsed_args.domain: if parsed_args.domain:
domain = common.find_domain(identity_client, parsed_args.domain).id domain = common.find_domain_id_sdk(
identity_client, parsed_args.domain
)
data = []
if parsed_args.user: if parsed_args.user:
user = common.find_user( user = common.find_user_id_sdk(
identity_client, identity_client,
parsed_args.user, parsed_args.user,
parsed_args.user_domain, parsed_args.user_domain,
).id )
if domain:
# NOTE(0weng): The API doesn't actually support filtering
# additionally by domain_id, so this doesn't really do
# anything.
data = identity_client.user_groups(user, domain_id=domain)
else: else:
user = None data = identity_client.user_groups(user)
else:
if domain:
data = identity_client.groups(domain_id=domain)
else:
data = identity_client.groups()
# List groups # List groups
columns: tuple[str, ...] = ('ID', 'Name')
if parsed_args.long: if parsed_args.long:
columns = ('ID', 'Name', 'Domain ID', 'Description') columns += ('Domain ID', 'Description')
else:
columns = ('ID', 'Name')
data = identity_client.groups.list(
domain=domain,
user=user,
)
return ( return (
columns, columns,
@ -324,19 +369,19 @@ class RemoveUserFromGroup(command.Command):
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
identity_client = self.app.client_manager.identity identity_client = self.app.client_manager.sdk_connection.identity
group_id = common.find_group( group_id = common.find_group_id_sdk(
identity_client, parsed_args.group, parsed_args.group_domain identity_client, parsed_args.group, parsed_args.group_domain
).id )
result = 0 result = 0
for i in parsed_args.user: for i in parsed_args.user:
try: try:
user_id = common.find_user( user_id = common.find_user_id_sdk(
identity_client, i, parsed_args.user_domain identity_client, i, parsed_args.user_domain
).id )
identity_client.users.remove_from_group(user_id, group_id) identity_client.remove_user_from_group(user_id, group_id)
except Exception as e: except Exception as e:
result += 1 result += 1
msg = _("%(user)s not removed from group %(group)s: %(e)s") % { msg = _("%(user)s not removed from group %(group)s: %(e)s") % {
@ -388,8 +433,8 @@ class SetGroup(command.Command):
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
identity_client = self.app.client_manager.identity identity_client = self.app.client_manager.sdk_connection.identity
group = common.find_group( group = common.find_group_id_sdk(
identity_client, parsed_args.group, parsed_args.domain identity_client, parsed_args.group, parsed_args.domain
) )
kwargs = {} kwargs = {}
@ -398,7 +443,7 @@ class SetGroup(command.Command):
if parsed_args.description: if parsed_args.description:
kwargs['description'] = parsed_args.description kwargs['description'] = parsed_args.description
identity_client.groups.update(group.id, **kwargs) identity_client.update_group(group, **kwargs)
class ShowGroup(command.ShowOne): class ShowGroup(command.ShowOne):
@ -419,13 +464,18 @@ class ShowGroup(command.ShowOne):
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
identity_client = self.app.client_manager.identity identity_client = self.app.client_manager.sdk_connection.identity
group = common.find_group( if parsed_args.domain:
identity_client, domain = common.find_domain_id_sdk(
parsed_args.group, identity_client, parsed_args.domain
domain_name_or_id=parsed_args.domain, )
group = identity_client.find_group(
parsed_args.group, domain_id=domain, ignore_missing=False
)
else:
group = identity_client.find_group(
parsed_args.group, ignore_missing=False
) )
group._info.pop('links') return _format_group(group)
return zip(*sorted(group._info.items()))

View file

@ -16,10 +16,10 @@
import logging import logging
from osc_lib.cli import format_columns from osc_lib.cli import format_columns
from osc_lib.command import command
from osc_lib import exceptions from osc_lib import exceptions
from osc_lib import utils from osc_lib import utils
from openstackclient import command
from openstackclient.i18n import _ from openstackclient.i18n import _
from openstackclient.identity import common from openstackclient.identity import common
@ -41,6 +41,7 @@ class CreateIdentityProvider(command.ShowOne):
identity_remote_id_provider.add_argument( identity_remote_id_provider.add_argument(
'--remote-id', '--remote-id',
metavar='<remote-id>', metavar='<remote-id>',
dest='remote_ids',
action='append', action='append',
help=_( help=_(
'Remote IDs to associate with the Identity Provider ' 'Remote IDs to associate with the Identity Provider '
@ -99,16 +100,15 @@ class CreateIdentityProvider(command.ShowOne):
def take_action(self, parsed_args): def take_action(self, parsed_args):
identity_client = self.app.client_manager.identity identity_client = self.app.client_manager.identity
remote_ids: list[str] | None = None
if parsed_args.remote_id_file: if parsed_args.remote_id_file:
file_content = utils.read_blob_file_contents( file_content = utils.read_blob_file_contents(
parsed_args.remote_id_file parsed_args.remote_id_file
) )
remote_ids = file_content.splitlines() remote_ids = file_content.splitlines()
remote_ids = list(map(str.strip, remote_ids)) remote_ids = list(map(str.strip, remote_ids))
else: elif parsed_args.remote_ids:
remote_ids = ( remote_ids = parsed_args.remote_ids
parsed_args.remote_id if parsed_args.remote_id else None
)
domain_id = None domain_id = None
if parsed_args.domain: if parsed_args.domain:
@ -137,8 +137,9 @@ class CreateIdentityProvider(command.ShowOne):
) )
idp._info.pop('links', None) idp._info.pop('links', None)
remote_ids = format_columns.ListColumn(idp._info.pop('remote_ids', [])) idp._info['remote_ids'] = format_columns.ListColumn(
idp._info['remote_ids'] = remote_ids idp._info.pop('remote_ids', [])
)
return zip(*sorted(idp._info.items())) return zip(*sorted(idp._info.items()))
@ -174,8 +175,7 @@ class DeleteIdentityProvider(command.Command):
if result > 0: if result > 0:
total = len(parsed_args.identity_provider) total = len(parsed_args.identity_provider)
msg = _( msg = _(
"%(result)s of %(total)s identity providers failed" "%(result)s of %(total)s identity providers failed to delete."
" to delete."
) % {'result': result, 'total': total} ) % {'result': result, 'total': total}
raise exceptions.CommandError(msg) raise exceptions.CommandError(msg)
@ -241,6 +241,7 @@ class SetIdentityProvider(command.Command):
identity_remote_id_provider.add_argument( identity_remote_id_provider.add_argument(
'--remote-id', '--remote-id',
metavar='<remote-id>', metavar='<remote-id>',
dest='remote_ids',
action='append', action='append',
help=_( help=_(
'Remote IDs to associate with the Identity Provider ' 'Remote IDs to associate with the Identity Provider '
@ -288,8 +289,8 @@ class SetIdentityProvider(command.Command):
) )
remote_ids = file_content.splitlines() remote_ids = file_content.splitlines()
remote_ids = list(map(str.strip, remote_ids)) remote_ids = list(map(str.strip, remote_ids))
elif parsed_args.remote_id: elif parsed_args.remote_ids:
remote_ids = parsed_args.remote_id remote_ids = parsed_args.remote_ids
# Setup keyword args for the client # Setup keyword args for the client
kwargs = {} kwargs = {}
@ -299,7 +300,7 @@ class SetIdentityProvider(command.Command):
kwargs['enabled'] = True kwargs['enabled'] = True
if parsed_args.disable: if parsed_args.disable:
kwargs['enabled'] = False kwargs['enabled'] = False
if parsed_args.remote_id_file or parsed_args.remote_id: if parsed_args.remote_id_file or parsed_args.remote_ids:
kwargs['remote_ids'] = remote_ids kwargs['remote_ids'] = remote_ids
# TODO(pas-ha) actually check for 3.14 microversion # TODO(pas-ha) actually check for 3.14 microversion

View file

@ -17,8 +17,8 @@
import logging import logging
from osc_lib.command import command
from openstackclient import command
from openstackclient.i18n import _ from openstackclient.i18n import _

View file

@ -15,10 +15,10 @@
import logging import logging
from osc_lib.command import command
from osc_lib import exceptions from osc_lib import exceptions
from osc_lib import utils from osc_lib import utils
from openstackclient import command
from openstackclient.i18n import _ from openstackclient.i18n import _
from openstackclient.identity import common as common_utils from openstackclient.identity import common as common_utils
@ -77,8 +77,7 @@ class CreateLimit(command.ShowOne):
) )
region = None region = None
if parsed_args.region: if parsed_args.region:
val = getattr(parsed_args, 'region', None) if 'None' not in parsed_args.region:
if 'None' not in val:
# NOTE (vishakha): Due to bug #1799153 and for any another # NOTE (vishakha): Due to bug #1799153 and for any another
# related case where GET resource API does not support the # related case where GET resource API does not support the
# filter by name, osc_lib.utils.find_resource() method cannot # filter by name, osc_lib.utils.find_resource() method cannot
@ -90,6 +89,13 @@ class CreateLimit(command.ShowOne):
region = common_utils.get_resource( region = common_utils.get_resource(
identity_client.regions, parsed_args.region identity_client.regions, parsed_args.region
) )
else:
self.log.warning(
_(
"Passing 'None' to indicate no region is deprecated. "
"Instead, don't pass --region."
)
)
limit = identity_client.limits.create( limit = identity_client.limits.create(
project, project,
@ -142,11 +148,7 @@ class ListLimit(command.Lister):
) )
region = None region = None
if parsed_args.region: if parsed_args.region:
region = utils.find_resource( if 'None' not in parsed_args.region:
identity_client.regions, parsed_args.region
)
val = getattr(parsed_args, 'region', None)
if 'None' not in val:
# NOTE (vishakha): Due to bug #1799153 and for any another # NOTE (vishakha): Due to bug #1799153 and for any another
# related case where GET resource API does not support the # related case where GET resource API does not support the
# filter by name, osc_lib.utils.find_resource() method cannot # filter by name, osc_lib.utils.find_resource() method cannot
@ -158,6 +160,14 @@ class ListLimit(command.Lister):
region = common_utils.get_resource( region = common_utils.get_resource(
identity_client.regions, parsed_args.region identity_client.regions, parsed_args.region
) )
else:
self.log.warning(
_(
"Passing 'None' to indicate no region is deprecated. "
"Instead, don't pass --region."
)
)
project = None project = None
if parsed_args.project: if parsed_args.project:
project = utils.find_resource( project = utils.find_resource(
@ -266,13 +276,13 @@ class DeleteLimit(command.Command):
except Exception as e: except Exception as e:
errors += 1 errors += 1
LOG.error( LOG.error(
_("Failed to delete limit with ID " "'%(id)s': %(e)s"), _("Failed to delete limit with ID '%(id)s': %(e)s"),
{'id': limit_id, 'e': e}, {'id': limit_id, 'e': e},
) )
if errors > 0: if errors > 0:
total = len(parsed_args.limit_id) total = len(parsed_args.limit_id)
msg = _("%(errors)s of %(total)s limits failed to " "delete.") % { msg = _("%(errors)s of %(total)s limits failed to delete.") % {
'errors': errors, 'errors': errors,
'total': total, 'total': total,
} }

View file

@ -18,10 +18,10 @@
import json import json
import logging import logging
from osc_lib.command import command
from osc_lib import exceptions from osc_lib import exceptions
from osc_lib import utils from osc_lib import utils
from openstackclient import command
from openstackclient.i18n import _ from openstackclient.i18n import _
@ -161,9 +161,10 @@ class DeleteMapping(command.Command):
if result > 0: if result > 0:
total = len(parsed_args.mapping) total = len(parsed_args.mapping)
msg = _( msg = _("%(result)s of %(total)s mappings failed to delete.") % {
"%(result)s of %(total)s mappings failed " "to delete." 'result': result,
) % {'result': result, 'total': total} 'total': total,
}
raise exceptions.CommandError(msg) raise exceptions.CommandError(msg)

View file

@ -17,10 +17,10 @@
import logging import logging
from osc_lib.command import command
from osc_lib import exceptions from osc_lib import exceptions
from osc_lib import utils from osc_lib import utils
from openstackclient import command
from openstackclient.i18n import _ from openstackclient.i18n import _
@ -92,9 +92,7 @@ class DeletePolicy(command.Command):
if result > 0: if result > 0:
total = len(parsed_args.policy) total = len(parsed_args.policy)
msg = _( msg = _("%(result)s of %(total)s policies failed to delete.") % {
"%(result)s of %(total)s policies failed " "to delete."
) % {
'result': result, 'result': result,
'total': total, 'total': total,
} }
@ -115,12 +113,11 @@ class ListPolicy(command.Lister):
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
columns: tuple[str, ...] = ('ID', 'Type')
column_headers: tuple[str, ...] = columns
if parsed_args.long: if parsed_args.long:
columns = ('ID', 'Type', 'Blob') columns += ('Blob',)
column_headers = ('ID', 'Type', 'Rules') column_headers += ('Rules',)
else:
columns = ('ID', 'Type')
column_headers = columns
data = self.app.client_manager.identity.policies.list() data = self.app.client_manager.identity.policies.list()
return ( return (
column_headers, column_headers,

View file

@ -19,10 +19,10 @@ import logging
from keystoneauth1 import exceptions as ks_exc from keystoneauth1 import exceptions as ks_exc
from osc_lib.cli import parseractions from osc_lib.cli import parseractions
from osc_lib.command import command
from osc_lib import exceptions from osc_lib import exceptions
from osc_lib import utils from osc_lib import utils
from openstackclient import command
from openstackclient.i18n import _ from openstackclient.i18n import _
from openstackclient.identity import common from openstackclient.identity import common
from openstackclient.identity.v3 import tag from openstackclient.identity.v3 import tag
@ -59,16 +59,21 @@ class CreateProject(command.ShowOne):
enable_group.add_argument( enable_group.add_argument(
'--enable', '--enable',
action='store_true', action='store_true',
dest='enabled',
default=True,
help=_('Enable project'), help=_('Enable project'),
) )
enable_group.add_argument( enable_group.add_argument(
'--disable', '--disable',
action='store_true', action='store_false',
dest='enabled',
default=True,
help=_('Disable project'), help=_('Disable project'),
) )
parser.add_argument( parser.add_argument(
'--property', '--property',
metavar='<key=value>', metavar='<key=value>',
dest='properties',
action=parseractions.KeyValueAction, action=parseractions.KeyValueAction,
help=_( help=_(
'Add a property to <name> ' 'Add a property to <name> '
@ -98,15 +103,9 @@ class CreateProject(command.ShowOne):
parsed_args.parent, parsed_args.parent,
).id ).id
enabled = True
if parsed_args.disable:
enabled = False
options = common.get_immutable_options(parsed_args)
kwargs = {} kwargs = {}
if parsed_args.property: if parsed_args.properties:
kwargs = parsed_args.property.copy() kwargs = parsed_args.properties.copy()
if 'is_domain' in kwargs.keys(): if 'is_domain' in kwargs.keys():
if kwargs['is_domain'].lower() == "true": if kwargs['is_domain'].lower() == "true":
kwargs['is_domain'] = True kwargs['is_domain'] = True
@ -117,13 +116,17 @@ class CreateProject(command.ShowOne):
kwargs['tags'] = list(set(parsed_args.tags)) kwargs['tags'] = list(set(parsed_args.tags))
options = {}
if parsed_args.immutable is not None:
options['immutable'] = parsed_args.immutable
try: try:
project = identity_client.projects.create( project = identity_client.projects.create(
name=parsed_args.name, name=parsed_args.name,
domain=domain, domain=domain,
parent=parent, parent=parent,
description=parsed_args.description, description=parsed_args.description,
enabled=enabled, enabled=parsed_args.enabled,
options=options, options=options,
**kwargs, **kwargs,
) )
@ -143,7 +146,14 @@ class CreateProject(command.ShowOne):
class DeleteProject(command.Command): class DeleteProject(command.Command):
_description = _("Delete project(s)") _description = _(
"Delete project(s). This command will remove specified "
"existing project(s) if an active user is authorized to do "
"this. If there are resources managed by other services "
"(for example, Nova, Neutron, Cinder) associated with "
"specified project(s), delete operation will proceed "
"regardless."
)
def get_parser(self, prog_name): def get_parser(self, prog_name):
parser = super().get_parser(prog_name) parser = super().get_parser(prog_name)
@ -190,9 +200,10 @@ class DeleteProject(command.Command):
if errors > 0: if errors > 0:
total = len(parsed_args.projects) total = len(parsed_args.projects)
msg = _( msg = _("%(errors)s of %(total)s projects failed to delete.") % {
"%(errors)s of %(total)s projects failed " "to delete." 'errors': errors,
) % {'errors': errors, 'total': total} 'total': total,
}
raise exceptions.CommandError(msg) raise exceptions.CommandError(msg)
@ -239,15 +250,28 @@ class ListProject(command.Lister):
'keys and directions.' 'keys and directions.'
), ),
) )
parser.add_argument(
'--enabled',
action='store_true',
dest='is_enabled',
default=None,
help=_('List only enabled projects'),
)
parser.add_argument(
'--disabled',
action='store_false',
dest='is_enabled',
default=None,
help=_('List only disabled projects'),
)
tag.add_tag_filtering_option_to_parser(parser, _('projects')) tag.add_tag_filtering_option_to_parser(parser, _('projects'))
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
identity_client = self.app.client_manager.identity identity_client = self.app.client_manager.identity
columns: tuple[str, ...] = ('ID', 'Name')
if parsed_args.long: if parsed_args.long:
columns = ('ID', 'Name', 'Domain ID', 'Description', 'Enabled') columns += ('Domain ID', 'Description', 'Enabled')
else:
columns = ('ID', 'Name')
kwargs = {} kwargs = {}
domain_id = None domain_id = None
@ -277,6 +301,9 @@ class ListProject(command.Lister):
kwargs['user'] = user_id kwargs['user'] = user_id
if parsed_args.is_enabled is not None:
kwargs['is_enabled'] = parsed_args.is_enabled
tag.get_tag_filtering_args(parsed_args, kwargs) tag.get_tag_filtering_args(parsed_args, kwargs)
if parsed_args.my_projects: if parsed_args.my_projects:
@ -339,16 +366,21 @@ class SetProject(command.Command):
enable_group.add_argument( enable_group.add_argument(
'--enable', '--enable',
action='store_true', action='store_true',
dest='enabled',
default=None,
help=_('Enable project'), help=_('Enable project'),
) )
enable_group.add_argument( enable_group.add_argument(
'--disable', '--disable',
action='store_true', action='store_false',
dest='enabled',
default=None,
help=_('Disable project'), help=_('Disable project'),
) )
parser.add_argument( parser.add_argument(
'--property', '--property',
metavar='<key=value>', metavar='<key=value>',
dest='properties',
action=parseractions.KeyValueAction, action=parseractions.KeyValueAction,
help=_( help=_(
'Set a property on <project> ' 'Set a property on <project> '
@ -371,15 +403,12 @@ class SetProject(command.Command):
kwargs['name'] = parsed_args.name kwargs['name'] = parsed_args.name
if parsed_args.description: if parsed_args.description:
kwargs['description'] = parsed_args.description kwargs['description'] = parsed_args.description
if parsed_args.enable: if parsed_args.enabled is not None:
kwargs['enabled'] = True kwargs['enabled'] = parsed_args.enabled
if parsed_args.disable: if parsed_args.immutable is not None:
kwargs['enabled'] = False kwargs['options'] = {'immutable': parsed_args.immutable}
options = common.get_immutable_options(parsed_args) if parsed_args.properties:
if options: kwargs.update(parsed_args.properties)
kwargs['options'] = options
if parsed_args.property:
kwargs.update(parsed_args.property)
tag.update_tags_in_args(parsed_args, project, kwargs) tag.update_tags_in_args(parsed_args, project, kwargs)
identity_client.projects.update(project.id, **kwargs) identity_client.projects.update(project.id, **kwargs)

View file

@ -15,16 +15,25 @@
import logging import logging
from osc_lib.command import command
from osc_lib import exceptions from osc_lib import exceptions
from osc_lib import utils from osc_lib import utils
from openstackclient import command
from openstackclient.i18n import _ from openstackclient.i18n import _
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
def _format_region(region):
columns = ('id', 'description', 'parent_region_id')
column_headers = ('region', 'description', 'parent_region')
return (
column_headers,
utils.get_item_properties(region, columns),
)
class CreateRegion(command.ShowOne): class CreateRegion(command.ShowOne):
_description = _("Create new region") _description = _("Create new region")
@ -50,18 +59,15 @@ class CreateRegion(command.ShowOne):
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
identity_client = self.app.client_manager.identity identity_client = self.app.client_manager.sdk_connection.identity
region = identity_client.regions.create( region = identity_client.create_region(
id=parsed_args.region, id=parsed_args.region,
parent_region=parsed_args.parent_region, parent_region_id=parsed_args.parent_region,
description=parsed_args.description, description=parsed_args.description,
) )
region._info['region'] = region._info.pop('id') return _format_region(region)
region._info['parent_region'] = region._info.pop('parent_region_id')
region._info.pop('links', None)
return zip(*sorted(region._info.items()))
class DeleteRegion(command.Command): class DeleteRegion(command.Command):
@ -78,24 +84,21 @@ class DeleteRegion(command.Command):
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
identity_client = self.app.client_manager.identity identity_client = self.app.client_manager.sdk_connection.identity
result = 0 result = 0
for i in parsed_args.region: for i in parsed_args.region:
try: try:
identity_client.regions.delete(i) identity_client.delete_region(i)
except Exception as e: except Exception as e:
result += 1 result += 1
LOG.error( LOG.error(
_( _("Failed to delete region with ID '%(region)s': %(e)s"),
"Failed to delete region with "
"ID '%(region)s': %(e)s"
),
{'region': i, 'e': e}, {'region': i, 'e': e},
) )
if result > 0: if result > 0:
total = len(parsed_args.region) total = len(parsed_args.region)
msg = _("%(result)s of %(total)s regions failed " "to delete.") % { msg = _("%(result)s of %(total)s regions failed to delete.") % {
'result': result, 'result': result,
'total': total, 'total': total,
} }
@ -115,7 +118,7 @@ class ListRegion(command.Lister):
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
identity_client = self.app.client_manager.identity identity_client = self.app.client_manager.sdk_connection.identity
kwargs = {} kwargs = {}
if parsed_args.parent_region: if parsed_args.parent_region:
@ -124,7 +127,7 @@ class ListRegion(command.Lister):
columns_headers = ('Region', 'Parent Region', 'Description') columns_headers = ('Region', 'Parent Region', 'Description')
columns = ('ID', 'Parent Region Id', 'Description') columns = ('ID', 'Parent Region Id', 'Description')
data = identity_client.regions.list(**kwargs) data = identity_client.regions(**kwargs)
return ( return (
columns_headers, columns_headers,
( (
@ -161,15 +164,15 @@ class SetRegion(command.Command):
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
identity_client = self.app.client_manager.identity identity_client = self.app.client_manager.sdk_connection.identity
kwargs = {} kwargs = {}
if parsed_args.description: if parsed_args.description:
kwargs['description'] = parsed_args.description kwargs['description'] = parsed_args.description
if parsed_args.parent_region: if parsed_args.parent_region:
kwargs['parent_region'] = parsed_args.parent_region kwargs['parent_region_id'] = parsed_args.parent_region
identity_client.regions.update(parsed_args.region, **kwargs) identity_client.update_region(parsed_args.region, **kwargs)
class ShowRegion(command.ShowOne): class ShowRegion(command.ShowOne):
@ -185,13 +188,8 @@ class ShowRegion(command.ShowOne):
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
identity_client = self.app.client_manager.identity identity_client = self.app.client_manager.sdk_connection.identity
region = utils.find_resource( region = identity_client.get_region(parsed_args.region)
identity_client.regions, parsed_args.region
)
region._info['region'] = region._info.pop('id') return _format_region(region)
region._info['parent_region'] = region._info.pop('parent_region_id')
region._info.pop('links', None)
return zip(*sorted(region._info.items()))

View file

@ -15,10 +15,10 @@
import logging import logging
from osc_lib.command import command
from osc_lib import exceptions from osc_lib import exceptions
from osc_lib import utils from osc_lib import utils
from openstackclient import command
from openstackclient.i18n import _ from openstackclient.i18n import _
from openstackclient.identity import common as common_utils from openstackclient.identity import common as common_utils
@ -44,7 +44,10 @@ class CreateRegisteredLimit(command.ShowOne):
'--service', '--service',
metavar='<service>', metavar='<service>',
required=True, required=True,
help=_('Service responsible for the resource to limit (required)'), help=_(
'Service responsible for the resource to limit (required) '
'(name or ID)'
),
) )
parser.add_argument( parser.add_argument(
'--default-limit', '--default-limit',
@ -68,8 +71,7 @@ class CreateRegisteredLimit(command.ShowOne):
) )
region = None region = None
if parsed_args.region: if parsed_args.region:
val = getattr(parsed_args, 'region', None) if 'None' not in parsed_args.region:
if 'None' not in val:
# NOTE (vishakha): Due to bug #1799153 and for any another # NOTE (vishakha): Due to bug #1799153 and for any another
# related case where GET resource API does not support the # related case where GET resource API does not support the
# filter by name, osc_lib.utils.find_resource() method cannot # filter by name, osc_lib.utils.find_resource() method cannot
@ -81,6 +83,13 @@ class CreateRegisteredLimit(command.ShowOne):
region = common_utils.get_resource( region = common_utils.get_resource(
identity_client.regions, parsed_args.region identity_client.regions, parsed_args.region
) )
else:
self.log.warning(
_(
"Passing 'None' to indicate no region is deprecated. "
"Instead, don't pass --region."
)
)
registered_limit = identity_client.registered_limits.create( registered_limit = identity_client.registered_limits.create(
service, service,
@ -100,10 +109,10 @@ class DeleteRegisteredLimit(command.Command):
def get_parser(self, prog_name): def get_parser(self, prog_name):
parser = super().get_parser(prog_name) parser = super().get_parser(prog_name)
parser.add_argument( parser.add_argument(
'registered_limit_id', 'registered_limits',
metavar='<registered-limit-id>', metavar='<registered-limits>',
nargs="+", nargs="+",
help=_('Registered limit to delete (ID)'), help=_('Registered limit(s) to delete (ID)'),
) )
return parser return parser
@ -111,7 +120,7 @@ class DeleteRegisteredLimit(command.Command):
identity_client = self.app.client_manager.identity identity_client = self.app.client_manager.identity
errors = 0 errors = 0
for registered_limit_id in parsed_args.registered_limit_id: for registered_limit_id in parsed_args.registered_limits:
try: try:
identity_client.registered_limits.delete(registered_limit_id) identity_client.registered_limits.delete(registered_limit_id)
except Exception as e: except Exception as e:
@ -128,10 +137,9 @@ class DeleteRegisteredLimit(command.Command):
) )
if errors > 0: if errors > 0:
total = len(parsed_args.registered_limit_id) total = len(parsed_args.registered_limits)
msg = _( msg = _(
"%(errors)s of %(total)s registered limits failed to " "%(errors)s of %(total)s registered limits failed to delete."
"delete."
) % {'errors': errors, 'total': total} ) % {'errors': errors, 'total': total}
raise exceptions.CommandError(msg) raise exceptions.CommandError(msg)
@ -144,7 +152,9 @@ class ListRegisteredLimit(command.Lister):
parser.add_argument( parser.add_argument(
'--service', '--service',
metavar='<service>', metavar='<service>',
help=_('Service responsible for the resource to limit'), help=_(
'Service responsible for the resource to limit (name or ID)'
),
) )
parser.add_argument( parser.add_argument(
'--resource-name', '--resource-name',
@ -169,8 +179,7 @@ class ListRegisteredLimit(command.Lister):
) )
region = None region = None
if parsed_args.region: if parsed_args.region:
val = getattr(parsed_args, 'region', None) if 'None' not in parsed_args.region:
if 'None' not in val:
# NOTE (vishakha): Due to bug #1799153 and for any another # NOTE (vishakha): Due to bug #1799153 and for any another
# related case where GET resource API does not support the # related case where GET resource API does not support the
# filter by name, osc_lib.utils.find_resource() method cannot # filter by name, osc_lib.utils.find_resource() method cannot
@ -182,6 +191,13 @@ class ListRegisteredLimit(command.Lister):
region = common_utils.get_resource( region = common_utils.get_resource(
identity_client.regions, parsed_args.region identity_client.regions, parsed_args.region
) )
else:
self.log.warning(
_(
"Passing 'None' to indicate no region is deprecated. "
"Instead, don't pass --region."
)
)
registered_limits = identity_client.registered_limits.list( registered_limits = identity_client.registered_limits.list(
service=service, service=service,
@ -217,9 +233,9 @@ class SetRegisteredLimit(command.ShowOne):
'--service', '--service',
metavar='<service>', metavar='<service>',
help=_( help=_(
'Service to be updated responsible for the resource to ' 'Service to be updated responsible for the resource to limit '
'limit. Either --service, --resource-name or --region must ' '(name or ID). Either --service, --resource-name or --region '
'be different than existing value otherwise it will be ' 'must be different than existing value otherwise it will be '
'duplicate entry' 'duplicate entry'
), ),
) )
@ -267,8 +283,7 @@ class SetRegisteredLimit(command.ShowOne):
region = None region = None
if parsed_args.region: if parsed_args.region:
val = getattr(parsed_args, 'region', None) if 'None' not in parsed_args.region:
if 'None' not in val:
# NOTE (vishakha): Due to bug #1799153 and for any another # NOTE (vishakha): Due to bug #1799153 and for any another
# related case where GET resource API does not support the # related case where GET resource API does not support the
# filter by name, osc_lib.utils.find_resource() method cannot # filter by name, osc_lib.utils.find_resource() method cannot
@ -280,6 +295,10 @@ class SetRegisteredLimit(command.ShowOne):
region = common_utils.get_resource( region = common_utils.get_resource(
identity_client.regions, parsed_args.region identity_client.regions, parsed_args.region
) )
else:
self.log.warning(
_("Passing 'None' to indicate no region is deprecated.")
)
registered_limit = identity_client.registered_limits.update( registered_limit = identity_client.registered_limits.update(
parsed_args.registered_limit_id, parsed_args.registered_limit_id,

View file

@ -17,11 +17,11 @@
import logging import logging
from keystoneauth1 import exceptions as ks_exc from openstack import exceptions as sdk_exc
from osc_lib.command import command
from osc_lib import exceptions from osc_lib import exceptions
from osc_lib import utils from osc_lib import utils
from openstackclient import command
from openstackclient.i18n import _ from openstackclient.i18n import _
from openstackclient.identity import common from openstackclient.identity import common
@ -29,6 +29,25 @@ from openstackclient.identity import common
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
def _format_role(role):
columns = (
"id",
"name",
"domain_id",
"description",
)
column_headers = (
"id",
"name",
"domain_id",
"description",
)
return (
column_headers,
utils.get_item_properties(role, columns),
)
def _add_identity_and_resource_options_to_parser(parser): def _add_identity_and_resource_options_to_parser(parser):
system_or_domain_or_project = parser.add_mutually_exclusive_group() system_or_domain_or_project = parser.add_mutually_exclusive_group()
system_or_domain_or_project.add_argument( system_or_domain_or_project.add_argument(
@ -64,31 +83,58 @@ def _add_identity_and_resource_options_to_parser(parser):
def _process_identity_and_resource_options( def _process_identity_and_resource_options(
parsed_args, identity_client_manager, validate_actor_existence=True parsed_args, identity_client, validate_actor_existence=True
): ):
def _find_user(): def _find_user():
try: domain_id = (
return common.find_user( common._find_sdk_id(
identity_client_manager, identity_client.find_domain,
parsed_args.user, name_or_id=parsed_args.user_domain,
parsed_args.user_domain, validate_actor_existence=validate_actor_existence,
).id )
except exceptions.CommandError: if parsed_args.user_domain
if not validate_actor_existence: else None
return parsed_args.user )
raise return common._find_sdk_id(
identity_client.find_user,
name_or_id=parsed_args.user,
validate_actor_existence=validate_actor_existence,
domain_id=domain_id,
)
def _find_group(): def _find_group():
try: domain_id = (
return common.find_group( common._find_sdk_id(
identity_client_manager, identity_client.find_domain,
parsed_args.group, name_or_id=parsed_args.group_domain,
parsed_args.group_domain, validate_actor_existence=validate_actor_existence,
).id )
except exceptions.CommandError: if parsed_args.group_domain
if not validate_actor_existence: else None
return parsed_args.group )
raise return common._find_sdk_id(
identity_client.find_group,
name_or_id=parsed_args.group,
validate_actor_existence=validate_actor_existence,
domain_id=domain_id,
)
def _find_project():
domain_id = (
common._find_sdk_id(
identity_client.find_domain,
name_or_id=parsed_args.project_domain,
validate_actor_existence=validate_actor_existence,
)
if parsed_args.project_domain
else None
)
return common._find_sdk_id(
identity_client.find_project,
name_or_id=parsed_args.project,
validate_actor_existence=validate_actor_existence,
domain_id=domain_id,
)
kwargs = {} kwargs = {}
if parsed_args.user and parsed_args.system: if parsed_args.user and parsed_args.system:
@ -96,34 +142,35 @@ def _process_identity_and_resource_options(
kwargs['system'] = parsed_args.system kwargs['system'] = parsed_args.system
elif parsed_args.user and parsed_args.domain: elif parsed_args.user and parsed_args.domain:
kwargs['user'] = _find_user() kwargs['user'] = _find_user()
kwargs['domain'] = common.find_domain( kwargs['domain'] = common._find_sdk_id(
identity_client_manager, identity_client.find_domain,
parsed_args.domain, name_or_id=parsed_args.domain,
).id validate_actor_existence=validate_actor_existence,
)
elif parsed_args.user and parsed_args.project: elif parsed_args.user and parsed_args.project:
kwargs['user'] = _find_user() kwargs['user'] = _find_user()
kwargs['project'] = common.find_project( kwargs['project'] = _find_project()
identity_client_manager,
parsed_args.project,
parsed_args.project_domain,
).id
elif parsed_args.group and parsed_args.system: elif parsed_args.group and parsed_args.system:
kwargs['group'] = _find_group() kwargs['group'] = _find_group()
kwargs['system'] = parsed_args.system kwargs['system'] = parsed_args.system
elif parsed_args.group and parsed_args.domain: elif parsed_args.group and parsed_args.domain:
kwargs['group'] = _find_group() kwargs['group'] = _find_group()
kwargs['domain'] = common.find_domain( kwargs['domain'] = common._find_sdk_id(
identity_client_manager, identity_client.find_domain,
parsed_args.domain, name_or_id=parsed_args.domain,
).id validate_actor_existence=validate_actor_existence,
)
elif parsed_args.group and parsed_args.project: elif parsed_args.group and parsed_args.project:
kwargs['group'] = _find_group() kwargs['group'] = _find_group()
kwargs['project'] = common.find_project( kwargs['project'] = _find_project()
identity_client_manager, else:
parsed_args.project, msg = _(
parsed_args.project_domain, "Role not added, incorrect set of arguments "
).id "provided. See openstack --help for more details"
kwargs['os_inherit_extension_inherited'] = parsed_args.inherited )
raise exceptions.CommandError(msg)
kwargs['inherited'] = parsed_args.inherited
return kwargs return kwargs
@ -145,7 +192,7 @@ class AddRole(command.Command):
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
identity_client = self.app.client_manager.identity identity_client = self.app.client_manager.sdk_connection.identity
if ( if (
not parsed_args.user not parsed_args.user
@ -161,18 +208,71 @@ class AddRole(command.Command):
domain_id = None domain_id = None
if parsed_args.role_domain: if parsed_args.role_domain:
domain_id = common.find_domain( domain_id = common._find_sdk_id(
identity_client, parsed_args.role_domain identity_client.find_domain, name_or_id=parsed_args.role_domain
).id )
role = utils.find_resource( role = common._find_sdk_id(
identity_client.roles, parsed_args.role, domain_id=domain_id identity_client.find_role,
name_or_id=parsed_args.role,
domain_id=domain_id,
) )
kwargs = _process_identity_and_resource_options( add_kwargs = _process_identity_and_resource_options(
parsed_args, self.app.client_manager.identity parsed_args, identity_client
) )
identity_client.roles.grant(role.id, **kwargs) if add_kwargs.get("domain"):
if add_kwargs.get("user"):
identity_client.assign_domain_role_to_user(
domain=add_kwargs["domain"],
user=add_kwargs["user"],
role=role,
inherited=add_kwargs["inherited"],
)
if add_kwargs.get("group"):
identity_client.assign_domain_role_to_group(
domain=add_kwargs["domain"],
group=add_kwargs["group"],
role=role,
inherited=add_kwargs["inherited"],
)
elif add_kwargs.get("project"):
if add_kwargs.get("user"):
identity_client.assign_project_role_to_user(
project=add_kwargs["project"],
user=add_kwargs["user"],
role=role,
inherited=add_kwargs["inherited"],
)
if add_kwargs.get("group"):
identity_client.assign_project_role_to_group(
project=add_kwargs["project"],
group=add_kwargs["group"],
role=role,
inherited=add_kwargs["inherited"],
)
elif add_kwargs.get("system"):
if add_kwargs["inherited"]:
LOG.warning(
_(
"'--inherited' was given, which is not supported "
"when adding a system role. This will be an error "
"in a future release."
)
)
# TODO(0weng): This should be an error in a future release
if add_kwargs.get("user"):
identity_client.assign_system_role_to_user(
system=add_kwargs["system"],
user=add_kwargs["user"],
role=role,
)
if add_kwargs.get("group"):
identity_client.assign_system_role_to_group(
system=add_kwargs["system"],
group=add_kwargs["group"],
role=role,
)
class CreateRole(command.ShowOne): class CreateRole(command.ShowOne):
@ -204,37 +304,38 @@ class CreateRole(command.ShowOne):
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
identity_client = self.app.client_manager.identity identity_client = self.app.client_manager.sdk_connection.identity
domain_id = None create_kwargs = {}
if parsed_args.domain: if parsed_args.domain:
domain_id = common.find_domain( create_kwargs['domain_id'] = common._find_sdk_id(
identity_client, parsed_args.domain identity_client.find_domain, name_or_id=parsed_args.domain
).id
options = common.get_immutable_options(parsed_args)
try:
role = identity_client.roles.create(
name=parsed_args.name,
domain=domain_id,
description=parsed_args.description,
options=options,
) )
except ks_exc.Conflict: if parsed_args.name:
create_kwargs['name'] = parsed_args.name
if parsed_args.description:
create_kwargs['description'] = parsed_args.description
if parsed_args.immutable is not None:
create_kwargs['options'] = {"immutable": parsed_args.immutable}
try:
role = identity_client.create_role(**create_kwargs)
except sdk_exc.ConflictException:
if parsed_args.or_show: if parsed_args.or_show:
role = utils.find_resource( role = identity_client.find_role(
identity_client.roles, name_or_id=parsed_args.name,
parsed_args.name, domain_id=parsed_args.domain,
domain_id=domain_id, ignore_missing=False,
) )
LOG.info(_('Returning existing role %s'), role.name) LOG.info(_('Returning existing role %s'), role.name)
else: else:
raise raise
role._info.pop('links') return _format_role(role)
return zip(*sorted(role._info.items()))
class DeleteRole(command.Command): class DeleteRole(command.Command):
@ -245,7 +346,7 @@ class DeleteRole(command.Command):
parser.add_argument( parser.add_argument(
'roles', 'roles',
metavar='<role>', metavar='<role>',
nargs="+", nargs='+',
help=_('Role(s) to delete (name or ID)'), help=_('Role(s) to delete (name or ID)'),
) )
parser.add_argument( parser.add_argument(
@ -256,20 +357,22 @@ class DeleteRole(command.Command):
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
identity_client = self.app.client_manager.identity identity_client = self.app.client_manager.sdk_connection.identity
domain_id = None domain_id = None
if parsed_args.domain: if parsed_args.domain:
domain_id = common.find_domain( domain_id = common._find_sdk_id(
identity_client, parsed_args.domain identity_client.find_domain, parsed_args.domain
).id )
errors = 0 errors = 0
for role in parsed_args.roles: for role in parsed_args.roles:
try: try:
role_obj = utils.find_resource( role_id = common._find_sdk_id(
identity_client.roles, role, domain_id=domain_id identity_client.find_role,
name_or_id=role,
domain_id=domain_id,
) )
identity_client.roles.delete(role_obj.id) identity_client.delete_role(role=role_id, ignore_missing=False)
except Exception as e: except Exception as e:
errors += 1 errors += 1
LOG.error( LOG.error(
@ -282,7 +385,7 @@ class DeleteRole(command.Command):
if errors > 0: if errors > 0:
total = len(parsed_args.roles) total = len(parsed_args.roles)
msg = _("%(errors)s of %(total)s roles failed " "to delete.") % { msg = _("%(errors)s of %(total)s roles failed to delete.") % {
'errors': errors, 'errors': errors,
'total': total, 'total': total,
} }
@ -302,37 +405,34 @@ class ListRole(command.Lister):
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
identity_client = self.app.client_manager.identity identity_client = self.app.client_manager.sdk_connection.identity
if parsed_args.domain: if parsed_args.domain:
domain = common.find_domain( domain = identity_client.find_domain(
identity_client, name_or_id=parsed_args.domain,
parsed_args.domain, ignore_missing=False,
) )
columns = ('ID', 'Name', 'Domain') data = identity_client.roles(domain_id=domain.id)
data = identity_client.roles.list(domain_id=domain.id)
for role in data:
role.domain = domain.name
else:
columns = ('ID', 'Name')
data = identity_client.roles.list()
return ( return (
columns, ('ID', 'Name', 'Domain'),
( (
utils.get_item_properties( utils.get_item_properties(s, ('id', 'name'))
s, + (domain.name,)
columns,
formatters={},
)
for s in data for s in data
), ),
) )
else:
data = identity_client.roles()
return (
('ID', 'Name'),
(utils.get_item_properties(s, ('id', 'name')) for s in data),
)
class RemoveRole(command.Command): class RemoveRole(command.Command):
_description = _( _description = _(
"Removes a role assignment from system/domain/project : " "user/group" "Removes a role assignment from system/domain/project : user/group"
) )
def get_parser(self, prog_name): def get_parser(self, prog_name):
@ -348,8 +448,7 @@ class RemoveRole(command.Command):
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
identity_client = self.app.client_manager.identity identity_client = self.app.client_manager.sdk_connection.identity
if ( if (
not parsed_args.user not parsed_args.user
and not parsed_args.domain and not parsed_args.domain
@ -364,19 +463,65 @@ class RemoveRole(command.Command):
domain_id = None domain_id = None
if parsed_args.role_domain: if parsed_args.role_domain:
domain_id = common.find_domain( domain_id = common._find_sdk_id(
identity_client, parsed_args.role_domain identity_client.find_domain,
).id name_or_id=parsed_args.role_domain,
role = utils.find_resource( )
identity_client.roles, parsed_args.role, domain_id=domain_id role = common._find_sdk_id(
identity_client.find_role,
name_or_id=parsed_args.role,
domain_id=domain_id,
) )
kwargs = _process_identity_and_resource_options( remove_kwargs = _process_identity_and_resource_options(
parsed_args, parsed_args,
self.app.client_manager.identity, identity_client,
validate_actor_existence=False, validate_actor_existence=False,
) )
identity_client.roles.revoke(role.id, **kwargs)
if remove_kwargs.get("domain"):
if remove_kwargs.get("user"):
identity_client.unassign_domain_role_from_user(
domain=remove_kwargs["domain"],
user=remove_kwargs["user"],
role=role,
inherited=remove_kwargs["inherited"],
)
if remove_kwargs.get("group"):
identity_client.unassign_domain_role_from_group(
domain=remove_kwargs["domain"],
group=remove_kwargs["group"],
role=role,
inherited=remove_kwargs["inherited"],
)
elif remove_kwargs.get("project"):
if remove_kwargs.get("user"):
identity_client.unassign_project_role_from_user(
project=remove_kwargs["project"],
user=remove_kwargs["user"],
role=role,
inherited=remove_kwargs["inherited"],
)
if remove_kwargs.get("group"):
identity_client.unassign_project_role_from_group(
project=remove_kwargs["project"],
group=remove_kwargs["group"],
role=role,
inherited=remove_kwargs["inherited"],
)
elif remove_kwargs.get("system"):
if remove_kwargs.get("user"):
identity_client.unassign_system_role_from_user(
system=remove_kwargs["system"],
user=remove_kwargs["user"],
role=role,
)
if remove_kwargs.get("group"):
identity_client.unassign_system_role_from_group(
system=remove_kwargs["system"],
group=remove_kwargs["group"],
role=role,
)
class SetRole(command.Command): class SetRole(command.Command):
@ -408,25 +553,33 @@ class SetRole(command.Command):
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
identity_client = self.app.client_manager.identity identity_client = self.app.client_manager.sdk_connection.identity
update_kwargs = {}
if parsed_args.description:
update_kwargs["description"] = parsed_args.description
if parsed_args.name:
update_kwargs["name"] = parsed_args.name
domain_id = None domain_id = None
if parsed_args.domain: if parsed_args.domain:
domain_id = common.find_domain( domain_id = common._find_sdk_id(
identity_client, parsed_args.domain identity_client.find_domain,
).id name_or_id=parsed_args.domain,
options = common.get_immutable_options(parsed_args)
role = utils.find_resource(
identity_client.roles, parsed_args.role, domain_id=domain_id
) )
update_kwargs["domain_id"] = domain_id
identity_client.roles.update( if parsed_args.immutable is not None:
role.id, update_kwargs["options"] = {"immutable": parsed_args.immutable}
name=parsed_args.name,
description=parsed_args.description, role = common._find_sdk_id(
options=options, identity_client.find_role,
name_or_id=parsed_args.role,
domain_id=domain_id,
) )
update_kwargs["role"] = role
identity_client.update_role(**update_kwargs)
class ShowRole(command.ShowOne): class ShowRole(command.ShowOne):
@ -447,17 +600,19 @@ class ShowRole(command.ShowOne):
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
identity_client = self.app.client_manager.identity identity_client = self.app.client_manager.sdk_connection.identity
domain_id = None domain_id = None
if parsed_args.domain: if parsed_args.domain:
domain_id = common.find_domain( domain_id = common._find_sdk_id(
identity_client, parsed_args.domain identity_client.find_domain,
).id name_or_id=parsed_args.domain,
role = utils.find_resource(
identity_client.roles, parsed_args.role, domain_id=domain_id
) )
role._info.pop('links') role = identity_client.find_role(
return zip(*sorted(role._info.items())) name_or_id=parsed_args.role,
domain_id=domain_id,
ignore_missing=False,
)
return _format_role(role)

View file

@ -13,9 +13,7 @@
"""Identity v3 Assignment action implementations""" """Identity v3 Assignment action implementations"""
from openstack import exceptions as sdk_exceptions from openstackclient import command
from osc_lib.command import command
from openstackclient.i18n import _ from openstackclient.i18n import _
from openstackclient.identity import common from openstackclient.identity import common
@ -51,15 +49,6 @@ def _format_role_assignment_(assignment, include_names):
) )
def _find_sdk_id(find_command, name_or_id, **kwargs):
try:
return find_command(
name_or_id=name_or_id, ignore_missing=False, **kwargs
).id
except sdk_exceptions.ForbiddenException:
return name_or_id
class ListRoleAssignment(command.Lister): class ListRoleAssignment(command.Lister):
_description = _("List role assignments") _description = _("List role assignments")
@ -135,12 +124,12 @@ class ListRoleAssignment(command.Lister):
role_id = None role_id = None
role_domain_id = None role_domain_id = None
if parsed_args.role_domain: if parsed_args.role_domain:
role_domain_id = _find_sdk_id( role_domain_id = common._find_sdk_id(
identity_client.find_domain, identity_client.find_domain,
name_or_id=parsed_args.role_domain, name_or_id=parsed_args.role_domain,
) )
if parsed_args.role: if parsed_args.role:
role_id = _find_sdk_id( role_id = common._find_sdk_id(
identity_client.find_role, identity_client.find_role,
name_or_id=parsed_args.role, name_or_id=parsed_args.role,
domain_id=role_domain_id, domain_id=role_domain_id,
@ -148,21 +137,21 @@ class ListRoleAssignment(command.Lister):
user_domain_id = None user_domain_id = None
if parsed_args.user_domain: if parsed_args.user_domain:
project_domain_id = _find_sdk_id( user_domain_id = common._find_sdk_id(
identity_client.find_domain, identity_client.find_domain,
name_or_id=parsed_args.user_domain, name_or_id=parsed_args.user_domain,
) )
user_id = None user_id = None
if parsed_args.user: if parsed_args.user:
user_id = _find_sdk_id( user_id = common._find_sdk_id(
identity_client.find_user, identity_client.find_user,
name_or_id=parsed_args.user, name_or_id=parsed_args.user,
domain_id=user_domain_id, domain_id=user_domain_id,
) )
elif parsed_args.authuser: elif parsed_args.authuser:
if auth_ref: if auth_ref:
user_id = _find_sdk_id( user_id = common._find_sdk_id(
identity_client.find_user, identity_client.find_user,
name_or_id=auth_ref.user_id, name_or_id=auth_ref.user_id,
) )
@ -173,21 +162,21 @@ class ListRoleAssignment(command.Lister):
domain_id = None domain_id = None
if parsed_args.domain: if parsed_args.domain:
domain_id = _find_sdk_id( domain_id = common._find_sdk_id(
identity_client.find_domain, identity_client.find_domain,
name_or_id=parsed_args.domain, name_or_id=parsed_args.domain,
) )
project_domain_id = None project_domain_id = None
if parsed_args.project_domain: if parsed_args.project_domain:
project_domain_id = _find_sdk_id( project_domain_id = common._find_sdk_id(
identity_client.find_domain, identity_client.find_domain,
name_or_id=parsed_args.project_domain, name_or_id=parsed_args.project_domain,
) )
project_id = None project_id = None
if parsed_args.project: if parsed_args.project:
project_id = _find_sdk_id( project_id = common._find_sdk_id(
identity_client.find_project, identity_client.find_project,
name_or_id=common._get_token_resource( name_or_id=common._get_token_resource(
identity_client, 'project', parsed_args.project identity_client, 'project', parsed_args.project
@ -196,21 +185,21 @@ class ListRoleAssignment(command.Lister):
) )
elif parsed_args.authproject: elif parsed_args.authproject:
if auth_ref: if auth_ref:
project_id = _find_sdk_id( project_id = common._find_sdk_id(
identity_client.find_project, identity_client.find_project,
name_or_id=auth_ref.project_id, name_or_id=auth_ref.project_id,
) )
group_domain_id = None group_domain_id = None
if parsed_args.group_domain: if parsed_args.group_domain:
group_domain_id = _find_sdk_id( group_domain_id = common._find_sdk_id(
identity_client.find_domain, identity_client.find_domain,
name_or_id=parsed_args.group_domain, name_or_id=parsed_args.group_domain,
) )
group_id = None group_id = None
if parsed_args.group: if parsed_args.group:
group_id = _find_sdk_id( group_id = common._find_sdk_id(
identity_client.find_group, identity_client.find_group,
name_or_id=parsed_args.group, name_or_id=parsed_args.group,
domain_id=group_domain_id, domain_id=group_domain_id,

View file

@ -17,10 +17,10 @@
import logging import logging
from osc_lib.command import command
from osc_lib import exceptions from osc_lib import exceptions
from osc_lib import utils from osc_lib import utils
from openstackclient import command
from openstackclient.i18n import _ from openstackclient.i18n import _
from openstackclient.identity import common from openstackclient.identity import common
@ -135,9 +135,10 @@ class DeleteService(command.Command):
if result > 0: if result > 0:
total = len(parsed_args.service) total = len(parsed_args.service)
msg = _( msg = _("%(result)s of %(total)s services failed to delete.") % {
"%(result)s of %(total)s services failed " "to delete." 'result': result,
) % {'result': result, 'total': total} 'total': total,
}
raise exceptions.CommandError(msg) raise exceptions.CommandError(msg)
@ -157,12 +158,11 @@ class ListService(command.Lister):
def take_action(self, parsed_args): def take_action(self, parsed_args):
identity_client = self.app.client_manager.sdk_connection.identity identity_client = self.app.client_manager.sdk_connection.identity
columns: tuple[str, ...] = ('id', 'name', 'type')
column_headers: tuple[str, ...] = ('ID', 'Name', 'Type')
if parsed_args.long: if parsed_args.long:
columns = ('id', 'name', 'type', 'description', 'is_enabled') columns += ('description', 'is_enabled')
column_headers = ('ID', 'Name', 'Type', 'Description', 'Enabled') column_headers += ('Description', 'Enabled')
else:
columns = ('id', 'name', 'type')
column_headers = ('ID', 'Name', 'Type')
data = identity_client.services() data = identity_client.services()

View file

@ -15,16 +15,39 @@
import logging import logging
from osc_lib.command import command
from osc_lib import exceptions from osc_lib import exceptions
from osc_lib import utils from osc_lib import utils
from openstackclient import command
from openstackclient.i18n import _ from openstackclient.i18n import _
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
def _format_service_provider(sp):
column_headers = (
'id',
'enabled',
'description',
'auth_url',
'sp_url',
'relay_state_prefix',
)
columns = (
'id',
'is_enabled',
'description',
'auth_url',
'sp_url',
'relay_state_prefix',
)
return (
column_headers,
utils.get_item_properties(sp, columns),
)
class CreateServiceProvider(command.ShowOne): class CreateServiceProvider(command.ShowOne):
_description = _("Create new service provider") _description = _("Create new service provider")
@ -54,22 +77,21 @@ class CreateServiceProvider(command.ShowOne):
metavar='<sp-url>', metavar='<sp-url>',
required=True, required=True,
help=_( help=_(
'A service URL where SAML assertions are being sent ' 'A service URL where SAML assertions are being sent (required)'
'(required)'
), ),
) )
enable_service_provider = parser.add_mutually_exclusive_group() enable_service_provider = parser.add_mutually_exclusive_group()
enable_service_provider.add_argument( enable_service_provider.add_argument(
'--enable', '--enable',
dest='enabled', dest='is_enabled',
action='store_true', action='store_true',
default=True, default=True,
help=_('Enable the service provider (default)'), help=_('Enable the service provider (default)'),
) )
enable_service_provider.add_argument( enable_service_provider.add_argument(
'--disable', '--disable',
dest='enabled', dest='is_enabled',
action='store_false', action='store_false',
help=_('Disable the service provider'), help=_('Disable the service provider'),
) )
@ -77,17 +99,27 @@ class CreateServiceProvider(command.ShowOne):
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
service_client = self.app.client_manager.identity service_client = self.app.client_manager.sdk_connection.identity
sp = service_client.federation.service_providers.create(
id=parsed_args.service_provider_id,
auth_url=parsed_args.auth_url,
description=parsed_args.description,
enabled=parsed_args.enabled,
sp_url=parsed_args.service_provider_url,
)
sp._info.pop('links', None) kwargs = {}
return zip(*sorted(sp._info.items()))
kwargs = {'id': parsed_args.service_provider_id}
if parsed_args.is_enabled is not None:
kwargs['is_enabled'] = parsed_args.is_enabled
if parsed_args.description:
kwargs['description'] = parsed_args.description
if parsed_args.auth_url:
kwargs['auth_url'] = parsed_args.auth_url
if parsed_args.service_provider_url:
kwargs['sp_url'] = parsed_args.service_provider_url
sp = service_client.create_service_provider(**kwargs)
return _format_service_provider(sp)
class DeleteServiceProvider(command.Command): class DeleteServiceProvider(command.Command):
@ -104,11 +136,11 @@ class DeleteServiceProvider(command.Command):
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
service_client = self.app.client_manager.identity service_client = self.app.client_manager.sdk_connection.identity
result = 0 result = 0
for i in parsed_args.service_provider: for i in parsed_args.service_provider:
try: try:
service_client.federation.service_providers.delete(i) service_client.delete_service_provider(i)
except Exception as e: except Exception as e:
result += 1 result += 1
LOG.error( LOG.error(
@ -122,8 +154,7 @@ class DeleteServiceProvider(command.Command):
if result > 0: if result > 0:
total = len(parsed_args.service_provider) total = len(parsed_args.service_provider)
msg = _( msg = _(
"%(result)s of %(total)s service providers failed" "%(result)s of %(total)s service providers failed to delete."
" to delete."
) % {'result': result, 'total': total} ) % {'result': result, 'total': total}
raise exceptions.CommandError(msg) raise exceptions.CommandError(msg)
@ -132,24 +163,32 @@ class ListServiceProvider(command.Lister):
_description = _("List service providers") _description = _("List service providers")
def take_action(self, parsed_args): def take_action(self, parsed_args):
service_client = self.app.client_manager.identity service_client = self.app.client_manager.sdk_connection.identity
data = service_client.federation.service_providers.list() data = service_client.service_providers()
column_headers = ('ID', 'Enabled', 'Description', 'Auth URL') column_headers = (
'ID',
'Enabled',
'Description',
'Auth URL',
'Service Provider URL',
'Relay State Prefix',
)
columns = (
'id',
'is_enabled',
'description',
'auth_url',
'sp_url',
'relay_state_prefix',
)
return ( return (
column_headers, column_headers,
( (utils.get_item_properties(s, columns) for s in data),
utils.get_item_properties(
s,
column_headers,
formatters={},
)
for s in data
),
) )
class SetServiceProvider(command.Command): class SetServiceProvider(command.ShowOne):
_description = _("Set service provider properties") _description = _("Set service provider properties")
def get_parser(self, prog_name): def get_parser(self, prog_name):
@ -163,8 +202,7 @@ class SetServiceProvider(command.Command):
'--auth-url', '--auth-url',
metavar='<auth-url>', metavar='<auth-url>',
help=_( help=_(
'New Authentication URL of remote ' 'New Authentication URL of remote federated service provider'
'federated service provider'
), ),
) )
@ -181,33 +219,44 @@ class SetServiceProvider(command.Command):
enable_service_provider = parser.add_mutually_exclusive_group() enable_service_provider = parser.add_mutually_exclusive_group()
enable_service_provider.add_argument( enable_service_provider.add_argument(
'--enable', '--enable',
dest='is_enabled',
action='store_true', action='store_true',
default=None,
help=_('Enable the service provider'), help=_('Enable the service provider'),
) )
enable_service_provider.add_argument( enable_service_provider.add_argument(
'--disable', '--disable',
action='store_true', dest='is_enabled',
action='store_false',
default=None,
help=_('Disable the service provider'), help=_('Disable the service provider'),
) )
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
federation_client = self.app.client_manager.identity.federation service_client = self.app.client_manager.sdk_connection.identity
enabled = None kwargs = {}
if parsed_args.enable is True:
enabled = True
elif parsed_args.disable is True:
enabled = False
federation_client.service_providers.update( if parsed_args.is_enabled is not None:
kwargs['is_enabled'] = parsed_args.is_enabled
if parsed_args.description:
kwargs['description'] = parsed_args.description
if parsed_args.auth_url:
kwargs['auth_url'] = parsed_args.auth_url
if parsed_args.service_provider_url:
kwargs['sp_url'] = parsed_args.service_provider_url
service_provider = service_client.update_service_provider(
parsed_args.service_provider, parsed_args.service_provider,
enabled=enabled, **kwargs,
description=parsed_args.description,
auth_url=parsed_args.auth_url,
sp_url=parsed_args.service_provider_url,
) )
return _format_service_provider(service_provider)
class ShowServiceProvider(command.ShowOne): class ShowServiceProvider(command.ShowOne):
_description = _("Display service provider details") _description = _("Display service provider details")
@ -222,12 +271,10 @@ class ShowServiceProvider(command.ShowOne):
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
service_client = self.app.client_manager.identity service_client = self.app.client_manager.sdk_connection.identity
service_provider = utils.find_resource( service_provider = service_client.find_service_provider(
service_client.federation.service_providers,
parsed_args.service_provider, parsed_args.service_provider,
id=parsed_args.service_provider, ignore_missing=False,
) )
service_provider._info.pop('links', None) return _format_service_provider(service_provider)
return zip(*sorted(service_provider._info.items()))

View file

@ -83,7 +83,7 @@ def add_tag_option_to_parser_for_create(parser, resource_name):
metavar='<tag>', metavar='<tag>',
default=[], default=[],
help=_( help=_(
'Tag to be added to the %s ' '(repeat option to set multiple tags)' 'Tag to be added to the %s (repeat option to set multiple tags)'
) )
% resource_name, % resource_name,
) )
@ -97,7 +97,7 @@ def add_tag_option_to_parser_for_set(parser, resource_name):
metavar='<tag>', metavar='<tag>',
default=[], default=[],
help=_( help=_(
'Tag to be added to the %s ' '(repeat option to set multiple tags)' 'Tag to be added to the %s (repeat option to set multiple tags)'
) )
% resource_name, % resource_name,
) )
@ -114,6 +114,7 @@ def add_tag_option_to_parser_for_set(parser, resource_name):
parser.add_argument( parser.add_argument(
'--remove-tag', '--remove-tag',
action='append', action='append',
dest='remove_tags',
metavar='<tag>', metavar='<tag>',
default=[], default=[],
help=_( help=_(
@ -128,8 +129,8 @@ def update_tags_in_args(parsed_args, obj, args):
if parsed_args.clear_tags: if parsed_args.clear_tags:
args['tags'] = [] args['tags'] = []
obj.tags = [] obj.tags = []
if parsed_args.remove_tag: if parsed_args.remove_tags:
args['tags'] = sorted(set(obj.tags) - set(parsed_args.remove_tag)) args['tags'] = sorted(set(obj.tags) - set(parsed_args.remove_tags))
return return
if parsed_args.tags: if parsed_args.tags:
args['tags'] = sorted(set(obj.tags).union(set(parsed_args.tags))) args['tags'] = sorted(set(obj.tags).union(set(parsed_args.tags)))

Some files were not shown because too many files have changed in this diff Show more