python-openstackclient 8.0.0__py3-none-any.whl → 8.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. openstackclient/api/compute_v2.py +2 -2
  2. openstackclient/api/volume_v2.py +60 -0
  3. openstackclient/api/volume_v3.py +60 -0
  4. openstackclient/compute/client.py +5 -0
  5. openstackclient/compute/v2/console.py +7 -0
  6. openstackclient/compute/v2/console_connection.py +48 -0
  7. openstackclient/compute/v2/flavor.py +14 -1
  8. openstackclient/compute/v2/keypair.py +10 -3
  9. openstackclient/compute/v2/server.py +76 -13
  10. openstackclient/compute/v2/server_event.py +1 -1
  11. openstackclient/identity/common.py +85 -11
  12. openstackclient/identity/v3/application_credential.py +88 -87
  13. openstackclient/identity/v3/domain.py +67 -49
  14. openstackclient/identity/v3/group.py +113 -68
  15. openstackclient/identity/v3/project.py +42 -20
  16. openstackclient/identity/v3/role.py +7 -2
  17. openstackclient/identity/v3/user.py +38 -5
  18. openstackclient/image/client.py +5 -0
  19. openstackclient/image/v1/image.py +16 -1
  20. openstackclient/image/v2/cache.py +10 -6
  21. openstackclient/image/v2/image.py +59 -12
  22. openstackclient/image/v2/metadef_objects.py +8 -2
  23. openstackclient/image/v2/metadef_properties.py +9 -2
  24. openstackclient/network/client.py +0 -6
  25. openstackclient/network/v2/floating_ip.py +58 -29
  26. openstackclient/network/v2/network_qos_rule.py +3 -11
  27. openstackclient/network/v2/port.py +16 -0
  28. openstackclient/network/v2/router.py +1 -1
  29. openstackclient/network/v2/security_group.py +49 -7
  30. openstackclient/network/v2/security_group_rule.py +18 -1
  31. openstackclient/shell.py +1 -1
  32. openstackclient/tests/functional/base.py +5 -1
  33. openstackclient/tests/functional/compute/v2/test_keypair.py +41 -5
  34. openstackclient/tests/functional/identity/v3/test_access_rule.py +1 -1
  35. openstackclient/tests/functional/identity/v3/test_application_credential.py +7 -7
  36. openstackclient/tests/functional/image/v2/test_image.py +36 -14
  37. openstackclient/tests/functional/volume/v2/test_volume.py +1 -1
  38. openstackclient/tests/functional/volume/v3/test_volume.py +2 -2
  39. openstackclient/tests/unit/api/test_volume_v2.py +124 -0
  40. openstackclient/tests/unit/api/test_volume_v3.py +124 -0
  41. openstackclient/tests/unit/compute/v2/fakes.py +81 -305
  42. openstackclient/tests/unit/compute/v2/test_console.py +18 -1
  43. openstackclient/tests/unit/compute/v2/test_console_connection.py +72 -0
  44. openstackclient/tests/unit/compute/v2/test_flavor.py +160 -175
  45. openstackclient/tests/unit/compute/v2/test_keypair.py +12 -5
  46. openstackclient/tests/unit/compute/v2/test_server.py +211 -97
  47. openstackclient/tests/unit/compute/v2/test_server_backup.py +32 -71
  48. openstackclient/tests/unit/compute/v2/test_server_event.py +2 -2
  49. openstackclient/tests/unit/compute/v2/test_server_image.py +33 -72
  50. openstackclient/tests/unit/compute/v2/test_server_migration.py +4 -4
  51. openstackclient/tests/unit/identity/v3/test_application_credential.py +93 -65
  52. openstackclient/tests/unit/identity/v3/test_domain.py +117 -107
  53. openstackclient/tests/unit/identity/v3/test_group.py +353 -202
  54. openstackclient/tests/unit/identity/v3/test_project.py +46 -53
  55. openstackclient/tests/unit/identity/v3/test_role.py +2 -8
  56. openstackclient/tests/unit/identity/v3/test_user.py +86 -6
  57. openstackclient/tests/unit/image/v1/test_image.py +55 -9
  58. openstackclient/tests/unit/image/v2/test_image.py +128 -58
  59. openstackclient/tests/unit/image/v2/test_metadef_objects.py +22 -0
  60. openstackclient/tests/unit/image/v2/test_metadef_properties.py +24 -10
  61. openstackclient/tests/unit/network/v2/fakes.py +406 -485
  62. openstackclient/tests/unit/network/v2/test_floating_ip_network.py +13 -19
  63. openstackclient/tests/unit/network/v2/test_l3_conntrack_helper.py +2 -2
  64. openstackclient/tests/unit/network/v2/test_ndp_proxy.py +3 -5
  65. openstackclient/tests/unit/network/v2/test_network.py +4 -4
  66. openstackclient/tests/unit/network/v2/test_network_agent.py +15 -29
  67. openstackclient/tests/unit/network/v2/test_network_qos_policy.py +16 -19
  68. openstackclient/tests/unit/network/v2/test_network_qos_rule.py +79 -152
  69. openstackclient/tests/unit/network/v2/test_network_qos_rule_type.py +4 -6
  70. openstackclient/tests/unit/network/v2/test_network_rbac.py +2 -2
  71. openstackclient/tests/unit/network/v2/test_port.py +57 -17
  72. openstackclient/tests/unit/network/v2/test_router.py +73 -57
  73. openstackclient/tests/unit/network/v2/test_security_group_network.py +31 -27
  74. openstackclient/tests/unit/network/v2/test_security_group_rule_compute.py +1 -3
  75. openstackclient/tests/unit/network/v2/test_security_group_rule_network.py +82 -39
  76. openstackclient/tests/unit/volume/v2/fakes.py +1 -2
  77. openstackclient/tests/unit/volume/v2/test_service.py +57 -91
  78. openstackclient/tests/unit/volume/v2/test_volume.py +466 -410
  79. openstackclient/tests/unit/volume/v2/test_volume_backup.py +141 -148
  80. openstackclient/tests/unit/volume/v2/test_volume_snapshot.py +293 -283
  81. openstackclient/tests/unit/volume/v3/test_block_storage_log_level.py +61 -71
  82. openstackclient/tests/unit/volume/v3/test_service.py +221 -141
  83. openstackclient/tests/unit/volume/v3/test_volume.py +569 -534
  84. openstackclient/tests/unit/volume/v3/test_volume_attachment.py +1 -1
  85. openstackclient/tests/unit/volume/v3/test_volume_backup.py +198 -203
  86. openstackclient/tests/unit/volume/v3/test_volume_snapshot.py +682 -47
  87. openstackclient/volume/v2/service.py +41 -38
  88. openstackclient/volume/v2/volume.py +140 -88
  89. openstackclient/volume/v2/volume_backup.py +9 -3
  90. openstackclient/volume/v2/volume_snapshot.py +121 -84
  91. openstackclient/volume/v3/block_storage_log_level.py +22 -28
  92. openstackclient/volume/v3/service.py +105 -14
  93. openstackclient/volume/v3/volume.py +287 -99
  94. openstackclient/volume/v3/volume_backup.py +24 -19
  95. openstackclient/volume/v3/volume_group.py +1 -1
  96. openstackclient/volume/v3/volume_snapshot.py +485 -10
  97. {python_openstackclient-8.0.0.dist-info → python_openstackclient-8.2.0.dist-info}/AUTHORS +13 -0
  98. python_openstackclient-8.2.0.dist-info/METADATA +264 -0
  99. {python_openstackclient-8.0.0.dist-info → python_openstackclient-8.2.0.dist-info}/RECORD +104 -98
  100. {python_openstackclient-8.0.0.dist-info → python_openstackclient-8.2.0.dist-info}/entry_points.txt +7 -6
  101. python_openstackclient-8.2.0.dist-info/pbr.json +1 -0
  102. python_openstackclient-8.0.0.dist-info/METADATA +0 -166
  103. python_openstackclient-8.0.0.dist-info/pbr.json +0 -1
  104. {python_openstackclient-8.0.0.dist-info → python_openstackclient-8.2.0.dist-info}/LICENSE +0 -0
  105. {python_openstackclient-8.0.0.dist-info → python_openstackclient-8.2.0.dist-info}/WHEEL +0 -0
  106. {python_openstackclient-8.0.0.dist-info → python_openstackclient-8.2.0.dist-info}/top_level.txt +0 -0
