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.
- openstackclient/api/compute_v2.py +2 -2
- openstackclient/api/volume_v2.py +60 -0
- openstackclient/api/volume_v3.py +60 -0
- openstackclient/compute/v2/flavor.py +14 -1
- openstackclient/compute/v2/server.py +1 -3
- openstackclient/identity/common.py +8 -13
- openstackclient/identity/v3/application_credential.py +86 -85
- openstackclient/identity/v3/domain.py +5 -6
- openstackclient/identity/v3/project.py +25 -20
- openstackclient/identity/v3/role.py +7 -2
- openstackclient/image/v1/image.py +16 -1
- openstackclient/image/v2/cache.py +10 -6
- openstackclient/image/v2/image.py +48 -1
- openstackclient/image/v2/metadef_objects.py +8 -2
- openstackclient/image/v2/metadef_properties.py +9 -2
- openstackclient/network/v2/port.py +16 -0
- openstackclient/network/v2/security_group.py +44 -3
- openstackclient/network/v2/security_group_rule.py +17 -0
- openstackclient/tests/functional/identity/v3/test_access_rule.py +1 -1
- openstackclient/tests/functional/identity/v3/test_application_credential.py +7 -7
- openstackclient/tests/functional/image/v2/test_image.py +36 -14
- openstackclient/tests/functional/volume/v2/test_volume.py +1 -1
- openstackclient/tests/functional/volume/v3/test_volume.py +2 -2
- openstackclient/tests/unit/api/test_volume_v2.py +124 -0
- openstackclient/tests/unit/api/test_volume_v3.py +124 -0
- openstackclient/tests/unit/compute/v2/test_flavor.py +159 -174
- openstackclient/tests/unit/compute/v2/test_server.py +42 -51
- openstackclient/tests/unit/identity/v3/test_application_credential.py +47 -41
- openstackclient/tests/unit/identity/v3/test_domain.py +2 -2
- openstackclient/tests/unit/identity/v3/test_project.py +30 -53
- openstackclient/tests/unit/identity/v3/test_role.py +2 -8
- openstackclient/tests/unit/image/v1/test_image.py +47 -0
- openstackclient/tests/unit/image/v2/test_image.py +79 -9
- openstackclient/tests/unit/image/v2/test_metadef_objects.py +22 -0
- openstackclient/tests/unit/image/v2/test_metadef_properties.py +24 -10
- openstackclient/tests/unit/network/v2/fakes.py +1 -0
- openstackclient/tests/unit/network/v2/test_ndp_proxy.py +2 -2
- openstackclient/tests/unit/network/v2/test_port.py +40 -0
- openstackclient/tests/unit/network/v2/test_security_group_network.py +6 -0
- openstackclient/tests/unit/network/v2/test_security_group_rule_network.py +49 -0
- openstackclient/tests/unit/volume/v2/test_volume.py +358 -305
- openstackclient/tests/unit/volume/v3/test_volume.py +439 -415
- openstackclient/volume/v2/service.py +1 -1
- openstackclient/volume/v2/volume.py +78 -52
- openstackclient/volume/v3/service.py +1 -1
- openstackclient/volume/v3/volume.py +102 -75
- openstackclient/volume/v3/volume_group.py +1 -1
- {python_openstackclient-8.1.0.dist-info → python_openstackclient-8.2.0.dist-info}/AUTHORS +5 -0
- {python_openstackclient-8.1.0.dist-info → python_openstackclient-8.2.0.dist-info}/METADATA +7 -7
- {python_openstackclient-8.1.0.dist-info → python_openstackclient-8.2.0.dist-info}/RECORD +55 -51
- python_openstackclient-8.2.0.dist-info/pbr.json +1 -0
- python_openstackclient-8.1.0.dist-info/pbr.json +0 -1
- {python_openstackclient-8.1.0.dist-info → python_openstackclient-8.2.0.dist-info}/LICENSE +0 -0
- {python_openstackclient-8.1.0.dist-info → python_openstackclient-8.2.0.dist-info}/WHEEL +0 -0
- {python_openstackclient-8.1.0.dist-info → python_openstackclient-8.2.0.dist-info}/entry_points.txt +0 -0
- {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(
|
|
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=_(
|
|
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=_(
|
|
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 = [
|
|
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 = (
|
|
281
|
-
|
|
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['
|
|
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
|
-
'
|
|
25
|
-
'
|
|
26
|
-
'
|
|
27
|
-
'
|
|
28
|
-
'
|
|
29
|
-
'
|
|
30
|
-
'
|
|
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
|
-
|
|
222
|
-
#
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
#
|
|
233
|
-
|
|
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
|
+
)
|
|
@@ -124,7 +124,7 @@ class VolumeTests(common.BaseVolumeTests):
|
|
|
124
124
|
cmd_output["properties"],
|
|
125
125
|
)
|
|
126
126
|
self.assertEqual(
|
|
127
|
-
|
|
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
|
-
|
|
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
|
+
)
|