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
@@ -17,14 +17,16 @@
17
17
 
18
18
  import argparse
19
19
  from base64 import b64encode
20
+ import copy
20
21
  import logging
21
22
  import os
22
23
  import sys
23
24
  import typing as ty
25
+ import urllib.parse
24
26
 
25
- from cinderclient import api_versions
26
27
  from openstack import exceptions as sdk_exceptions
27
28
  from openstack.image import image_signer
29
+ from openstack import utils as sdk_utils
28
30
  from osc_lib.api import utils as api_utils
29
31
  from osc_lib.cli import format_columns
30
32
  from osc_lib.cli import parseractions
@@ -53,6 +55,19 @@ DISK_CHOICES = [
53
55
  "iso",
54
56
  "ploop",
55
57
  ]
58
+ # A list of openstacksdk Image object attributes (values) that named
59
+ # differently from actual properties stored by Glance (keys).
60
+ IMAGE_ATTRIBUTES_CUSTOM_NAMES = {
61
+ 'os_hidden': 'is_hidden',
62
+ 'protected': 'is_protected',
63
+ 'os_hash_algo': 'hash_algo',
64
+ 'os_hash_value': 'hash_value',
65
+ 'img_config_drive': 'needs_config_drive',
66
+ 'os_secure_boot': 'needs_secure_boot',
67
+ 'hw_vif_multiqueue_enabled': 'is_hw_vif_multiqueue_enabled',
68
+ 'hw_boot_menu': 'is_hw_boot_menu_enabled',
69
+ 'auto_disk_config': 'has_auto_disk_config',
70
+ }
56
71
  MEMBER_STATUS_CHOICES = ["accepted", "pending", "rejected", "all"]
57
72
 
58
73
  LOG = logging.getLogger(__name__)
@@ -83,6 +98,9 @@ def _format_image(image, human_readable=False):
83
98
  'virtual_size',
84
99
  'min_ram',
85
100
  'schema',
101
+ 'is_hidden',
102
+ 'hash_algo',
103
+ 'hash_value',
86
104
  ]
87
105
 
88
106
  # TODO(gtema/anybody): actually it should be possible to drop this method,
@@ -576,7 +594,7 @@ class CreateImage(command.ShowOne):
576
594
  return _format_image(image)
577
595
 
578
596
  def _take_action_volume(self, parsed_args):
579
- volume_client = self.app.client_manager.volume
597
+ volume_client = self.app.client_manager.sdk_connection.volume
580
598
 