@@ -64,7 +64,7 @@ def list_security_groups(compute_client, all_projects=None):
64
64
 
65
65
 
66
66
  def find_security_group(compute_client, name_or_id):
67
- """Find the name for a given security group name or ID
67
+ """Find the security group for a given name or ID
68
68
 
69
69
  https://docs.openstack.org/api-ref/compute/#show-security-group-details
70
70
 
@@ -240,7 +240,7 @@ def list_networks(compute_client):
240
240
 
241
241
 
242
242
  def find_network(compute_client, name_or_id):
243
- """Find the ID for a given network name or ID
243
+ """Find the network for a given name or ID
244
244
 
245
245
  https://docs.openstack.org/api-ref/compute/#show-network-details
246
246
 
@@ -0,0 +1,60 @@
1
+ # Licensed under the Apache License, Version 2.0 (the "License"); you may
2
+ # not use this file except in compliance with the License. You may obtain
3
+ # a copy of the License at
4
+ #
5
+ # http://www.apache.org/licenses/LICENSE-2.0
6
+ #
7
+ # Unless required by applicable law or agreed to in writing, software
8
+ # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9
+ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10
+ # License for the specific language governing permissions and limitations
11
+ # under the License.
12
+
13
+ """Volume v2 API Library
14
+
15
+ A collection of wrappers for deprecated Block Storage v2 APIs that are not
16
+ intentionally supported by SDK.
17
+ """
18
+
19
+ import http
20
+
21
+ from openstack import exceptions as sdk_exceptions
22
+ from osc_lib import exceptions
23
+
24
+
25
+ # consistency groups
26
+
27
+
28
+ def find_consistency_group(compute_client, name_or_id):
29
+ """Find the consistency group for a given name or ID
30
+
31
+ https://docs.openstack.org/api-ref/block-storage/v3/#show-a-consistency-group-s-details
32
+
33
+ :param volume_client: A volume client
34
+ :param name_or_id: The name or ID of the consistency group to look up
35
+ :returns: A consistency group object
36
+ :raises exception.NotFound: If a matching consistency group could not be
37
+ found or more than one match was found
38
+ """
39
+ response = compute_client.get(f'/consistencygroups/{name_or_id}')
40
+ if response.status_code != http.HTTPStatus.NOT_FOUND:
41
+ # there might be other, non-404 errors
42
+ sdk_exceptions.raise_from_response(response)
43
+ return response.json()['consistencygroup']
44
+
45
+ response = compute_client.get('/consistencygroups')
46
+ sdk_exceptions.raise_from_response(response)
47
+ found = None
48
+ consistency_groups = response.json()['consistencygroups']
49
+ for consistency_group in consistency_groups:
50
+ if consistency_group['name'] == name_or_id:
51
+ if found:
52
+ raise exceptions.NotFound(
53
+ f'multiple matches found for {name_or_id}'
54
+ )
55
+ found = consistency_group
56
+
57
+ if not found:
58
+ raise exceptions.NotFound(f'{name_or_id} not found')
59
+
60
+ return found
@@ -0,0 +1,60 @@
1
+ # Licensed under the Apache License, Version 2.0 (the "License"); you may
2
+ # not use this file except in compliance with the License. You may obtain
3
+ # a copy of the License at
4
+ #
5
+ # http://www.apache.org/licenses/LICENSE-2.0
6
+ #
7
+ # Unless required by applicable law or agreed to in writing, software
8
+ # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9
+ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10
+ # License for the specific language governing permissions and limitations
11
+ # under the License.
12
+
13
+ """Volume v3 API Library
14
+
15
+ A collection of wrappers for deprecated Block Storage v3 APIs that are not
16
+ intentionally supported by SDK.
17
+ """
18
+
19
+ import http
20
+
21
+ from openstack import exceptions as sdk_exceptions
22
+ from osc_lib import exceptions
23
+
24
+
25
+ # consistency groups
26
+
27
+
28
+ def find_consistency_group(compute_client, name_or_id):
29
+ """Find the consistency group for a given name or ID
30
+
31
+ https://docs.openstack.org/api-ref/block-storage/v3/#show-a-consistency-group-s-details
32
+
33
+ :param volume_client: A volume client
34
+ :param name_or_id: The name or ID of the consistency group to look up
35
+ :returns: A consistency group object
36
+ :raises exception.NotFound: If a matching consistency group could not be
37
+ found or more than one match was found
38
+ """
39
+ response = compute_client.get(f'/consistencygroups/{name_or_id}')
40
+ if response.status_code != http.HTTPStatus.NOT_FOUND:
41
+ # there might be other, non-404 errors
42
+ sdk_exceptions.raise_from_response(response)
43
+ return response.json()['consistencygroup']
44
+
45
+ response = compute_client.get('/consistencygroups')
46
+ sdk_exceptions.raise_from_response(response)
47
+ found = None
48
+ consistency_groups = response.json()['consistencygroups']
49
+ for consistency_group in consistency_groups:
50
+ if consistency_group['name'] == name_or_id:
51
+ if found:
52
+ raise exceptions.NotFound(
53
+ f'multiple matches found for {name_or_id}'
54
+ )
55
+ found = consistency_group
56
+
57
+ if not found:
58
+ raise exceptions.NotFound(f'{name_or_id} not found')
59
+
60
+ return found
@@ -46,3 +46,8 @@ def build_option_parser(parser):
46
46
  % DEFAULT_API_VERSION,
