python-openstackclient 6.4.0__py3-none-any.whl → 6.5.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 (44) hide show
  1. openstackclient/common/availability_zone.py +4 -4
  2. openstackclient/compute/v2/server.py +173 -99
  3. openstackclient/identity/v3/mapping.py +25 -3
  4. openstackclient/identity/v3/policy.py +3 -1
  5. openstackclient/image/v2/image.py +37 -0
  6. openstackclient/image/v2/metadef_namespaces.py +25 -21
  7. openstackclient/image/v2/metadef_objects.py +26 -30
  8. openstackclient/image/v2/metadef_properties.py +60 -38
  9. openstackclient/network/v2/default_security_group_rule.py +23 -4
  10. openstackclient/network/v2/local_ip_association.py +1 -1
  11. openstackclient/network/v2/ndp_proxy.py +7 -3
  12. openstackclient/network/v2/network.py +2 -2
  13. openstackclient/network/v2/port.py +23 -7
  14. openstackclient/network/v2/subnet.py +1 -0
  15. openstackclient/tests/functional/base.py +7 -4
  16. openstackclient/tests/unit/compute/v2/test_server.py +72 -54
  17. openstackclient/tests/unit/identity/v3/test_mappings.py +9 -4
  18. openstackclient/tests/unit/identity/v3/test_trust.py +0 -2
  19. openstackclient/tests/unit/image/v1/fakes.py +1 -1
  20. openstackclient/tests/unit/image/v2/test_image.py +60 -0
  21. openstackclient/tests/unit/image/v2/test_metadef_namespaces.py +5 -19
  22. openstackclient/tests/unit/integ/cli/test_shell.py +0 -2
  23. openstackclient/tests/unit/network/v2/fakes.py +1 -0
  24. openstackclient/tests/unit/network/v2/test_network.py +33 -0
  25. openstackclient/tests/unit/network/v2/test_network_trunk.py +6 -8
  26. openstackclient/tests/unit/network/v2/test_port.py +60 -18
  27. openstackclient/tests/unit/network/v2/test_subnet.py +92 -0
  28. openstackclient/tests/unit/network/v2/test_subnet_pool.py +11 -13
  29. openstackclient/tests/unit/test_shell.py +1 -7
  30. openstackclient/tests/unit/utils.py +8 -1
  31. openstackclient/tests/unit/volume/v1/test_volume.py +4 -6
  32. openstackclient/tests/unit/volume/v2/test_volume.py +4 -6
  33. openstackclient/tests/unit/volume/v2/test_volume_snapshot.py +3 -5
  34. openstackclient/volume/v2/volume_type.py +3 -3
  35. {python_openstackclient-6.4.0.dist-info → python_openstackclient-6.5.0.dist-info}/AUTHORS +4 -0
  36. {python_openstackclient-6.4.0.dist-info → python_openstackclient-6.5.0.dist-info}/METADATA +3 -1
  37. {python_openstackclient-6.4.0.dist-info → python_openstackclient-6.5.0.dist-info}/RECORD +42 -43
  38. {python_openstackclient-6.4.0.dist-info → python_openstackclient-6.5.0.dist-info}/entry_points.txt +6 -5
  39. python_openstackclient-6.5.0.dist-info/pbr.json +1 -0
  40. openstackclient/tests/unit/common/test_parseractions.py +0 -233
  41. python_openstackclient-6.4.0.dist-info/pbr.json +0 -1
  42. {python_openstackclient-6.4.0.dist-info → python_openstackclient-6.5.0.dist-info}/LICENSE +0 -0
  43. {python_openstackclient-6.4.0.dist-info → python_openstackclient-6.5.0.dist-info}/WHEEL +0 -0
  44. {python_openstackclient-6.4.0.dist-info → python_openstackclient-6.5.0.dist-info}/top_level.txt +0 -0
@@ -16,7 +16,7 @@
16
16
  import copy
17
17
  import logging
18
18
 
19
- from novaclient import exceptions as nova_exceptions
19
+ from openstack import exceptions as sdk_exceptions
20
20
  from osc_lib.command import command
21
21
  from osc_lib import utils
