python-openstackclient 8.1.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 (56) 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/v2/flavor.py +14 -1
  5. openstackclient/compute/v2/server.py +1 -3
  6. openstackclient/identity/common.py +8 -13
  7. openstackclient/identity/v3/application_credential.py +86 -85
  8. openstackclient/identity/v3/domain.py +5 -6
  9. openstackclient/identity/v3/project.py +25 -20
  10. openstackclient/identity/v3/role.py +7 -2
  11. openstackclient/image/v1/image.py +16 -1
  12. openstackclient/image/v2/cache.py +10 -6
  13. openstackclient/image/v2/image.py +48 -1
  14. openstackclient/image/v2/metadef_objects.py +8 -2
  15. openstackclient/image/v2/metadef_properties.py +9 -2
  16. openstackclient/network/v2/port.py +16 -0
  17. openstackclient/network/v2/security_group.py +44 -3
  18. openstackclient/network/v2/security_group_rule.py +17 -0
  19. openstackclient/tests/functional/identity/v3/test_access_rule.py +1 -1
  20. openstackclient/tests/functional/identity/v3/test_application_credential.py +7 -7
  21. openstackclient/tests/functional/image/v2/test_image.py +36 -14
  22. openstackclient/tests/functional/volume/v2/test_volume.py +1 -1
  23. openstackclient/tests/functional/volume/v3/test_volume.py +2 -2
  24. openstackclient/tests/unit/api/test_volume_v2.py +124 -0
  25. openstackclient/tests/unit/api/test_volume_v3.py +124 -0
  26. openstackclient/tests/unit/compute/v2/test_flavor.py +159 -174
  27. openstackclient/tests/unit/compute/v2/test_server.py +42 -51
  28. openstackclient/tests/unit/identity/v3/test_application_credential.py +47 -41
  29. openstackclient/tests/unit/identity/v3/test_domain.py +2 -2
  30. openstackclient/tests/unit/identity/v3/test_project.py +30 -53
  31. openstackclient/tests/unit/identity/v3/test_role.py +2 -8
  32. openstackclient/tests/unit/image/v1/test_image.py +47 -0
  33. openstackclient/tests/unit/image/v2/test_image.py +79 -9
  34. openstackclient/tests/unit/image/v2/test_metadef_objects.py +22 -0
  35. openstackclient/tests/unit/image/v2/test_metadef_properties.py +24 -10
  36. openstackclient/tests/unit/network/v2/fakes.py +1 -0
  37. openstackclient/tests/unit/network/v2/test_ndp_proxy.py +2 -2
  38. openstackclient/tests/unit/network/v2/test_port.py +40 -0
  39. openstackclient/tests/unit/network/v2/test_security_group_network.py +6 -0
  40. openstackclient/tests/unit/network/v2/test_security_group_rule_network.py +49 -0
  41. openstackclient/tests/unit/volume/v2/test_volume.py +358 -305
  42. openstackclient/tests/unit/volume/v3/test_volume.py +439 -415
  43. openstackclient/volume/v2/service.py +1 -1
  44. openstackclient/volume/v2/volume.py +78 -52
  45. openstackclient/volume/v3/service.py +1 -1
  46. openstackclient/volume/v3/volume.py +102 -75
  47. openstackclient/volume/v3/volume_group.py +1 -1
  48. {python_openstackclient-8.1.0.dist-info → python_openstackclient-8.2.0.dist-info}/AUTHORS +5 -0
  49. {python_openstackclient-8.1.0.dist-info → python_openstackclient-8.2.0.dist-info}/METADATA +7 -7
  50. {python_openstackclient-8.1.0.dist-info → python_openstackclient-8.2.0.dist-info}/RECORD +55 -51
  51. python_openstackclient-8.2.0.dist-info/pbr.json +1 -0
  52. python_openstackclient-8.1.0.dist-info/pbr.json +0 -1
  53. {python_openstackclient-8.1.0.dist-info → python_openstackclient-8.2.0.dist-info}/LICENSE +0 -0
  54. {python_openstackclient-8.1.0.dist-info → python_openstackclient-8.2.0.dist-info}/WHEEL +0 -0
  55. {python_openstackclient-8.1.0.dist-info → python_openstackclient-8.2.0.dist-info}/entry_points.txt +0 -0
  56. {python_openstackclient-8.1.0.dist-info → python_openstackclient-8.2.0.dist-info}/top_level.txt +0 -0
@@ -22,6 +22,7 @@ import logging
22
22
  import os