47
47
  )
48
48
  return parser
49
+
50
+
51
+ def check_api_version(check_version):
52
+ # SDK supports auto-negotiation for us: always return True
53
+ return True
@@ -106,6 +106,13 @@ class ShowConsoleURL(command.ShowOne):
106
106
  const='spice-html5',
107
107
  help=_("Show SPICE console URL"),
108
108
  )
109
+ type_group.add_argument(
110
+ '--spice-direct',
111
+ dest='url_type',
112
+ action='store_const',
113
+ const='spice-direct',
114
+ help=_("Show SPICE direct protocol native console URL"),
115
+ )
109
116
  type_group.add_argument(
110
117
  '--rdp',
111
118
  dest='url_type',
@@ -0,0 +1,48 @@
1
+ # Licensed under the Apache License, Version 2.0 (the "License"); you may
2
+ # not use this file except in compliance with the License. You may obtain
3
+ # a copy of the License at
4
+ #
5
+ # http://www.apache.org/licenses/LICENSE-2.0
6
+ #
7
+ # Unless required by applicable law or agreed to in writing, software
8
+ # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9
+ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10
+ # License for the specific language governing permissions and limitations
11
+ # under the License.
12
+ #
13
+
14
+ """Compute v2 Console auth token implementations."""
15
+
16
+ from osc_lib.command import command
17
+ from osc_lib import utils
18
+
19
+ from openstackclient.i18n import _
20
+
21
+
22
+ def _get_console_connection_columns(item):
23
+ column_map: dict[str, str] = {}
24
+ hidden_columns = ['id', 'location', 'name']
25
+ return utils.get_osc_show_columns_for_sdk_resource(
26
+ item, column_map, hidden_columns
27
+ )
28
+
29
+
30
+ class ShowConsoleConnectionInformation(command.ShowOne):
31
+ _description = _("Show server's remote console connection information")
32
+
33
+ def get_parser(self, prog_name):
34
+ parser = super().get_parser(prog_name)
35
+ parser.add_argument(
36
+ 'token',
37
+ metavar='<token>',
38
+ help=_("Nova console token to lookup"),
39
+ )
40
+ return parser
41
+
42
+ def take_action(self, parsed_args):
43
+ compute_client = self.app.client_manager.compute
44
+ data = compute_client.validate_console_auth_token(parsed_args.token)
45
+ display_columns, columns = _get_console_connection_columns(data)
46
+ data = utils.get_dict_properties(data, columns)
47
+
48
+ return (display_columns, data)
@@ -156,12 +156,25 @@ class CreateFlavor(command.ShowOne):
156
156
  msg = _("--project is only allowed with --private")
157
157
  raise exceptions.CommandError(msg)
158
158
 
159
+ flavor_id = parsed_args.id
160
+ if parsed_args.id == 'auto':
161
+ # novaclient aliased 'auto' to mean "generate a UUID for me": we
162
+ # do the same to avoid breaking existing users
163
+ flavor_id = None
164
+
165
+ msg = _(
166
+ "Passing '--id auto' is deprecated. Nova will automatically "
167
+ "assign a UUID-like ID if no ID is provided. Omit the '--id' "
168
+ "parameter instead."
169
+ )
170
+ self.log.warning(msg)
171
+
159
172
  args = {
160
173
  'name': parsed_args.name,
161
174
  'ram': parsed_args.ram,
162
175
  'vcpus': parsed_args.vcpus,
163
176
  'disk': parsed_args.disk,
164
- 'id': parsed_args.id,
177
+ 'id': flavor_id,
165
178
  'ephemeral': parsed_args.ephemeral,
166
179
  'swap': parsed_args.swap,
167
180
  'rxtx_factor': parsed_args.rxtx_factor,
@@ -300,6 +300,7 @@ class ListKeypair(command.Lister):
300
300
  def take_action(self, parsed_args):
301
301
  compute_client = self.app.client_manager.compute
302
302
  identity_client = self.app.client_manager.identity
303
+ identity_sdk_client = self.app.client_manager.sdk_connection.identity
303
304
 
304
305
  kwargs = {}
305
306
 
@@ -345,11 +346,17 @@ class ListKeypair(command.Lister):
345
346
  parsed_args.project,
346
347
  parsed_args.project_domain,
347
348
  ).id
348
- users = identity_client.users.list(tenant_id=project)
349
+ assignments = identity_sdk_client.role_assignments(
350
+ scope_project_id=project
351
+ )
352
+ user_ids = set()
353
+ for assignment in assignments:
354
+ if assignment.user:
355
+ user_ids.add(assignment.user['id'])
349
356
 
350
357
  data = []
351
- for user in users:
352
- kwargs['user_id'] = user.id
358
+ for user_id in user_ids:
359
+ kwargs['user_id'] = user_id
353
360
  data.extend(compute_client.keypairs(**kwargs))
354
361
  elif parsed_args.user:
355
362
  if not sdk_utils.supports_microversion(compute_client, '2.10'):
@@ -21,7 +21,6 @@ import getpass
21
21
  import json
22
22
  import logging
23
23
  import os
24
- import typing as ty
25
24
 
26
25
  from cliff import columns as cliff_columns
27
26
  import iso8601
@@ -185,6 +184,7 @@ def _prep_server_detail(compute_client, image_client, server, *, refresh=True):
185
184
  'user_data': 'OS-EXT-SRV-ATTR:user_data',
186
185
  'vm_state': 'OS-EXT-STS:vm_state',
187
186
  'pinned_availability_zone': 'pinned_availability_zone',
187
+ 'scheduler_hints': 'scheduler_hints',
188
188
  }