22
22
 
@@ -119,8 +119,8 @@ class ListAvailabilityZone(command.Lister):
119
119
  def _get_compute_availability_zones(self, parsed_args):
120
120
  compute_client = self.app.client_manager.sdk_connection.compute
121
121
  try:
122
- data = compute_client.availability_zones(details=True)
123
- except nova_exceptions.Forbidden: # policy doesn't allow
122
+ data = list(compute_client.availability_zones(details=True))
123
+ except sdk_exceptions.ForbiddenException: # policy doesn't allow
124
124
  try:
125
125
  data = compute_client.availability_zones(details=False)
126
126
  except Exception:
@@ -135,7 +135,7 @@ class ListAvailabilityZone(command.Lister):
135
135
  volume_client = self.app.client_manager.sdk_connection.volume
136
136
  data = []
137
137
  try:
138
- data = volume_client.availability_zones()
138
+ data = list(volume_client.availability_zones())
139
139
  except Exception as e:
140
140
  LOG.debug('Volume availability zone exception: %s', e)
141
141
  if parsed_args.volume:
@@ -149,16 +149,13 @@ def _prep_server_detail(compute_client, image_client, server, refresh=True):
149
149
  # Some commands using this routine were originally implemented with the
150
150
  # nova python wrappers, and were later migrated to use the SDK. Map the
151
151
  # SDK's property names to the original property names to maintain backward
152
- # compatibility for existing users. Data is duplicated under both the old
153
- # and new name so users can consume the data by either name.
152
+ # compatibility for existing users.
154
153
  column_map = {
155
154
  'access_ipv4': 'accessIPv4',
156
155
  'access_ipv6': 'accessIPv6',
157
156
  'admin_password': 'adminPass',
158
- 'admin_password': 'adminPass',
159
- 'volumes': 'os-extended-volumes:volumes_attached',
157
+ 'attached_volumes': 'volumes_attached',
160
158
  'availability_zone': 'OS-EXT-AZ:availability_zone',
161
- 'block_device_mapping': 'block_device_mapping_v2',
162
159
  'compute_host': 'OS-EXT-SRV-ATTR:host',
163
160
  'created_at': 'created',
164
161
  'disk_config': 'OS-DCF:diskConfig',
@@ -168,7 +165,6 @@ def _prep_server_detail(compute_client, image_client, server, refresh=True):
168
165
  'fault': 'fault',
169
166
  'hostname': 'OS-EXT-SRV-ATTR:hostname',
170
167
  'hypervisor_hostname': 'OS-EXT-SRV-ATTR:hypervisor_hostname',
171
- 'image_id': 'imageRef',
172
168
  'instance_name': 'OS-EXT-SRV-ATTR:instance_name',
173
169
  'is_locked': 'locked',
174
170
  'kernel_id': 'OS-EXT-SRV-ATTR:kernel_id',
@@ -179,21 +175,56 @@ def _prep_server_detail(compute_client, image_client, server, refresh=True):
179
175
  'ramdisk_id': 'OS-EXT-SRV-ATTR:ramdisk_id',
180
176
  'reservation_id': 'OS-EXT-SRV-ATTR:reservation_id',
181
177
  'root_device_name': 'OS-EXT-SRV-ATTR:root_device_name',
182
- 'scheduler_hints': 'OS-SCH-HNT:scheduler_hints',
183
178
  'task_state': 'OS-EXT-STS:task_state',
184
179
  'terminated_at': 'OS-SRV-USG:terminated_at',
185
180
  'updated_at': 'updated',
186
181
  'user_data': 'OS-EXT-SRV-ATTR:user_data',
187
182
  'vm_state': 'OS-EXT-STS:vm_state',
188
183
  }