23
23
  import sys
24
24
  import typing as ty
25
+ import urllib.parse
25
26
 
26
27
  from openstack import exceptions as sdk_exceptions
27
28
  from openstack.image import image_signer
@@ -54,6 +55,19 @@ DISK_CHOICES = [
54
55
  "iso",
55
56
  "ploop",
56
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
+ }
57
71
  MEMBER_STATUS_CHOICES = ["accepted", "pending", "rejected", "all"]
58
72
 
59
73
  LOG = logging.getLogger(__name__)
@@ -84,6 +98,9 @@ def _format_image(image, human_readable=False):
84
98
  'virtual_size',
85
99
  'min_ram',
86
100
  'schema',
101
+ 'is_hidden',
102
+ 'hash_algo',
103
+ 'hash_value',
87
104
  ]
88
105
 
89
106
  # TODO(gtema/anybody): actually it should be possible to drop this method,
@@ -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:
@@ -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)
@@ -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
  (
@@ -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)
@@ -62,7 +62,7 @@ class AccessRuleTests(common.IdentityTests):
62
62
 
63
63
  items = self.parse_show_as_object(raw_output)
64
64
  self.access_rule_ids = [
65
- x['id'] for x in ast.literal_eval(items['access_rules'])
65
+ x['id'] for x in ast.literal_eval(items['Access Rules'])
66
66
  ]
67
67
  self.addCleanup(
68
68
  self.openstack,
@@ -21,13 +21,13 @@ from openstackclient.tests.functional.identity.v3 import common
21
21
 
22
22
  class ApplicationCredentialTests(common.IdentityTests):
23
23
  APPLICATION_CREDENTIAL_FIELDS = [
24
- 'id',
25
- 'name',
26
- 'project_id',
27
- 'description',
28
- 'roles',
29
- 'expires_at',
30
- 'unrestricted',
24
+ 'ID',
25
+ 'Name',
26
+ 'Project ID',
27
+ 'Description',
28
+ 'Roles',
29
+ 'Expires At',
30
+ 'Unrestricted',
31
31
  ]
32
32
  APPLICATION_CREDENTIAL_LIST_HEADERS = [
33
33
  'ID',
@@ -218,17 +218,39 @@ class ImageTests(base.BaseImageTests):
218
218
  'image remove project ' + self.name + ' ' + my_project_id
219
219
  )
220
220
 
221
- # else:
222
- # # Test not shared
223
- # self.assertRaises(
224
- # image_exceptions.HTTPForbidden,
225
- # self.openstack,
226
- # 'image add project ' +
227
- # self.name + ' ' +
228
- # my_project_id
229
- # )
230
- # self.openstack(
231
- # 'image set ' +
232
- # '--share ' +
233
- # self.name
234
- # )
221
+ def test_image_hidden(self):
222
+ # Test image is shown in list
223
+ output = self.openstack(
224
+ 'image list',
225
+ parse_output=True,
226
+ )
227
+ self.assertIn(
228
+ self.name,
229
+ [img['Name'] for img in output],
230
+ )
231
+
232
+ # Hide the image and test image not show in the list
233
+ self.openstack('image set ' + '--hidden ' + self.name)
234
+ output = self.openstack(
235
+ 'image list',
236
+ parse_output=True,
237
+ )
238
+ self.assertNotIn(self.name, [img['Name'] for img in output])
239
+
240
+ # Test image show in the list with flag
241
+ output = self.openstack(
242
+ 'image list',
243
+ parse_output=True,
244
+ )
245
+ self.assertNotIn(self.name, [img['Name'] for img in output])
246
+
247
+ # Unhide the image and test image is again visible in regular list
248
+ self.openstack('image set ' + '--unhidden ' + self.name)
249
+ output = self.openstack(
250
+ 'image list',
251
+ parse_output=True,
252
+ )
253
+ self.assertIn(
254
+ self.name,
255
+ [img['Name'] for img in output],
256
+ )
@@ -171,7 +171,7 @@ class VolumeTests(common.BaseVolumeTests):
171
171
  cmd_output["volume_image_metadata"],
172
172
  )
173
173
  self.assertEqual(
174
- 'true',
174
+ True,
175
175
  cmd_output["bootable"],
176
176
  )
177
177
 
@@ -124,7 +124,7 @@ class VolumeTests(common.BaseVolumeTests):
124
124
  cmd_output["properties"],
125
125
  )
126
126
  self.assertEqual(
127
- 'false',
127
+ False,
128
128
  cmd_output["bootable"],
129
129
  )