189
189
  # Some columns returned by openstacksdk should not be shown because they're
190
190
  # either irrelevant or duplicates
@@ -205,7 +205,6 @@ def _prep_server_detail(compute_client, image_client, server, *, refresh=True):
205
205
  'min_count',
206
206
  'networks',
207
207
  'personality',
208
- 'scheduler_hints',
209
208
  # aliases
210
209
  'volumes',
211
210
  # unnecessary
@@ -236,6 +235,11 @@ def _prep_server_detail(compute_client, image_client, server, *, refresh=True):
236
235
 
237
236
  info = data
238
237
 
238
+ # NOTE(dviroel): microversion 2.100 is now retrieving scheduler_hints
239
+ # content from request_spec on detailed responses
240
+ if not sdk_utils.supports_microversion(compute_client, '2.100'):
241
+ info.pop('scheduler_hints', None)
242
+
239
243
  # Convert the image blob to a name
240
244
  image_info = info.get('image', {})
241
245
  if image_info and any(image_info.values()):
@@ -322,6 +326,11 @@ def _prep_server_detail(compute_client, image_client, server, *, refresh=True):
322
326
  info['OS-EXT-STS:power_state']
323
327
  )
324
328
 
329
+ if 'scheduler_hints' in info:
330
+ info['scheduler_hints'] = format_columns.DictListColumn(
331
+ info.pop('scheduler_hints', {}),
332
+ )
333
+
325
334
  return info