184
+ # Some columns returned by openstacksdk should not be shown because they're
185
+ # either irrelevant or duplicates
186
+ ignored_columns = {
187
+ # computed columns
188
+ 'interface_ip',
189
+ 'location',
190
+ 'private_v4',
191
+ 'private_v6',
192
+ 'public_v4',
193
+ 'public_v6',
194
+ # create-only columns
195
+ 'block_device_mapping',
196
+ 'image_id',
197
+ 'max_count',
198
+ 'min_count',
199
+ 'scheduler_hints',
200
+ # aliases
201
+ 'volumes',
202
+ # unnecessary
203
+ 'links',
204
+ }
205
+ # Some columns are only present in certain responses and should not be
206
+ # shown otherwise.
207
+ optional_columns = {
208
+ 'admin_password', # removed in 2.14
209
+ 'fault', # only present in errored servers
210
+ 'flavor_id', # removed in 2.47
211
+ 'networks', # only present in create responses
212
+ 'security_groups', # only present in create, detail responses
213
+ }
189
214
 
190
- info.update(
191
- {
192
- column_map[column]: data
193
- for column, data in info.items()
194
- if column in column_map
195
- }
196
- )
215
+ data = {}
216
+ for key, value in info.items():
217
+ if key in ignored_columns:
218
+ continue
219
+
220
+ if key in optional_columns:
221
+ if info[key] is None:
222
+ continue
223
+
224
+ alias = column_map.get(key)
225
+ data[alias or key] = value
226
+
227
+ info = data
197
228
 
198
229
  # Convert the image blob to a name
199
230
  image_info = info.get('image', {})
@@ -214,46 +245,57 @@ def _prep_server_detail(compute_client, image_client, server, refresh=True):
214
245
  # Convert the flavor blob to a name
215
246
  flavor_info = info.get('flavor', {})
216
247
  # Microversion 2.47 puts the embedded flavor into the server response
217
- # body but omits the id, so if not present we just expose the flavor
218
- # dict in the server output.
219
- if 'id' in flavor_info:
248
+ # body. The presence of the 'original_name' attribute indicates this.
249
+ if flavor_info.get('original_name') is None: # microversion < 2.47
220
250
  flavor_id = flavor_info.get('id', '')
221
251
  try:
222
252
  flavor = utils.find_resource(compute_client.flavors, flavor_id)
223
253
  info['flavor'] = "%s (%s)" % (flavor.name, flavor_id)
224
254
  except Exception:
225
255
  info['flavor'] = flavor_id
226
- else:
256
+ else: # microversion >= 2.47
227
257
  info['flavor'] = format_columns.DictColumn(flavor_info)
228
258
 
229
- if 'os-extended-volumes:volumes_attached' in info:
259
+ # there's a lot of redundant information in BDMs - strip it
260
+ if 'volumes_attached' in info:
230
261
  info.update(
231
262
  {
232
263
  'volumes_attached': format_columns.ListDictColumn(
233
- info.pop('os-extended-volumes:volumes_attached')
264
+ [
265
+ {
266
+ k: v
267
+ for k, v in volume.items()
268
+ if v is not None and k != 'location'
269
+ }
270
+ for volume in info.pop('volumes_attached') or []
271
+ ]
234
272
  )
235
273
  }
236
274
  )
275
+
237
276
  if 'security_groups' in info:
238
277
  info.update(
239
278
  {
240
279
  'security_groups': format_columns.ListDictColumn(
241
- info.pop('security_groups')
280
+ info.pop('security_groups'),
242
281
  )
243
282
  }
244
283
  )
284
+
245
285
  if 'tags' in info:
246
286
  info.update({'tags': format_columns.ListColumn(info.pop('tags'))})
247
287
 
248
- # NOTE(dtroyer): novaclient splits these into separate entries...
249
- # Format addresses in a useful way
250
- info['addresses'] = (
251
- AddressesColumn(info['addresses'])
252
- if 'addresses' in info
253
- else format_columns.DictListColumn(info.get('networks'))
254
- )
288
+ # Map 'networks' to 'addresses', if present. Note that the 'networks' key
289
+ # is used for create responses, otherwise it's 'addresses'. We know it'll
290
+ # be set because this is one of our optional columns.
291
+ if 'networks' in info:
292
+ info['addresses'] = format_columns.DictListColumn(
293
+ info.pop('networks', {}),
294
+ )
295
+ else:
296
+ info['addresses'] = AddressesColumn(info.get('addresses', {}))
255
297
 