581
599
  unsupported_opts = {
582
600
  # 'name', # 'name' is a positional argument and will always exist
@@ -607,15 +625,14 @@ class CreateImage(command.ShowOne):
607
625
  # version
608
626
  LOG.warning(msg % opt_name)
609
627
 
610
- source_volume = utils.find_resource(
611
- volume_client.volumes,
612
- parsed_args.volume,
628
+ source_volume = volume_client.find_volume(
629
+ parsed_args.volume, ignore_missing=False
613
630
  )
614
631
  kwargs: dict[str, ty.Any] = {
615
632
  'visibility': None,
616
633
  'protected': None,
617
634
  }
618
- if volume_client.api_version < api_versions.APIVersion('3.1'):
635
+ if not sdk_utils.supports_microversion(volume_client, '3.1'):
619
636
  if parsed_args.visibility or parsed_args.is_protected is not None:
620
637
  msg = _(
621
638
  '--os-volume-api-version 3.1 or greater is required '
@@ -627,15 +644,15 @@ class CreateImage(command.ShowOne):
627
644
  kwargs['visibility'] = parsed_args.visibility or 'private'
628
645
  kwargs['protected'] = parsed_args.is_protected or False
629
646
 
630
- response, body = volume_client.volumes.upload_to_image(
647
+ response = volume_client.upload_volume_to_image(
631
648
  source_volume.id,
632
- parsed_args.force,
633
649
  parsed_args.name,
634
- parsed_args.container_format,
635
- parsed_args.disk_format,
650
+ force=parsed_args.force,
651
+ disk_format=parsed_args.disk_format,
652
+ container_format=parsed_args.container_format,
636
653
  **kwargs,
637
654
  )
638
- info = body['os-volume_upload_image']
655
+ info = copy.deepcopy(response)
639
656
  try:
640
657
  info['volume_type'] = info['volume_type']['name']
641
658
  except TypeError:
@@ -889,6 +906,8 @@ class ListImage(command.Lister):
889
906
  'visibility',
890
907
  'is_protected',
891
908
  'owner_id',
909
+ 'hash_algo',
910
+ 'hash_value',
892
911
  'tags',
893
912
  )
894
913
  column_headers: tuple[str, ...] = (
@@ -902,6 +921,8 @@ class ListImage(command.Lister):
902
921
  'Visibility',
903
922
  'Protected',
904
923
  'Project',
924
+ 'Hash Algorithm',
925
+ 'Hash Value',
905
926
  'Tags',
906
927
  )
907
928
  else:
@@ -1052,6 +1073,16 @@ class SaveImage(command.Command):
1052
1073
 
1053
1074
  def get_parser(self, prog_name):
1054
1075
  parser = super().get_parser(prog_name)
1076
+ parser.add_argument(
1077
+ "--chunk-size",
1078
+ type=int,
1079
+ default=1024,
1080
+ metavar="<chunk-size>",
1081
+ help=_(
1082
+ "Size in bytes to read from the wire and buffer at one "
1083
+ "time (default: 1024)"
1084
+ ),
1085
+ )
1055
1086
  parser.add_argument(
1056
1087
  "--file",
1057
1088
  metavar="<filename>",
@@ -1076,7 +1107,12 @@ class SaveImage(command.Command):
1076
1107
  if output_file is None:
1077
1108
  output_file = getattr(sys.stdout, "buffer", sys.stdout)
1078
1109
 
1079
- image_client.download_image(image.id, stream=True, output=output_file)
1110
+ image_client.download_image(
1111
+ image.id,
1112
+ stream=True,
1113
+ output=output_file,
1114
+ chunk_size=parsed_args.chunk_size,
1115
+ )
1080
1116
 
1081
1117
 
1082
1118
  class SetImage(command.Command):
@@ -1477,6 +1513,11 @@ class UnsetImage(command.Command):
1477
1513
  )
1478
1514
  new_props.pop(k, None)
1479
1515
  kwargs['properties'] = new_props
1516
+ elif (
1517
+ k in IMAGE_ATTRIBUTES_CUSTOM_NAMES
1518
+ and IMAGE_ATTRIBUTES_CUSTOM_NAMES[k] in image
1519
+ ):
1520
+ delattr(image, IMAGE_ATTRIBUTES_CUSTOM_NAMES[k])
1480
1521
  else:
1481
1522
  LOG.error(
1482
1523
  _(
@@ -1744,6 +1785,12 @@ class ImportImage(command.ShowOne):
1744
1785
  "'--method=web-download'"
1745
1786
  )
1746
1787
  raise exceptions.CommandError(msg)
1788
+ _parsed = urllib.parse.urlparse(parsed_args.uri)
1789
+ if not all({_parsed.scheme, _parsed.netloc}):
1790
+ msg = _("'%(uri)s' is not a valid url")
1791
+ raise exceptions.CommandError(
1792
+ msg % {'uri': parsed_args.uri},
1793
+ )
1747
1794
  else:
1748
1795
  if parsed_args.uri:
1749
1796
  msg = _(
@@ -123,8 +123,11 @@ class DeleteMetadefObject(command.Command):
123
123
  parser.add_argument(
124
124
  "objects",
125
125
  metavar="<object>",
126
- nargs="+",
127
- help=_("Metadef object(s) to delete (name)"),
126
+ nargs="*",
127
+ help=_(
128
+ "Metadef object(s) to delete (name) "
129
+ "(omit this argument to delete all objects in the namespace)"
130
+ ),
128
131
  )
129
132
  return parser
130
133
 
@@ -133,6 +136,9 @@ class DeleteMetadefObject(command.Command):
133
136
 
134
137
  namespace = parsed_args.namespace
135
138
 
139
+ if not parsed_args.objects:
140
+ return image_client.delete_all_metadef_objects(namespace)
141
+
136
142
  result = 0
137
143
  for obj in parsed_args.objects:
138
144
  try:
@@ -124,14 +124,21 @@ class DeleteMetadefProperty(command.Command):
124
124
  parser.add_argument(
125
125
  "properties",
126
126
  metavar="<property>",
127
- nargs="+",
128
- help=_("Metadef propert(ies) to delete (name)"),
127
+ nargs="*",
128
+ help=_(
129
+ "Metadef properties to delete (name) "
130
+ "(omit this argument to delete all properties in the namespace)"
131
+ ),
129
132
  )
130
133
  return parser
131
134
 
132
135
  def take_action(self, parsed_args):
133
136
  image_client = self.app.client_manager.image
134
137
 
138
+ if not parsed_args.properties:
139
+ image_client.delete_all_metadef_properties(parsed_args.namespace)
140
+ return
141
+
135
142
  result = 0
136
143
  for prop in parsed_args.properties:
137
144
  try:
@@ -27,12 +27,6 @@ API_VERSIONS = ('2.0', '2')
27
27
 
28
28
  def make_client(instance):
29
29
  """Returns a network proxy"""
30
- # NOTE(dtroyer): As of osc-lib 1.8.0 and OpenStackSDK 0.10.0 the
31
- # old Profile interface and separate client creation
32
- # for each API that uses the SDK is unnecessary. This
33
- # callback remains as a remnant of the original plugin
34
- # interface and to avoid the code churn of changing all
35
- # of the existing references.
36
30
  LOG.debug(
37
31
  'Network client initialized using OpenStack SDK: %s',
38
32
  instance.sdk_connection.network,
@@ -243,18 +243,26 @@ class ListFloatingIP(common.NetworkAndComputeLister):
243
243
  parser.add_argument(
244
244
  '--network',
245
245
  metavar='<network>',
246
+ dest='networks',
247
+ action='append',
246
248
  help=self.enhance_help_neutron(
247
249
  _(
248
- "List floating IP(s) according to "
249
- "given network (name or ID)"
250
+ "List floating IP(s) according to given network "
251
+ "(name or ID) "
252
+ "(repeat option to fiter on multiple networks)"
250
253
  )
251
254
  ),
252
255
  )
253
256
  parser.add_argument(
254
257
  '--port',
255
258
  metavar='<port>',
259
+ dest='ports',
260
+ action='append',
256
261
  help=self.enhance_help_neutron(
257
- _("List floating IP(s) according to given port (name or ID)")
262
+ _(
263
+ "List floating IP(s) according to given port (name or ID) "
264
+ "(repeat option to fiter on multiple ports)"
265
+ )
258
266
  ),
259
267
  )
260
268
  parser.add_argument(
@@ -271,14 +279,6 @@ class ListFloatingIP(common.NetworkAndComputeLister):
271
279
  _("List floating IP(s) according to given floating IP address")
272
280
  ),
273
281
  )
274
- parser.add_argument(
275
- '--long',
276
- action='store_true',
277
- default=False,
278
- help=self.enhance_help_neutron(
279
- _("List additional fields in output")
280
- ),
281
- )
282
282
  parser.add_argument(
283
283
  '--status',
284
284
  metavar='<status>',
@@ -295,8 +295,8 @@ class ListFloatingIP(common.NetworkAndComputeLister):
295
295
  metavar='<project>',
296
296
  help=self.enhance_help_neutron(
297
297
  _(
298
- "List floating IP(s) according to given project (name or "
299
- "ID)"
298
+ "List floating IP(s) according to given project "
299
+ "(name or ID) "
300
300
  )
301
301
  ),
302
302
  )
@@ -304,13 +304,27 @@ class ListFloatingIP(common.NetworkAndComputeLister):
304
304
  parser.add_argument(
305
305
  '--router',
306
306
  metavar='<router>',
307
+ dest='routers',
308
+ action='append',
307
309
  help=self.enhance_help_neutron(
308
- _("List floating IP(s) according to given router (name or ID)")
310
+ _(
311
+ "List floating IP(s) according to given router "
312
+ "(name or ID) "
313
+ "(repeat option to fiter on multiple routers)"
314
+ )
309
315
  ),
310
316
  )
311
317
  _tag.add_tag_filtering_option_to_parser(
312
318
  parser, _('floating IP'), enhance_help=self.enhance_help_neutron
313
319
  )
320
+ parser.add_argument(
321
+ '--long',
322
+ action='store_true',
323
+ default=False,
324
+ help=self.enhance_help_neutron(
325
+ _("List additional fields in output")
326
+ ),
327
+ )
314
328
 
315
329
  return parser
316
330
 
@@ -354,22 +368,33 @@ class ListFloatingIP(common.NetworkAndComputeLister):
354
368
 
355
369
  query = {}
356
370
 
357
- if parsed_args.network is not None:
358
- network = network_client.find_network(
359
- parsed_args.network, ignore_missing=False
360
- )
361
- query['floating_network_id'] = network.id
362
- if parsed_args.port is not None:
363
- port = network_client.find_port(
364
- parsed_args.port, ignore_missing=False
365
- )
366
- query['port_id'] = port.id
371
+ if parsed_args.networks is not None:
372
+ network_ids = []
373
+ for network in parsed_args.networks:
374
+ network_id = network_client.find_network(
375
+ network, ignore_missing=False
376
+ ).id
377
+ network_ids.append(network_id)
378
+ query['floating_network_id'] = network_ids
379
+
380
+ if parsed_args.ports is not None:
381
+ port_ids = []
382
+ for port in parsed_args.ports:
383
+ port_id = network_client.find_port(
384
+ port, ignore_missing=False
385
+ ).id
386
+ port_ids.append(port_id)
387
+ query['port_id'] = port_ids
388
+
367
389
  if parsed_args.fixed_ip_address is not None:
368
390
  query['fixed_ip_address'] = parsed_args.fixed_ip_address
391
+
369
392
  if parsed_args.floating_ip_address is not None:
370
393
  query['floating_ip_address'] = parsed_args.floating_ip_address
394
+
371
395
  if parsed_args.status:
372
396
  query['status'] = parsed_args.status
397
+
373
398
  if parsed_args.project is not None:
374
399
  project = identity_common.find_project(
375
400
  identity_client,
@@ -377,11 +402,15 @@ class ListFloatingIP(common.NetworkAndComputeLister):
377
402
  parsed_args.project_domain,
378
403
  )
379
404
  query['project_id'] = project.id
380
- if parsed_args.router is not None:
381
- router = network_client.find_router(
382
- parsed_args.router, ignore_missing=False
383
- )
384
- query['router_id'] = router.id
405
+
406
+ if parsed_args.routers is not None:
407
+ router_ids = []
408
+ for router in parsed_args.routers:
409
+ router_id = network_client.find_router(
410
+ router, ignore_missing=False
411
+ ).id
412
+ router_ids.append(router_id)
413
+ query['router_id'] = router_ids
385
414
 
386
415
  _tag.get_tag_filtering_args(parsed_args, query)
387
416
 
@@ -73,7 +73,7 @@ ACTION_SHOW = 'get'
73
73
 
74
74
 
75
75
  def _get_columns(item):
76
- hidden_columns = ['location', 'tenant_id']
76
+ hidden_columns = ['location', 'name', 'tenant_id']
77
77
  return utils.get_osc_show_columns_for_sdk_resource(
78
78
  item, {}, hidden_columns
79
79
  )
@@ -148,14 +148,6 @@ def _get_attrs(network_client, parsed_args, is_create=False):
148
148
  return attrs
149
149
 
150
150
 
151
- def _get_item_properties(item, fields):
152
- """Return a tuple containing the item properties."""
153
- row = []
154
- for field in fields:
155
- row.append(item.get(field, ''))
156
- return tuple(row)
157
-
158
-
159
151
  def _rule_action_call(client, action, rule_type):
160
152
  rule_type = rule_type.replace('-', '_')
161
153
  func_name = f'{action}_qos_{rule_type}_rule'
@@ -357,10 +349,10 @@ class ListNetworkQosRule(command.Lister):
357
349
  qos = client.find_qos_policy(
358
350
  parsed_args.qos_policy, ignore_missing=False
359
351
  )
360
- data = qos.rules
352
+
361
353
  return (
362
354
  column_headers,
363
- (_get_item_properties(s, columns) for s in data),
355
+ (utils.get_dict_properties(s, columns) for s in qos.rules),
364
356
  )
365
357
 
366
358
 
@@ -1316,6 +1316,18 @@ class UnsetPort(common.NeutronUnsetCommandWithExtraArgs):
1316
1316
  default=False,
1317
1317
  help=_("Clear hints for the port"),
1318
1318
  )
1319
+ parser.add_argument(
1320
+ '--device',
1321
+ action='store_true',
1322
+ default=False,
1323
+ help=_("Clear device ID for the port."),
1324
+ )
1325
+ parser.add_argument(
1326
+ '--device-owner',
1327
+ action='store_true',
1328
+ default=False,
1329
+ help=_("Clear device owner for the port."),
1330
+ )
1319
1331
  _tag.add_tag_option_to_parser_for_unset(parser, _('port'))
1320
1332
  parser.add_argument(
1321
1333
  'port',
@@ -1382,6 +1394,10 @@ class UnsetPort(common.NeutronUnsetCommandWithExtraArgs):
1382
1394
  attrs['binding:host_id'] = None
1383
1395
  if parsed_args.hints:
1384
1396
  attrs['hints'] = None
1397
+ if parsed_args.device:
1398
+ attrs['device_id'] = ''
1399
+ if parsed_args.device_owner:
1400
+ attrs['device_owner'] = ''
1385
1401
 
1386
1402
  attrs.update(
1387
1403
  self._parse_extra_properties(parsed_args.extra_properties)
@@ -76,7 +76,7 @@ def _get_columns(item):
76
76
  }
77
77
  if hasattr(item, 'interfaces_info'):
78
78
  column_map['interfaces_info'] = 'interfaces_info'
79
- invisible_columns = ['location']
79
+ invisible_columns = ['location', 'tenant_id']
80
80
  if item.is_ha is None:
81
81
  invisible_columns.append('is_ha')
82
82
  column_map.pop('is_ha')
@@ -89,9 +89,8 @@ def _get_columns(item):
89
89
  # We still support Nova managed security groups, where we have tenant_id.
90
90
  column_map = {
91
91
  'security_group_rules': 'rules',
92
- 'tenant_id': 'project_id',
93
92
  }
94
- hidden_columns = ['location']
93
+ hidden_columns = ['location', 'tenant_id']
95
94
  return utils.get_osc_show_columns_for_sdk_resource(
96
95
  item, column_map, hidden_columns
97
96
  )
@@ -186,7 +185,8 @@ class CreateSecurityGroup(
186
185
  parsed_args.name,
187
186
  description,
188
187
  )
189
- display_columns, property_columns = _get_columns(obj)
188
+ display_columns = ('description', 'id', 'name', 'project_id', 'rules')
189
+ property_columns = ('description', 'id', 'name', 'tenant_id', 'rules')
190
190
  data = utils.get_dict_properties(
191
191
  obj, property_columns, formatters=_formatters_compute
192
192
  )
@@ -222,7 +222,14 @@ class DeleteSecurityGroup(common.NetworkAndComputeDelete):
222
222
  # the OSC minimum requirements include SDK 1.0.
223
223
  class ListSecurityGroup(common.NetworkAndComputeLister):
224
224
  _description = _("List security groups")
225
- FIELDS_TO_RETRIEVE = ['id', 'name', 'description', 'project_id', 'tags']
225
+ FIELDS_TO_RETRIEVE = [
226
+ 'id',
227
+ 'name',
228
+ 'description',
229
+ 'project_id',
230
+ 'tags',
231
+ 'shared',
232
+ ]
226
233
 
227
234
  def update_parser_network(self, parser):
228
235
  if not self.is_docs_build:
@@ -245,6 +252,23 @@ class ListSecurityGroup(common.NetworkAndComputeLister):
245
252
  identity_common.add_project_domain_option_to_parser(
246
253
  parser, enhance_help=self.enhance_help_neutron
247
254
  )
255
+
256
+ shared_group = parser.add_mutually_exclusive_group()
257
+ shared_group.add_argument(
258
+ '--share',
259
+ action='store_true',
260
+ dest='shared',
261
+ default=None,
262
+ help=_("List security groups shared between projects"),
263
+ )
264
+ shared_group.add_argument(
265
+ '--no-share',
266
+ action='store_false',
267
+ dest='shared',
268
+ default=None,
269
+ help=_("List security groups not shared between projects"),
270
+ )
271
+
248
272
  _tag.add_tag_filtering_option_to_parser(
249
273
  parser, _('security group'), enhance_help=self.enhance_help_neutron
250
274
  )
@@ -272,13 +296,30 @@ class ListSecurityGroup(common.NetworkAndComputeLister):
272
296
  ).id
273
297
  filters['project_id'] = project_id
274
298
 
299
+ if parsed_args.shared is not None:
300
+ filters['shared'] = parsed_args.shared
301
+
275
302
  _tag.get_tag_filtering_args(parsed_args, filters)
276
303
  data = client.security_groups(
277
304
  fields=self.FIELDS_TO_RETRIEVE, **filters
278
305
  )
279
306
 
280
- columns = ("id", "name", "description", "project_id", "tags")
281
- column_headers = ("ID", "Name", "Description", "Project", "Tags")
307
+ columns = (
308
+ "id",
309
+ "name",
310
+ "description",
311
+ "project_id",
312
+ "tags",
313
+ "is_shared",
314
+ )
315
+ column_headers = (
316
+ "ID",
317
+ "Name",
318
+ "Description",
319
+ "Project",
320
+ "Tags",
321
+ "Shared",
322
+ )
282
323
  return (
283
324
  column_headers,
284
325
  (
@@ -420,7 +461,8 @@ class ShowSecurityGroup(common.NetworkAndComputeShowOne):
420
461
 
421
462
  def take_action_compute(self, client, parsed_args):
422
463
  obj = compute_v2.find_security_group(client, parsed_args.group)
423
- display_columns, property_columns = _get_columns(obj)
464
+ display_columns = ('description', 'id', 'name', 'project_id', 'rules')
465
+ property_columns = ('description', 'id', 'name', 'tenant_id', 'rules')
424
466
  data = utils.get_dict_properties(
425
467
  obj, property_columns, formatters=_formatters_compute
426
468
  )
@@ -30,7 +30,7 @@ LOG = logging.getLogger(__name__)
30
30
 
31
31
 
32
32
  def _get_columns(item):
33
- hidden_columns = ['location', 'tenant_id']
33
+ hidden_columns = ['location', 'name', 'tenant_id', 'tags']
34
34
  return utils.get_osc_show_columns_for_sdk_resource(
35
35
  item, {}, hidden_columns
36
36
  )
@@ -427,6 +427,14 @@ class ListSecurityGroupRule(common.NetworkAndComputeLister):
427
427
  _("**Deprecated** This argument is no longer needed")
428
428
  ),
429
429
  )
430
+ parser.add_argument(
431
+ '--project',
432
+ metavar='<project>',
433
+ help=self.enhance_help_neutron(_("Owner's project (name or ID)")),
434
+ )
435
+ identity_common.add_project_domain_option_to_parser(
436
+ parser, enhance_help=self.enhance_help_neutron
437
+ )
430
438
  return parser
431
439
 
432
440
  def update_parser_compute(self, parser):
@@ -503,6 +511,15 @@ class ListSecurityGroupRule(common.NetworkAndComputeLister):
503
511
  query['direction'] = 'egress'
504
512
  if parsed_args.protocol is not None:
505
513
  query['protocol'] = parsed_args.protocol
514
+ if parsed_args.project is not None:
515
+ identity_client = self.app.client_manager.identity
516
+ project_id = identity_common.find_project(
517
+ identity_client,
518
+ parsed_args.project,
519
+ parsed_args.project_domain,
520
+ ).id
521
+ query['tenant_id'] = project_id
522
+ query['project_id'] = project_id
506
523
 
507
524
  rules = [
508
525
  self._format_network_security_group_rule(r)
openstackclient/shell.py CHANGED
@@ -94,7 +94,7 @@ class OpenStackShell(shell.OpenStackShell):
94
94
  # instead.
95
95
  mod_versions = getattr(mod, 'API_VERSIONS', None)
96
96
  if mod_versions is not None and not isinstance(
97
- mod_versions, (dict, tuple)
97
+ mod_versions, dict | tuple
98
98
  ):
99
99
  raise TypeError(
100
100
  f'Plugin {mod} has incompatible API_VERSIONS. '
@@ -97,7 +97,11 @@ class TestCase(testtools.TestCase):
97
97
  )
98
98
 
99
99
  if parse_output:
100
- return json.loads(output)
100
+ try:
101
+ return json.loads(output)
102
+ except json.JSONDecodeError:
103
+ print(f'failed to decode: {output}')
104
+ raise
101
105
  else:
102
106
  return output
103
107