326
335
 
327
336
 
@@ -1864,7 +1873,7 @@ class CreateServer(command.ShowOne):
1864
1873
 
1865
1874
  # Default to empty list if nothing was specified and let nova
1866
1875
  # decide the default behavior.
1867
- networks: ty.Union[str, list[dict[str, str]], None] = []
1876
+ networks: str | list[dict[str, str]] | None = []
1868
1877
 
1869
1878
  if 'auto' in parsed_args.nics or 'none' in parsed_args.nics:
1870
1879
  if len(parsed_args.nics) > 1:
@@ -1938,9 +1947,9 @@ class CreateServer(command.ShowOne):
1938
1947
  network['port'] = nic['port-id']
1939
1948
 
1940
1949
  if nic['v4-fixed-ip']:
1941
- network['fixed'] = nic['v4-fixed-ip']
1950
+ network['fixed_ip'] = nic['v4-fixed-ip']
1942
1951
  elif nic['v6-fixed-ip']:
1943
- network['fixed'] = nic['v6-fixed-ip']
1952
+ network['fixed_ip'] = nic['v6-fixed-ip']
1944
1953
 
1945
1954
  if nic.get('tag'): # tags are optional
1946
1955
  network['tag'] = nic['tag']
@@ -2120,13 +2129,11 @@ class CreateServer(command.ShowOne):
2120
2129
  f.close()