256
- # Map 'metadata' field to 'properties'
298
+ # Map 'metadata' field to 'properties' and format
257
299
  info['properties'] = format_columns.DictColumn(info.pop('metadata'))
258
300
 
259
301
  # Migrate tenant_id to project_id naming
@@ -266,9 +308,6 @@ def _prep_server_detail(compute_client, image_client, server, refresh=True):
266
308
  info['OS-EXT-STS:power_state']
267
309
  )
268
310
 
269
- # Remove values that are long and not too useful
270
- info.pop('links', None)
271
-
272
311
  return info
273
312
 
274
313
 
@@ -841,7 +880,7 @@ class NICAction(argparse.Action):
841
880
  "Invalid argument %s; characters ',' and '=' are not "
842
881
  "allowed"
843
882
  )
844
- raise argparse.ArgumentTypeError(msg % values)
883
+ raise argparse.ArgumentError(self, msg % values)
845
884
 
846
885
  values = '='.join([self.key, values])
847
886
  else:
@@ -869,7 +908,7 @@ class NICAction(argparse.Action):
869
908
  "'net-id=net-uuid,port-id=port-uuid,v4-fixed-ip=ip-addr,"
870
909
  "v6-fixed-ip=ip-addr,tag=tag'"
871
910
  )
872
- raise argparse.ArgumentTypeError(msg % values)
911
+ raise argparse.ArgumentError(self, msg % values)
873
912
 
874
913
  info[k] = v
875
914
 
@@ -878,7 +917,7 @@ class NICAction(argparse.Action):
878
917
  'Invalid argument %s; either network or port should be '
879
918
  'specified but not both'
880
919
  )
881
- raise argparse.ArgumentTypeError(msg % values)
920
+ raise argparse.ArgumenteError(self, msg % values)
882
921
 
883
922
  getattr(namespace, self.dest).append(info)
884
923
 
@@ -896,7 +935,7 @@ class BDMLegacyAction(argparse.Action):
896
935
  "Invalid argument %s; argument must be of form "
897
936
  "'dev-name=id[:type[:size[:delete-on-terminate]]]'"
898
937
  )
899
- raise argparse.ArgumentTypeError(msg % values)
938
+ raise argparse.ArgumentError(self, msg % values)
900
939
 
901
940
  mapping = {
902
941
  'device_name': dev_name,
@@ -913,7 +952,7 @@ class BDMLegacyAction(argparse.Action):
913
952
  "Invalid argument %s; 'type' must be one of: volume, "
914
953
  "snapshot, image"
915
954
  )
916
- raise argparse.ArgumentTypeError(msg % values)
955
+ raise argparse.ArgumentError(self, msg % values)
917
956
 
918
957
  mapping['source_type'] = dev_map[1]
919
958
 
@@ -966,12 +1005,13 @@ class BDMAction(parseractions.MultiKeyValueAction):
966
1005
  "Invalid keys %(invalid_keys)s specified.\n"
967
1006
  "Valid keys are: %(valid_keys)s"
968
1007
  )
969
- raise argparse.ArgumentTypeError(
1008
+ raise argparse.ArgumentError(
1009
+ self,
970
1010
  msg
971
1011
  % {
972
1012
  'invalid_keys': ', '.join(invalid_keys),
973
1013
  'valid_keys': ', '.join(valid_keys),
974
- }
1014
+ },
975
1015
  )
976
1016
 
977
1017
  missing_keys = [k for k in self.required_keys if k not in keys]
@@ -980,12 +1020,13 @@ class BDMAction(parseractions.MultiKeyValueAction):
980
1020
  "Missing required keys %(missing_keys)s.\n"
981
1021
  "Required keys are: %(required_keys)s"
982
1022
  )
983
- raise argparse.ArgumentTypeError(
1023
+ raise argparse.ArgumentError(
1024
+ self,
984
1025
  msg
985
1026
  % {
986
1027
  'missing_keys': ', '.join(missing_keys),
987
1028
  'required_keys': ', '.join(self.required_keys),
988
- }
1029
+ },
989
1030
  )
990
1031
 