130
130
  self.wait_for_status("volume", name, "available")
@@ -172,7 +172,7 @@ class VolumeTests(common.BaseVolumeTests):
172
172
  cmd_output["volume_image_metadata"],
173
173
  )
174
174
  self.assertEqual(
175
- 'true',
175
+ True,
176
176
  cmd_output["bootable"],
177
177
  )
178
178
 
@@ -0,0 +1,124 @@
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
+ """Volume v2 API Library Tests"""
15
+
16
+ import http
17
+ from unittest import mock
18
+ import uuid
19
+
20
+ from openstack.block_storage.v2 import _proxy
21
+ from osc_lib import exceptions as osc_lib_exceptions
22
+
23
+ from openstackclient.api import volume_v2 as volume
24
+ from openstackclient.tests.unit import fakes
25
+ from openstackclient.tests.unit import utils
26
+
27
+
28
+ class TestConsistencyGroup(utils.TestCase):
29
+ def setUp(self):
30
+ super().setUp()
31
+
32
+ self.volume_sdk_client = mock.Mock(_proxy.Proxy)
33
+
34
+ def test_find_consistency_group_by_id(self):
35
+ cg_id = uuid.uuid4().hex
36
+ cg_name = 'name-' + uuid.uuid4().hex
37
+ data = {
38
+ 'consistencygroup': {
39
+ 'id': cg_id,
40
+ 'name': cg_name,
41
+ 'status': 'available',
42
+ 'availability_zone': 'az1',
43
+ 'created_at': '2015-09-16T09:28:52.000000',
44
+ 'description': 'description-' + uuid.uuid4().hex,
45
+ 'volume_types': ['123456'],
46
+ }
47
+ }
48
+ self.volume_sdk_client.get.side_effect = [
49
+ fakes.FakeResponse(data=data),
50
+ ]
51
+
52
+ result = volume.find_consistency_group(self.volume_sdk_client, cg_id)
53
+
54
+ self.volume_sdk_client.get.assert_has_calls(
55
+ [
56
+ mock.call(f'/consistencygroups/{cg_id}'),
57
+ ]
58
+ )
59
+ self.assertEqual(data['consistencygroup'], result)
60
+
61
+ def test_find_consistency_group_by_name(self):
62
+ cg_id = uuid.uuid4().hex
63
+ cg_name = 'name-' + uuid.uuid4().hex
64
+ data = {
65
+ 'consistencygroups': [
66
+ {
67
+ 'id': cg_id,
68
+ 'name': cg_name,
69
+ }
70
+ ],
71
+ }
72
+ self.volume_sdk_client.get.side_effect = [
73
+ fakes.FakeResponse(status_code=http.HTTPStatus.NOT_FOUND),
74
+ fakes.FakeResponse(data=data),
75
+ ]
76
+
77
+ result = volume.find_consistency_group(self.volume_sdk_client, cg_name)
78
+
79
+ self.volume_sdk_client.get.assert_has_calls(
80
+ [
81
+ mock.call(f'/consistencygroups/{cg_name}'),
82
+ mock.call('/consistencygroups'),
83
+ ]
84
+ )
85
+ self.assertEqual(data['consistencygroups'][0], result)
86
+
87
+ def test_find_consistency_group_not_found(self):
88
+ data = {'consistencygroups': []}
89
+ self.volume_sdk_client.get.side_effect = [
90
+ fakes.FakeResponse(status_code=http.HTTPStatus.NOT_FOUND),
91
+ fakes.FakeResponse(data=data),
92
+ ]
93
+ self.assertRaises(
94
+ osc_lib_exceptions.NotFound,
95
+ volume.find_consistency_group,
96
+ self.volume_sdk_client,
97
+ 'invalid-cg',
98
+ )
99
+
100
+ def test_find_consistency_group_by_name_duplicate(self):
101
+ cg_name = 'name-' + uuid.uuid4().hex
102
+ data = {
103
+ 'consistencygroups': [
104
+ {
105
+ 'id': uuid.uuid4().hex,
106
+ 'name': cg_name,
107
+ },
108
+ {
109
+ 'id': uuid.uuid4().hex,
110
+ 'name': cg_name,
111
+ },
112
+ ],
113
+ }
114
+ self.volume_sdk_client.get.side_effect = [
115
+ fakes.FakeResponse(status_code=http.HTTPStatus.NOT_FOUND),
116
+ fakes.FakeResponse(data=data),
117
+ ]
118
+
119
+ self.assertRaises(
120
+ osc_lib_exceptions.NotFound,
121
+ volume.find_consistency_group,
122
+ self.volume_sdk_client,
123
+ cg_name,
124
+ )
@@ -0,0 +1,124 @@
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
+ """Volume v3 API Library Tests"""
15
+
16
+ import http
17
+ from unittest import mock
18
+ import uuid
19
+
20
+ from openstack.block_storage.v3 import _proxy
21
+ from osc_lib import exceptions as osc_lib_exceptions
22
+
23
+ from openstackclient.api import volume_v3 as volume
24
+ from openstackclient.tests.unit import fakes
25
+ from openstackclient.tests.unit import utils
26
+
27
+
28
+ class TestConsistencyGroup(utils.TestCase):
29
+ def setUp(self):
30
+ super().setUp()
31
+
32
+ self.volume_sdk_client = mock.Mock(_proxy.Proxy)
33
+
34
+ def test_find_consistency_group_by_id(self):
35
+ cg_id = uuid.uuid4().hex
36
+ cg_name = 'name-' + uuid.uuid4().hex
37
+ data = {
38
+ 'consistencygroup': {
39
+ 'id': cg_id,
40
+ 'name': cg_name,
41
+ 'status': 'available',
42
+ 'availability_zone': 'az1',
43
+ 'created_at': '2015-09-16T09:28:52.000000',
44
+ 'description': 'description-' + uuid.uuid4().hex,
45
+ 'volume_types': ['123456'],
46
+ }
47
+ }
48
+ self.volume_sdk_client.get.side_effect = [
49
+ fakes.FakeResponse(data=data),
50
+ ]
51
+
52
+ result = volume.find_consistency_group(self.volume_sdk_client, cg_id)
53
+
54
+ self.volume_sdk_client.get.assert_has_calls(
55
+ [
56
+ mock.call(f'/consistencygroups/{cg_id}'),
57
+ ]
58
+ )
59
+ self.assertEqual(data['consistencygroup'], result)
60
+
61
+ def test_find_consistency_group_by_name(self):
62
+ cg_id = uuid.uuid4().hex
63
+ cg_name = 'name-' + uuid.uuid4().hex
64
+ data = {
65
+ 'consistencygroups': [
66
+ {
67
+ 'id': cg_id,
68
+ 'name': cg_name,
69
+ }
70
+ ],
71
+ }
72
+ self.volume_sdk_client.get.side_effect = [
73
+ fakes.FakeResponse(status_code=http.HTTPStatus.NOT_FOUND),
74
+ fakes.FakeResponse(data=data),
75
+ ]
76
+
77
+ result = volume.find_consistency_group(self.volume_sdk_client, cg_name)
78
+
79
+ self.volume_sdk_client.get.assert_has_calls(
80
+ [
81
+ mock.call(f'/consistencygroups/{cg_name}'),
82
+ mock.call('/consistencygroups'),
83
+ ]
84
+ )
85
+ self.assertEqual(data['consistencygroups'][0], result)
86
+
87
+ def test_find_consistency_group_not_found(self):
88
+ data = {'consistencygroups': []}
89
+ self.volume_sdk_client.get.side_effect = [
90
+ fakes.FakeResponse(status_code=http.HTTPStatus.NOT_FOUND),
91
+ fakes.FakeResponse(data=data),
92
+ ]
93
+ self.assertRaises(
94
+ osc_lib_exceptions.NotFound,
95
+ volume.find_consistency_group,
96
+ self.volume_sdk_client,
97
+ 'invalid-cg',
98
+ )
99
+
100
+ def test_find_consistency_group_by_name_duplicate(self):
101
+ cg_name = 'name-' + uuid.uuid4().hex
102
+ data = {
103
+ 'consistencygroups': [
104
+ {
105
+ 'id': uuid.uuid4().hex,
106
+ 'name': cg_name,
107
+ },
108
+ {
109
+ 'id': uuid.uuid4().hex,
110
+ 'name': cg_name,
111
+ },
112
+ ],
113
+ }
114
+ self.volume_sdk_client.get.side_effect = [
115
+ fakes.FakeResponse(status_code=http.HTTPStatus.NOT_FOUND),
116
+ fakes.FakeResponse(data=data),
117
+ ]
118
+
119
+ self.assertRaises(
120
+ osc_lib_exceptions.NotFound,
121
+ volume.find_consistency_group,
122
+ self.volume_sdk_client,
123
+ cg_name,
124
+ )