2121
2130
 
2122
2131
  if parsed_args.wait:
2123
- if utils.wait_for_status(
2132
+ if not utils.wait_for_status(
2124
2133
  compute_client.get_server,
2125
2134
  server.id,
2126
2135
  callback=_show_progress,
2127
2136
  ):
2128
- self.app.stdout.write('\n')
2129
- else:
2130
2137
  msg = _('Error creating server: %s') % parsed_args.server_name
2131
2138
  raise exceptions.CommandError(msg)
2132
2139
 
@@ -2834,14 +2841,20 @@ class ListServer(command.Lister):
2834
2841
  'pinned_availability_zone',
2835
2842
  'hypervisor_hostname',
2836
2843
  'metadata',
2844
+ 'scheduler_hints',
2837
2845
  )
2838
2846
  column_headers += (
2839
2847
  'Availability Zone',
2840
2848
  'Pinned Availability Zone',
2841
2849
  'Host',
2842
2850
  'Properties',
2851
+ 'Scheduler Hints',
2843
2852
  )
2844
2853
 
2854
+ if parsed_args.all_projects:
2855
+ columns += ('project_id',)
2856
+ column_headers += ('Project ID',)
2857
+
2845
2858
  # support for additional columns
2846
2859
  if parsed_args.columns:
2847
2860
  for c in parsed_args.columns:
@@ -2884,6 +2897,12 @@ class ListServer(command.Lister):
2884
2897
  if c in ('Properties', "properties"):
2885
2898
  columns += ('Metadata',)
2886
2899
  column_headers += ('Properties',)
2900
+ if c in (
2901
+ 'scheduler_hints',
2902
+ "Scheduler Hints",
2903
+ ):
2904
+ columns += ('scheduler_hints',)
2905
+ column_headers += ('Scheduler Hints',)
2887
2906
 
2888
2907
  # remove duplicates
2889
2908
  column_headers = tuple(dict.fromkeys(column_headers))
@@ -2986,7 +3005,7 @@ class ListServer(command.Lister):
2986
3005
  # infrastructure failure situations.
2987
3006
  # For those servers with partial constructs we just skip the
2988
3007
  # processing of the image and flavor information.
2989
- if not hasattr(s, 'image') or not hasattr(s, 'flavor'):
3008
+ if getattr(s, 'status') == 'UNKNOWN':
2990
3009
  continue
2991
3010
 
2992
3011
  if 'id' in s.image and s.image.id is not None:
@@ -3050,6 +3069,7 @@ class ListServer(command.Lister):
3050
3069
  'metadata': format_columns.DictColumn,
3051
3070
  'security_groups_name': format_columns.ListColumn,
3052
3071
  'hypervisor_hostname': HostColumn,
3072
+ 'scheduler_hints': format_columns.DictListColumn,
3053
3073
  },
3054
3074
  )
3055
3075
  for s in data