991
1032
  def __call__(self, parser, namespace, values, option_string=None):
@@ -1336,10 +1377,19 @@ class CreateServer(command.ShowOne):
1336
1377
  '(supported by --os-compute-api-version 2.74 or above)'
1337
1378
  ),
1338
1379
  )
1380
+ parser.add_argument(
1381
+ '--server-group',
1382
+ metavar='<server-group>',
1383
+ help=_(
1384
+ "Server group to create the server within "
1385
+ "(this is an alias for '--hint group=<server-group-id>')"
1386
+ ),
1387
+ )
1339
1388
  parser.add_argument(
1340
1389
  '--hint',
1341
1390
  metavar='<key=value>',
1342
1391
  action=parseractions.KeyValueAppendAction,
1392
+ dest='hints',
1343
1393
  default={},
1344
1394
  help=_('Hints for the scheduler'),
1345
1395
  )
@@ -1858,13 +1908,20 @@ class CreateServer(command.ShowOne):
1858
1908
  security_group_names.append(sg['name'])
1859
1909
 
1860
1910
  hints = {}
1861
- for key, values in parsed_args.hint.items():
1911
+ for key, values in parsed_args.hints.items():
1862
1912
  # only items with multiple values will result in a list
1863
1913
  if len(values) == 1:
1864
1914
  hints[key] = values[0]
1865
1915
  else:
1866
1916
  hints[key] = values
1867
1917
 
1918
+ if parsed_args.server_group:
1919
+ server_group_obj = utils.find_resource(
1920
+ compute_client.server_groups,
1921
+ parsed_args.server_group,
1922
+ )
1923
+ hints['group'] = server_group_obj.id
1924
+
1868
1925
  if isinstance(parsed_args.config_drive, bool):
1869
1926
  # NOTE(stephenfin): The API doesn't accept False as a value :'(
1870
1927
  config_drive = parsed_args.config_drive or None
@@ -1987,9 +2044,8 @@ class CreateServer(command.ShowOne):
1987
2044
  ):
1988
2045
  self.app.stdout.write('\n')
1989
2046
  else:
1990
- LOG.error('Error creating server: %s', parsed_args.server_name)
1991
- self.app.stdout.write(_('Error creating server\n'))
1992
- raise SystemExit
2047
+ msg = _('Error creating server: %s') % parsed_args.server_name
2048
+ raise exceptions.CommandError(msg)
1993
2049
 
1994
2050
  details = _prep_server_detail(compute_client, image_client, server)
1995
2051
  return zip(*sorted(details.items()))
@@ -2081,17 +2137,52 @@ class DeleteServer(command.Command):
2081
2137
  server_obj.id,
2082
2138
  callback=_show_progress,
2083
2139
  ):
2084
- msg = _('Error deleting server: %s')
2085
- LOG.error(msg, server_obj.id)
2086
- self.app.stdout.write(_('Error deleting server\n'))
2087
- raise SystemExit
2140
+ msg = _('Error deleting server: %s') % server_obj.id
2141
+ raise exceptions.CommandError(msg)
2088
2142
 
2089
2143
 
2090
- def percent_type(x):
2091
- x = int(x)
2092
- if not 0 < x <= 100:
2093
- raise argparse.ArgumentTypeError("Must be between 0 and 100")
2094
- return x
2144
+ class PercentAction(argparse.Action):
2145
+ def __init__(
2146
+ self,
2147
+ option_strings,
2148
+ dest,
2149
+ nargs=None,
2150
+ const=None,
2151
+ default=None,
2152
+ type=None,
2153
+ choices=None,
2154
+ required=False,
2155
+ help=None,
2156
+ metavar=None,
2157
+ ):
2158
+ if nargs == 0:
2159
+ raise ValueError(
2160
+ 'nargs for store actions must be != 0; if you '
2161
+ 'have nothing to store, actions such as store '
2162
+ 'true or store const may be more appropriate'
2163
+ )
2164
+
2165
+ if const is not None:
2166
+ raise ValueError('const does not make sense for PercentAction')
2167
+
2168
+ super().__init__(
2169
+ option_strings=option_strings,
2170
+ dest=dest,
2171
+ nargs=nargs,
2172
+ const=const,
2173
+ default=default,
2174
+ type=type,
2175
+ choices=choices,
2176
+ required=required,
2177
+ help=help,
2178
+ metavar=metavar,
2179
+ )
2180
+
2181
+ def __call__(self, parser, namespace, values, option_string=None):
2182
+ x = int(values)
2183
+ if not 0 < x <= 100:
2184
+ raise argparse.ArgumentError(self, "Must be between 0 and 100")
2185
+ setattr(namespace, self.dest, x)
2095
2186
 
2096
2187
 
2097
2188
  class ListServer(command.Lister):
@@ -2244,7 +2335,7 @@ class ListServer(command.Lister):
2244
2335
  )
2245
2336
  parser.add_argument(
2246
2337
  '--progress',
2247
- type=percent_type,
2338
+ action=PercentAction,
2248
2339
  default=None,
2249
2340
  help=_(
2250
2341
  'Search by progress value (%%) '
@@ -2751,7 +2842,7 @@ class ListServer(command.Lister):
2751
2842
  try:
2752
2843
  # some deployments can have *loads* of images so we only
2753
2844
  # want to list the ones we care about. It would be better
2754
- # to only retrun the *fields* we care about (name) but
2845
+ # to only return the *fields* we care about (name) but
2755
2846
  # glance doesn't support that
2756
2847
  # NOTE(stephenfin): This could result in super long URLs
2757
2848
  # but it seems unlikely to cause issues. Apache supports
@@ -3110,9 +3201,8 @@ revert to release the new server and restart the old one."""
3110
3201
  ):
3111
3202
  self.app.stdout.write(_('Complete\n'))
3112
3203
  else:
3113
- LOG.error(_('Error migrating server: %s'), server.id)
3114
- self.app.stdout.write(_('Error migrating server\n'))
3115
- raise SystemExit
3204
+ msg = _('Error migrating server: %s') % server.id
3205
+ raise exceptions.CommandError(msg)
3116
3206
 
3117
3207
 
3118
3208
  class PauseServer(command.Command):
@@ -3194,9 +3284,8 @@ class RebootServer(command.Command):
3194
3284
  ):
3195
3285
  self.app.stdout.write(_('Complete\n'))
3196
3286
  else:
3197
- LOG.error(_('Error rebooting server: %s'), server_id)
3198
- self.app.stdout.write(_('Error rebooting server\n'))
3199
- raise SystemExit
3287
+ msg = _('Error rebooting server: %s') % server_id
3288
+ raise exceptions.CommandError(msg)
3200
3289
 
3201
3290
 
3202
3291
  class RebuildServer(command.ShowOne):
@@ -3566,9 +3655,8 @@ class RebuildServer(command.ShowOne):
3566
3655
  ):
3567
3656
  self.app.stdout.write(_('Complete\n'))
3568
3657
  else:
3569
- LOG.error(_('Error rebuilding server: %s'), server.id)
3570
- self.app.stdout.write(_('Error rebuilding server\n'))
3571
- raise SystemExit
3658
+ msg = _('Error rebuilding server: %s') % server.id
3659
+ raise exceptions.CommandError(msg)
3572
3660
 
3573
3661
  details = _prep_server_detail(
3574
3662
  compute_client, image_client, server, refresh=False
@@ -3689,9 +3777,8 @@ host."""
3689
3777
  ):
3690
3778
  self.app.stdout.write(_('Complete\n'))
3691
3779
  else:
3692
- LOG.error(_('Error evacuating server: %s'), server.id)
3693
- self.app.stdout.write(_('Error evacuating server\n'))
3694
- raise SystemExit
3780
+ msg = _('Error evacuating server: %s') % server.id
3781
+ raise exceptions.CommandError(msg)
3695
3782
 
3696
3783
  details = _prep_server_detail(
3697
3784
  compute_client, image_client, server, refresh=True
@@ -4042,9 +4129,8 @@ release the new server and restart the old one."""
4042
4129
  ):
4043
4130
  self.app.stdout.write(_('Complete\n'))
4044
4131
  else:
4045
- LOG.error(_('Error resizing server: %s'), server.id)
4046
- self.app.stdout.write(_('Error resizing server\n'))
4047
- raise SystemExit
4132
+ msg = _('Error resizing server: %s') % server.id
4133
+ raise exceptions.CommandError(msg)
4048
4134
  elif parsed_args.confirm:
4049
4135
  self.log.warning(
4050
4136
  _(
@@ -4416,6 +4502,7 @@ class ShelveServer(command.Command):
4416
4502
  self.app.stdout.flush()
4417
4503
 
4418
4504
  compute_client = self.app.client_manager.sdk_connection.compute
4505
+ server_ids = []
4419
4506
 
4420
4507
  for server in parsed_args.servers:
4421
4508
  server_obj = compute_client.find_server(
@@ -4425,6 +4512,8 @@ class ShelveServer(command.Command):
4425
4512
  if server_obj.status.lower() in ('shelved', 'shelved_offloaded'):
4426
4513
  continue
4427
4514
 
4515
+ server_ids.append(server_obj.id)
4516
+
4428
4517
  compute_client.shelve_server(server_obj.id)
4429
4518
 
4430
4519
  # if we don't have to wait, either because it was requested explicitly
@@ -4432,56 +4521,44 @@ class ShelveServer(command.Command):
4432
4521
  if not parsed_args.wait and not parsed_args.offload:
4433
4522
  return
4434
4523
 
4435
- for server in parsed_args.servers:
4524
+ for server_id in server_ids:
4436
4525
  # We use osc-lib's wait_for_status since that allows for a callback
4437
4526
  # TODO(stephenfin): We should wait for these in parallel using e.g.
4438
4527
  # https://review.opendev.org/c/openstack/osc-lib/+/762503/
4439
4528
  if not utils.wait_for_status(
4440
4529
  compute_client.get_server,
4441
- server_obj.id,
4530
+ server_id,
4442
4531
  success_status=('shelved', 'shelved_offloaded'),
4443
4532
  callback=_show_progress,
4444
4533
  ):
4445
- LOG.error(_('Error shelving server: %s'), server_obj.id)
4446
- self.app.stdout.write(
4447
- _('Error shelving server: %s\n') % server_obj.id
4448
- )
4449
- raise SystemExit
4534
+ msg = _('Error shelving server: %s') % server_id
4535
+ raise exceptions.CommandError(msg)
4450
4536
 
4451
4537
  if not parsed_args.offload:
4452
4538
  return
4453
4539
 
4454
- for server in parsed_args.servers:
4455
- server_obj = compute_client.find_server(
4456
- server,
4457
- ignore_missing=False,
4458
- )
4540
+ for server_id in server_ids:
4541
+ server_obj = compute_client.get_server(server_id)
4459
4542
  if server_obj.status.lower() == 'shelved_offloaded':
4460
4543
  continue
4461
4544
 
4462
- compute_client.shelve_offload_server(server_obj.id)
4545
+ compute_client.shelve_offload_server(server_id)
4463
4546
 
4464
4547
  if not parsed_args.wait:
4465
4548
  return
4466
4549
 
4467
- for server in parsed_args.servers:
4550
+ for server_id in server_ids:
4468
4551
  # We use osc-lib's wait_for_status since that allows for a callback
4469
4552
  # TODO(stephenfin): We should wait for these in parallel using e.g.
4470
4553
  # https://review.opendev.org/c/openstack/osc-lib/+/762503/
4471
4554
  if not utils.wait_for_status(
4472
4555
  compute_client.get_server,
4473
- server_obj.id,
4556
+ server_id,
4474
4557
  success_status=('shelved_offloaded',),
4475
4558
  callback=_show_progress,
4476
4559
  ):
4477
- LOG.error(
4478
- _('Error offloading shelved server %s'),
4479
- server_obj.id,
4480
- )
4481
- self.app.stdout.write(
4482
- _('Error offloading shelved server: %s\n') % server_obj.id
4483
- )
4484
- raise SystemExit
4560
+ msg = _('Error offloading shelved server: %s') % server_id
4561
+ raise exceptions.CommandError(msg)
4485
4562
 
4486
4563
 
4487
4564
  class ShowServer(command.ShowOne):
@@ -5073,8 +5150,5 @@ class UnshelveServer(command.Command):
5073
5150
  success_status=('active', 'shutoff'),
5074
5151
  callback=_show_progress,
5075
5152
  ):
5076
- LOG.error(_('Error unshelving server %s'), server_obj.id)
5077
- self.app.stdout.write(
5078
- _('Error unshelving server: %s\n') % server_obj.id
5079
- )
5080
- raise SystemExit
5153
+ msg = _('Error unshelving server: %s') % server_obj.id
5154
+ raise exceptions.CommandError(msg)
@@ -81,6 +81,21 @@ class _RulesReader(object):
81
81
  else:
82
82
  return rules
83
83
 
84
+ @staticmethod
85
+ def add_federated_schema_version_option(parser):
86
+ parser.add_argument(
87
+ '--schema-version',
88
+ metavar='<schema_version>',
89
+ required=False,
90
+ default=None,
91
+ help=_(
92
+ "The federated attribute mapping schema version. The "
93
+ "default value on the client side is 'None'; however, that "
94
+ "will lead the backend to set the default according to "
95
+ "'attribute_mapping_default_schema_version' option."
96
+ ),
97
+ )
98
+
84
99
 
85
100
  class CreateMapping(command.ShowOne, _RulesReader):
86
101
  _description = _("Create new mapping")
@@ -98,6 +113,7 @@ class CreateMapping(command.ShowOne, _RulesReader):
98
113
  required=True,
99
114
  help=_('Filename that contains a set of mapping rules (required)'),
100
115
  )
116
+ _RulesReader.add_federated_schema_version_option(parser)
101
117
  return parser
102
118
 
103
119
  def take_action(self, parsed_args):
@@ -105,7 +121,9 @@ class CreateMapping(command.ShowOne, _RulesReader):
105
121
 
106
122
  rules = self._read_rules(parsed_args.rules)
107
123
  mapping = identity_client.federation.mappings.create(
108
- mapping_id=parsed_args.mapping, rules=rules
124
+ mapping_id=parsed_args.mapping,
125
+ rules=rules,
126
+ schema_version=parsed_args.schema_version,
109
127
  )
110
128
 
111
129
  mapping._info.pop('links', None)
@@ -158,7 +176,7 @@ class ListMapping(command.Lister):
158
176
  # rules, (s)he should show specific ones.
159
177
  identity_client = self.app.client_manager.identity
160
178
  data = identity_client.federation.mappings.list()
161
- columns = ('ID',)
179
+ columns = ('ID', 'schema_version')
162
180
  items = [utils.get_item_properties(s, columns) for s in data]
163
181
  return (columns, items)
164
182
 
@@ -178,6 +196,8 @@ class SetMapping(command.Command, _RulesReader):
178
196
  metavar='<filename>',
179
197
  help=_('Filename that contains a new set of mapping rules'),
180
198
  )
199
+
200
+ _RulesReader.add_federated_schema_version_option(parser)
181
201
  return parser
182
202
 
183
203
  def take_action(self, parsed_args):
@@ -186,7 +206,9 @@ class SetMapping(command.Command, _RulesReader):
186
206
  rules = self._read_rules(parsed_args.rules)
187
207
 
188
208
  mapping = identity_client.federation.mappings.update(
189
- mapping=parsed_args.mapping, rules=rules
209
+ mapping=parsed_args.mapping,
210
+ rules=rules,
211
+ schema_version=parsed_args.schema_version,
190
212
  )
191
213
 
192
214
  mapping._info.pop('links', None)
@@ -92,7 +92,9 @@ class DeletePolicy(command.Command):
92
92
 
93
93
  if result > 0:
94
94
  total = len(parsed_args.policy)
95
- msg = _("%(result)s of %(total)s policys failed " "to delete.") % {
95
+ msg = _(
96
+ "%(result)s of %(total)s policies failed " "to delete."
97
+ ) % {
96
98
  'result': result,
97
99
  'total': total,
98
100
  }