@@ -3860,9 +3880,15 @@ host."""
3860
3880
  compute_client.evacuate_server(server, **kwargs)
3861
3881
 
3862
3882
  if parsed_args.wait:
3883
+ orig_status = server.status
3884
+ success = ['ACTIVE']
3885
+ if orig_status == 'SHUTOFF':
3886
+ success.append('SHUTOFF')
3887
+
3863
3888
  if utils.wait_for_status(
3864
3889
  compute_client.get_server,
3865
3890
  server.id,
3891
+ success_status=success,
3866
3892
  callback=_show_progress,
3867
3893
  ):
3868
3894
  self.app.stdout.write(_('Complete\n'))
@@ -4462,15 +4488,27 @@ class SetServer(command.Command):
4462
4488
  '(repeat option to set multiple properties)'
4463
4489
  ),
4464
4490
  )
4491
+ parser.add_argument(
4492
+ '--auto-approve',
4493
+ action='store_true',
4494
+ help=_(
4495
+ "Allow server state override without asking for confirmation"
4496
+ ),
4497
+ )
4465
4498
  parser.add_argument(
4466
4499
  '--state',
4467
4500
  metavar='<state>',
4468
4501
  choices=['active', 'error'],
4469
4502
  help=_(
4470
- 'New server state '
4471
- '**WARNING** This can result in instances that are no longer '
4472
- 'usable and should be used with caution '
4473
- '(admin only)'
4503
+ 'New server state.'
4504
+ '**WARNING** Resetting the state is intended to work around '
4505
+ 'servers stuck in an intermediate state, such as deleting. '
4506
+ 'If the server is in an error state then this is almost '
4507
+ 'never the correct command to run and you should prefer hard '
4508
+ 'reboot where possible. In particular, if the server is in '
4509
+ 'an error state due to a move operation, setting the state '
4510
+ 'can result in instances that are no longer usable. Proceed '
4511
+ 'with caution. (admin only)'
4474
4512
  ),
4475
4513
  )
4476
4514
  parser.add_argument(
@@ -4505,6 +4543,20 @@ class SetServer(command.Command):
4505
4543
  )
4506
4544
  return parser
4507
4545
 
4546
+ @staticmethod
4547
+ def ask_user_yesno(msg):
4548
+ """Ask user Y/N question
4549
+
4550
+ :param str msg: question text
4551
+ :return bool: User choice
4552
+ """
4553
+ while True:
4554
+ answer = getpass.getpass('{} [{}]: '.format(msg, 'y/n'))
4555
+ if answer in ('y', 'Y', 'yes'):
4556
+ return True
4557
+ elif answer in ('n', 'N', 'no'):
4558
+ return False
4559
+
4508
4560
  def take_action(self, parsed_args):
4509
4561
  compute_client = self.app.client_manager.compute
4510
4562
  server = compute_client.find_server(
@@ -4555,6 +4607,17 @@ class SetServer(command.Command):
4555
4607
  )
4556
4608
 
4557
4609
  if parsed_args.state:
4610
+ if not parsed_args.auto_approve:
4611
+ if not self.ask_user_yesno(
4612
+ _(
4613
+ "Resetting the server state can make it much harder "
4614
+ "to recover a server from an error state. If the "
4615
+ "server is in error status due to a failed move "
4616
+ "operation then this is likely not the correct "
4617
+ "approach to fix the problem. Do you wish to continue?"
4618
+ )
4619
+ ):
4620
+ return
4558
4621
  compute_client.reset_server_state(server, state=parsed_args.state)
4559
4622
 
4560
4623
  if parsed_args.root_password:
@@ -82,7 +82,7 @@ class ServerActionEventColumn(columns.FormattableColumn):
82
82
 
83
83
 
84
84
  def _get_server_event_columns(item, client):
85
- hidden_columns = ['name', 'server_id', 'links', 'location']
85
+ hidden_columns = ['name', 'server_id', 'links', 'location', 'finish_time']
86
86
 
87
87
  if not sdk_utils.supports_microversion(client, '2.58'):
88
88
  # updated_at was introduced in 2.58
@@ -189,6 +189,16 @@ def find_domain(identity_client, name_or_id):
189
189
  )
190
190
 
191
191
 
192
+ def find_domain_id_sdk(
193
+ identity_client, name_or_id, *, validate_actor_existence=True
194
+ ):
195
+ return _find_sdk_id(
196
+ identity_client.find_domain,
197
+ name_or_id=name_or_id,
198
+ validate_actor_existence=validate_actor_existence,
199
+ )
200
+
201
+
192
202
  def find_group(identity_client, name_or_id, domain_name_or_id=None):
193
203
  if domain_name_or_id is None:
194
204
  return _find_identity_resource(
@@ -204,6 +214,33 @@ def find_group(identity_client, name_or_id, domain_name_or_id=None):
204
214
  )
205
215
 
206
216
 
217
+ def find_group_id_sdk(
218
+ identity_client,
219
+ name_or_id,
220
+ domain_name_or_id=None,
221
+ *,
222
+ validate_actor_existence=True,
223
+ ):
224
+ if domain_name_or_id is None:
225
+ return _find_sdk_id(
226
+ identity_client.find_group,
227
+ name_or_id=name_or_id,
228
+ validate_actor_existence=validate_actor_existence,
229
+ )
230
+
231
+ domain_id = find_domain_id_sdk(
232
+ identity_client,
233
+ name_or_id=domain_name_or_id,
234
+ validate_actor_existence=validate_actor_existence,
235
+ )
236
+ return _find_sdk_id(
237
+ identity_client.find_group,
238
+ name_or_id=name_or_id,
239
+ validate_actor_existence=validate_actor_existence,
240
+ domain_id=domain_id,
241
+ )
242
+
243
+
207
244
  def find_project(identity_client, name_or_id, domain_name_or_id=None):
208
245
  if domain_name_or_id is None:
209
246
  return _find_identity_resource(
@@ -229,6 +266,32 @@ def find_user(identity_client, name_or_id, domain_name_or_id=None):
229
266
  )
230
267
 
231
268
 
269
+ def find_user_id_sdk(
270
+ identity_client,
271
+ name_or_id,
272
+ domain_name_or_id=None,
273
+ *,
274
+ validate_actor_existence=True,
275
+ ):
276
+ if domain_name_or_id is None:
277
+ return _find_sdk_id(
278
+ identity_client.find_user,
279
+ name_or_id=name_or_id,
280
+ validate_actor_existence=validate_actor_existence,
281
+ )
282
+ domain_id = find_domain_id_sdk(
283
+ identity_client,
284
+ name_or_id=domain_name_or_id,
285
+ validate_actor_existence=validate_actor_existence,
286
+ )
287
+ return _find_sdk_id(
288
+ identity_client.find_user,
289
+ name_or_id=name_or_id,
290
+ validate_actor_existence=validate_actor_existence,
291
+ domain_id=domain_id,
292
+ )
293
+
294
+
232
295
  def _find_identity_resource(
233
296
  identity_client_manager, name_or_id, resource_type, **kwargs
234
297
  ):
@@ -269,13 +332,20 @@ def _find_identity_resource(
269
332
  return resource_type(None, {'id': name_or_id, 'name': name_or_id})
270
333
 
271
334
 
272
- def get_immutable_options(parsed_args):
273
- options = {}
274
- if parsed_args.immutable:
275
- options['immutable'] = True
276
- if parsed_args.no_immutable:
277
- options['immutable'] = False
278
- return options
335
+ def _find_sdk_id(
336
+ find_command, name_or_id, *, validate_actor_existence=True, **kwargs
337
+ ):
338
+ try:
339
+ resource = find_command(
340
+ name_or_id=name_or_id, ignore_missing=False, **kwargs
341
+ )
342
+ except sdk_exceptions.ForbiddenException:
343
+ return name_or_id
344
+ except sdk_exceptions.ResourceNotFound as exc:
345
+ if not validate_actor_existence:
346
+ return name_or_id
347
+ raise exceptions.CommandError from exc
348
+ return resource.id
279
349
 
280
350
 
281
351
  def add_user_domain_option_to_parser(parser):
@@ -340,17 +410,21 @@ def add_inherited_option_to_parser(parser):
340
410
 
341
411
 
342
412
  def add_resource_option_to_parser(parser):
343
- enable_group = parser.add_mutually_exclusive_group()
344
- enable_group.add_argument(
413
+ immutable_group = parser.add_mutually_exclusive_group()
414
+ immutable_group.add_argument(
345
415
  '--immutable',
346
416
  action='store_true',
417
+ dest='immutable',
418
+ default=None,
347
419
  help=_(
348
420
  'Make resource immutable. An immutable project may not '
349
421
  'be deleted or modified except to remove the immutable flag'
350
422
  ),
351
423
  )
352
- enable_group.add_argument(
424
+ immutable_group.add_argument(
353
425
  '--no-immutable',
354
- action='store_true',
426
+ action='store_false',
427
+ dest='immutable',
428
+ default=None,
355
429
  help=_('Make resource mutable (default)'),
356
430
  )