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
@@ -64,7 +64,7 @@ def list_security_groups(compute_client, all_projects=None):
64
64
 
65
65
 
66
66
  def find_security_group(compute_client, name_or_id):
67
- """Find the name for a given security group name or ID
67
+ """Find the security group for a given name or ID
68
68
 
69
69
  https://docs.openstack.org/api-ref/compute/#show-security-group-details
70
70
 
@@ -240,7 +240,7 @@ def list_networks(compute_client):
240
240
 
241
241
 
242
242
  def find_network(compute_client, name_or_id):
243
- """Find the ID for a given network name or ID
243
+ """Find the network for a given name or ID
244
244
 
245
245
  https://docs.openstack.org/api-ref/compute/#show-network-details
246
246
 
@@ -0,0 +1,60 @@
1
+ # Licensed under the Apache License, Version 2.0 (the "License"); you may
2
+ # not use this file except in compliance with the License. You may obtain
3
+ # a copy of the License at
4
+ #
5
+ # http://www.apache.org/licenses/LICENSE-2.0
6
+ #
7
+ # Unless required by applicable law or agreed to in writing, software
8
+ # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9
+ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10
+ # License for the specific language governing permissions and limitations
11
+ # under the License.
12
+
13
+ """Volume v2 API Library
14
+
15
+ A collection of wrappers for deprecated Block Storage v2 APIs that are not
16
+ intentionally supported by SDK.
17
+ """
18
+
19
+ import http
20
+
21
+ from openstack import exceptions as sdk_exceptions
22
+ from osc_lib import exceptions
23
+
24
+
25
+ # consistency groups
26
+
27
+
28
+ def find_consistency_group(compute_client, name_or_id):
29
+ """Find the consistency group for a given name or ID
30
+
31
+ https://docs.openstack.org/api-ref/block-storage/v3/#show-a-consistency-group-s-details
32
+
33
+ :param volume_client: A volume client
34
+ :param name_or_id: The name or ID of the consistency group to look up
35
+ :returns: A consistency group object
36
+ :raises exception.NotFound: If a matching consistency group could not be
37
+ found or more than one match was found
38
+ """
39
+ response = compute_client.get(f'/consistencygroups/{name_or_id}')
40
+ if response.status_code != http.HTTPStatus.NOT_FOUND:
41
+ # there might be other, non-404 errors
42
+ sdk_exceptions.raise_from_response(response)
43
+ return response.json()['consistencygroup']
44
+
45
+ response = compute_client.get('/consistencygroups')
46
+ sdk_exceptions.raise_from_response(response)
47
+ found = None
48
+ consistency_groups = response.json()['consistencygroups']
49
+ for consistency_group in consistency_groups:
50
+ if consistency_group['name'] == name_or_id:
51
+ if found:
52
+ raise exceptions.NotFound(
53
+ f'multiple matches found for {name_or_id}'
54
+ )
55
+ found = consistency_group
56
+
57
+ if not found:
58
+ raise exceptions.NotFound(f'{name_or_id} not found')
59
+
60
+ return found
@@ -0,0 +1,60 @@
1
+ # Licensed under the Apache License, Version 2.0 (the "License"); you may
2
+ # not use this file except in compliance with the License. You may obtain
3
+ # a copy of the License at
4
+ #
5
+ # http://www.apache.org/licenses/LICENSE-2.0
6
+ #
7
+ # Unless required by applicable law or agreed to in writing, software
8
+ # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9
+ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10
+ # License for the specific language governing permissions and limitations
11
+ # under the License.
12
+
13
+ """Volume v3 API Library
14
+
15
+ A collection of wrappers for deprecated Block Storage v3 APIs that are not
16
+ intentionally supported by SDK.
17
+ """
18
+
19
+ import http
20
+
21
+ from openstack import exceptions as sdk_exceptions
22
+ from osc_lib import exceptions
23
+
24
+
25
+ # consistency groups
26
+
27
+
28
+ def find_consistency_group(compute_client, name_or_id):
29
+ """Find the consistency group for a given name or ID
30
+
31
+ https://docs.openstack.org/api-ref/block-storage/v3/#show-a-consistency-group-s-details
32
+
33
+ :param volume_client: A volume client
34
+ :param name_or_id: The name or ID of the consistency group to look up
35
+ :returns: A consistency group object
36
+ :raises exception.NotFound: If a matching consistency group could not be
37
+ found or more than one match was found
38
+ """
39
+ response = compute_client.get(f'/consistencygroups/{name_or_id}')
40
+ if response.status_code != http.HTTPStatus.NOT_FOUND:
41
+ # there might be other, non-404 errors
42
+ sdk_exceptions.raise_from_response(response)
43
+ return response.json()['consistencygroup']
44
+
45
+ response = compute_client.get('/consistencygroups')
46
+ sdk_exceptions.raise_from_response(response)
47
+ found = None
48
+ consistency_groups = response.json()['consistencygroups']
49
+ for consistency_group in consistency_groups:
50
+ if consistency_group['name'] == name_or_id:
51
+ if found:
52
+ raise exceptions.NotFound(
53
+ f'multiple matches found for {name_or_id}'
54
+ )
55
+ found = consistency_group
56
+
57
+ if not found:
58
+ raise exceptions.NotFound(f'{name_or_id} not found')
59
+
60
+ return found
@@ -156,12 +156,25 @@ class CreateFlavor(command.ShowOne):
156
156
  msg = _("--project is only allowed with --private")
157
157
  raise exceptions.CommandError(msg)
158
158
 
159
+ flavor_id = parsed_args.id
160
+ if parsed_args.id == 'auto':
161
+ # novaclient aliased 'auto' to mean "generate a UUID for me": we
162
+ # do the same to avoid breaking existing users
163
+ flavor_id = None
164
+
165
+ msg = _(
166
+ "Passing '--id auto' is deprecated. Nova will automatically "
167
+ "assign a UUID-like ID if no ID is provided. Omit the '--id' "
168
+ "parameter instead."
169
+ )
170
+ self.log.warning(msg)
171
+
159
172
  args = {
160
173
  'name': parsed_args.name,
161
174
  'ram': parsed_args.ram,
162
175
  'vcpus': parsed_args.vcpus,
163
176
  'disk': parsed_args.disk,
164
- 'id': parsed_args.id,
177
+ 'id': flavor_id,
165
178
  'ephemeral': parsed_args.ephemeral,
166
179
  'swap': parsed_args.swap,
167
180
  'rxtx_factor': parsed_args.rxtx_factor,
@@ -2129,13 +2129,11 @@ class CreateServer(command.ShowOne):
2129
2129
  f.close()
2130
2130
 
2131
2131
  if parsed_args.wait:
2132
- if utils.wait_for_status(
2132
+ if not utils.wait_for_status(
2133
2133
  compute_client.get_server,
2134
2134
  server.id,
2135
2135
  callback=_show_progress,
2136
2136
  ):
2137
- self.app.stdout.write('\n')
2138
- else:
2139
2137
  msg = _('Error creating server: %s') % parsed_args.server_name
2140
2138
  raise exceptions.CommandError(msg)
2141
2139
 
@@ -348,15 +348,6 @@ def _find_sdk_id(
348
348
  return resource.id
349
349
 
350
350
 
351
- def get_immutable_options(parsed_args):
352
- options = {}
353
- if parsed_args.immutable:
354
- options['immutable'] = True
355
- if parsed_args.no_immutable:
356
- options['immutable'] = False
357
- return options
358
-
359
-
360
351
  def add_user_domain_option_to_parser(parser):
361
352
  parser.add_argument(
362
353
  '--user-domain',
@@ -419,17 +410,21 @@ def add_inherited_option_to_parser(parser):
419
410
 
420
411
 
421
412
  def add_resource_option_to_parser(parser):
422
- enable_group = parser.add_mutually_exclusive_group()
423
- enable_group.add_argument(
413
+ immutable_group = parser.add_mutually_exclusive_group()
414
+ immutable_group.add_argument(
424
415
  '--immutable',
425
416
  action='store_true',
417
+ dest='immutable',
418
+ default=None,
426
419
  help=_(
427
420
  'Make resource immutable. An immutable project may not '
428
421
  'be deleted or modified except to remove the immutable flag'
429
422
  ),
430
423
  )
431
- enable_group.add_argument(
424
+ immutable_group.add_argument(
432
425
  '--no-immutable',
433
- action='store_true',
426
+ action='store_false',
427
+ dest='immutable',
428
+ default=None,
434
429
  help=_('Make resource mutable (default)'),
435
430
  )
@@ -20,6 +20,7 @@ import json
20
20
  import logging
21
21
  import uuid
22
22
 
23
+ from cliff import columns as cliff_columns
23
24
  from osc_lib.command import command
24
25
  from osc_lib import exceptions
25
26
  from osc_lib import utils
@@ -27,10 +28,84 @@ from osc_lib import utils
27
28
  from openstackclient.i18n import _
28
29
  from openstackclient.identity import common
29
30
 
30
-
31
31
  LOG = logging.getLogger(__name__)
32
32
 
33
33
 
34
+ class RolesColumn(cliff_columns.FormattableColumn):
35
+ """Generate a formatted string of role names."""
36
+
37
+ def human_readable(self):
38
+ return utils.format_list(r['name'] for r in self._value)
39
+
40
+
41
+ def _format_application_credential(
42
+ application_credential, *, include_secret=False
43
+ ):
44
+ column_headers: tuple[str, ...] = (
45
+ 'ID',
46
+ 'Name',
47
+ 'Description',
48
+ 'Project ID',
49
+ 'Roles',
50
+ 'Unrestricted',
51
+ 'Access Rules',
52
+ 'Expires At',
53
+ )
54
+ columns: tuple[str, ...] = (
55
+ 'id',
56
+ 'name',
57
+ 'description',
58
+ 'project_id',
59
+ 'roles',
60
+ 'unrestricted',
61
+ 'access_rules',
62
+ 'expires_at',
63
+ )
64
+ if include_secret:
65
+ column_headers += ('Secret',)
66
+ columns += ('secret',)
67
+
68
+ return (
69
+ column_headers,
70
+ utils.get_item_properties(
71
+ application_credential, columns, formatters={'roles': RolesColumn}
72
+ ),
73
+ )
74
+
75
+
76
+ def _format_application_credentials(application_credentials):
77
+ column_headers = (
78
+ 'ID',
79
+ 'Name',
80
+ 'Description',
81
+ 'Project ID',
82
+ 'Roles',
83
+ 'Unrestricted',
84
+ 'Access Rules',
85
+ 'Expires At',
86
+ )
87
+ columns = (
88
+ 'id',
89
+ 'name',
90
+ 'description',
91
+ 'project_id',
92
+ 'roles',
93
+ 'unrestricted',
94
+ 'access_rules',
95
+ 'expires_at',
96
+ )
97
+
98
+ return (
99
+ column_headers,
100
+ (
101
+ utils.get_item_properties(
102
+ x, columns, formatters={'roles': RolesColumn}
103
+ )
104
+ for x in application_credentials
105
+ ),
106
+ )
107
+
108
+
34
109
  # TODO(stephenfin): Move this to osc_lib since it's useful elsewhere
35
110
  def is_uuid_like(value) -> bool:
36
111
  """Returns validation of a value as a UUID.
@@ -38,9 +113,6 @@ def is_uuid_like(value) -> bool:
38
113
  :param val: Value to verify
39
114
  :type val: string
40
115
  :returns: bool
41
-
42
- .. versionchanged:: 1.1.1
43
- Support non-lowercase UUIDs.
44
116
  """
45
117
  try:
46
118
  formatted_value = (
@@ -179,31 +251,8 @@ class CreateApplicationCredential(command.ShowOne):
179
251
  access_rules=access_rules,
180
252
  )
181
253
 
182
- # Format roles into something sensible
183
- if application_credential['roles']:
184
- roles = application_credential['roles']
185
- msg = ' '.join(r['name'] for r in roles)
186
- application_credential['roles'] = msg
187
-
188
- columns = (
189
- 'id',
190
- 'name',
191
- 'description',
192
- 'project_id',
193
- 'roles',
194
- 'unrestricted',
195
- 'access_rules',
196
- 'expires_at',
197
- 'secret',
198
- )
199
- return (
200
- columns,
201
- (
202
- utils.get_dict_properties(
203
- application_credential,
204
- columns,
205
- )
206
- ),
254
+ return _format_application_credential(
255
+ application_credential, include_secret=True
207
256
  )
208
257
 
209
258
 
@@ -252,6 +301,8 @@ class DeleteApplicationCredential(command.Command):
252
301
  ) % {'errors': errors, 'total': total}
253
302
  raise exceptions.CommandError(msg)
254
303
 
304
+ return None
305
+
255
306
 
256
307
  class ListApplicationCredential(command.Lister):
257
308
  _description = _("List application credentials")
@@ -276,39 +327,12 @@ class ListApplicationCredential(command.Lister):
276
327
  conn = self.app.client_manager.sdk_connection
277
328
  user_id = conn.config.get_auth().get_user_id(conn.identity)
278
329
 
279
- data = identity_client.application_credentials(user=user_id)
280
-
281
- data_formatted = []
282
- for ac in data:
283
- # Format roles into something sensible
284
- roles = ac['roles']
285
- msg = ' '.join(r['name'] for r in roles)
286
- ac['roles'] = msg
287
-
288
- data_formatted.append(ac)
289
-
290
- columns = (
291
- 'ID',
292
- 'Name',
293
- 'Description',
294
- 'Project ID',
295
- 'Roles',
296
- 'Unrestricted',
297
- 'Access Rules',
298
- 'Expires At',
299
- )
300
- return (
301
- columns,
302
- (
303
- utils.get_item_properties(
304
- s,
305
- columns,
306
- formatters={},
307
- )
308
- for s in data_formatted
309
- ),
330
+ application_credentials = identity_client.application_credentials(
331
+ user=user_id
310
332
  )
311
333
 
334
+ return _format_application_credentials(application_credentials)
335
+
312
336
 
313
337
  class ShowApplicationCredential(command.ShowOne):
314
338
  _description = _("Display application credential details")
@@ -327,31 +351,8 @@ class ShowApplicationCredential(command.ShowOne):
327
351
  conn = self.app.client_manager.sdk_connection
328
352
  user_id = conn.config.get_auth().get_user_id(conn.identity)
329
353
 
330
- app_cred = identity_client.find_application_credential(
354
+ application_credential = identity_client.find_application_credential(
331
355
  user_id, parsed_args.application_credential
332
356
  )
333
357
 
334
- # Format roles into something sensible
335
- roles = app_cred['roles']
336
- msg = ' '.join(r['name'] for r in roles)
337
- app_cred['roles'] = msg
338
-
339
- columns = (
340
- 'id',
341
- 'name',
342
- 'description',
343
- 'project_id',
344
- 'roles',
345
- 'unrestricted',
346
- 'access_rules',
347
- 'expires_at',
348
- )
349
- return (
350
- columns,
351
- (
352
- utils.get_dict_properties(
353
- app_cred,
354
- columns,
355
- )
356
- ),
357
- )
358
+ return _format_application_credential(application_credential)
@@ -94,7 +94,9 @@ class CreateDomain(command.ShowOne):
94
94
  def take_action(self, parsed_args):
95
95
  identity_client = self.app.client_manager.sdk_connection.identity
96
96
 
97
- options = common.get_immutable_options(parsed_args)
97
+ options = {}
98
+ if parsed_args.immutable is not None:
99
+ options['immutable'] = parsed_args.immutable
98
100
 
99
101
  try:
100
102
  domain = identity_client.create_domain(
@@ -242,13 +244,10 @@ class SetDomain(command.Command):
242
244
  kwargs['name'] = parsed_args.name
243
245
  if parsed_args.description:
244
246
  kwargs['description'] = parsed_args.description
245
-
246
247
  if parsed_args.is_enabled is not None:
247
248
  kwargs['is_enabled'] = parsed_args.is_enabled
248
-
249
- options = common.get_immutable_options(parsed_args)
250
- if options:
251
- kwargs['options'] = options
249
+ if parsed_args.immutable is not None:
250
+ kwargs['options'] = {'immutable': parsed_args.immutable}
252
251
 
253
252
  identity_client.update_domain(domain.id, **kwargs)
254
253
 
@@ -59,17 +59,22 @@ class CreateProject(command.ShowOne):
59
59
  enable_group.add_argument(
60
60
  '--enable',
61
61
  action='store_true',
62
+ dest='enabled',
63
+ default=True,
62
64
  help=_('Enable project'),
63
65
  )
64
66
  enable_group.add_argument(
65
67
  '--disable',
66
- action='store_true',
68
+ action='store_false',
69
+ dest='enabled',
70
+ default=True,
67
71
  help=_('Disable project'),
68
72
  )
69
73
  parser.add_argument(
70
74
  '--property',
71
75
  metavar='<key=value>',
72
76
  action=parseractions.KeyValueAction,
77
+ dest='properties',
73
78
  help=_(
74
79
  'Add a property to <name> '
75
80
  '(repeat option to set multiple properties)'
@@ -98,15 +103,9 @@ class CreateProject(command.ShowOne):
98
103
  parsed_args.parent,
99
104
  ).id
100
105
 
101
- enabled = True
102
- if parsed_args.disable:
103
- enabled = False
104
-
105
- options = common.get_immutable_options(parsed_args)
106
-
107
106
  kwargs = {}
108
- if parsed_args.property:
109
- kwargs = parsed_args.property.copy()
107
+ if parsed_args.properties:
108
+ kwargs = parsed_args.properties.copy()
110
109
  if 'is_domain' in kwargs.keys():
111
110
  if kwargs['is_domain'].lower() == "true":
112
111
  kwargs['is_domain'] = True
@@ -117,13 +116,17 @@ class CreateProject(command.ShowOne):
117
116
 
118
117
  kwargs['tags'] = list(set(parsed_args.tags))
119
118
 
119
+ options = {}
120
+ if parsed_args.immutable is not None:
121
+ options['immutable'] = parsed_args.immutable
122
+
120
123
  try:
121
124
  project = identity_client.projects.create(
122
125
  name=parsed_args.name,
123
126
  domain=domain,
124
127
  parent=parent,
125
128
  description=parsed_args.description,
126
- enabled=enabled,
129
+ enabled=parsed_args.enabled,
127
130
  options=options,
128
131
  **kwargs,
129
132
  )
@@ -356,16 +359,21 @@ class SetProject(command.Command):
356
359
  enable_group.add_argument(
357
360
  '--enable',
358
361
  action='store_true',
362
+ dest='enabled',
363
+ default=None,
359
364
  help=_('Enable project'),
360
365
  )
361
366
  enable_group.add_argument(
362
367
  '--disable',
363
- action='store_true',
368
+ action='store_false',
369
+ dest='enabled',
370
+ default=None,
364
371
  help=_('Disable project'),
365
372
  )
366
373
  parser.add_argument(
367
374
  '--property',
368
375
  metavar='<key=value>',
376
+ dest='properties',
369
377
  action=parseractions.KeyValueAction,
370
378
  help=_(
371
379
  'Set a property on <project> '
@@ -388,15 +396,12 @@ class SetProject(command.Command):
388
396
  kwargs['name'] = parsed_args.name
389
397
  if parsed_args.description:
390
398
  kwargs['description'] = parsed_args.description
391
- if parsed_args.enable:
392
- kwargs['enabled'] = True
393
- if parsed_args.disable:
394
- kwargs['enabled'] = False
395
- options = common.get_immutable_options(parsed_args)
396
- if options:
397
- kwargs['options'] = options
398
- if parsed_args.property:
399
- kwargs.update(parsed_args.property)
399
+ if parsed_args.enabled is not None:
400
+ kwargs['enabled'] = parsed_args.enabled
401
+ if parsed_args.immutable is not None:
402
+ kwargs['options'] = {'immutable': parsed_args.immutable}
403
+ if parsed_args.properties:
404
+ kwargs.update(parsed_args.properties)
400
405
  tag.update_tags_in_args(parsed_args, project, kwargs)
401
406
 
402
407
  identity_client.projects.update(project.id, **kwargs)
@@ -334,9 +334,12 @@ class CreateRole(command.ShowOne):
334
334
 
335
335
  if parsed_args.name:
336
336
  create_kwargs['name'] = parsed_args.name
337
+
337
338
  if parsed_args.description:
338
339
  create_kwargs['description'] = parsed_args.description
339
- create_kwargs['options'] = common.get_immutable_options(parsed_args)
340
+
341
+ if parsed_args.immutable is not None:
342
+ create_kwargs['options'] = {"immutable": parsed_args.immutable}
340
343
 
341
344
  try:
342
345
  role = identity_client.create_role(**create_kwargs)
@@ -585,7 +588,9 @@ class SetRole(command.Command):
585
588
  )
586
589
  update_kwargs["domain_id"] = domain_id
587
590
 
588
- update_kwargs["options"] = common.get_immutable_options(parsed_args)
591
+ if parsed_args.immutable is not None:
592
+ update_kwargs["options"] = {"immutable": parsed_args.immutable}
593
+
589
594
  role = _find_sdk_id(
590
595
  identity_client.find_role,
591
596
  name_or_id=parsed_args.role,
@@ -528,6 +528,16 @@ class SaveImage(command.Command):
528
528
 
529
529
  def get_parser(self, prog_name):
530
530
  parser = super().get_parser(prog_name)
531
+ parser.add_argument(
532
+ "--chunk-size",
533
+ type=int,
534
+ default=1024,
535
+ metavar="<chunk-size>",
536
+ help=_(
537
+ "Size in bytes to read from the wire and buffer at one "
538
+ "time (default: 1024)"
539
+ ),
540
+ )
531
541
  parser.add_argument(
532
542
  "--file",
533
543
  metavar="<filename>",
@@ -550,7 +560,12 @@ class SaveImage(command.Command):
550
560
  if output_file is None:
551
561
  output_file = getattr(sys.stdout, "buffer", sys.stdout)
552
562
 
553
- image_client.download_image(image.id, stream=True, output=output_file)
563
+ image_client.download_image(
564
+ image.id,
565
+ stream=True,
566
+ output=output_file,
567
+ chunk_size=parsed_args.chunk_size,
568
+ )
554
569
 
555
570
 
556
571
  class SetImage(command.Command):
@@ -37,14 +37,18 @@ def _format_image_cache(cached_images):
37
37
  image_obj = copy.deepcopy(image)
38
38
  image_obj['state'] = 'cached'
39
39
  image_obj['last_accessed'] = (
40
- datetime.datetime.utcfromtimestamp(
41
- image['last_accessed']
42
- ).isoformat()
40
+ datetime.datetime.fromtimestamp(
41
+ image['last_accessed'], tz=datetime.timezone.utc
42
+ )
43
+ .replace(tzinfo=None)
44
+ .isoformat()
43
45
  )
44
46
  image_obj['last_modified'] = (
45
- datetime.datetime.utcfromtimestamp(
46
- image['last_modified']
47
- ).isoformat()
47
+ datetime.datetime.fromtimestamp(
48
+ image['last_modified'], tz=datetime.timezone.utc
49
+ )
50
+ .replace(tzinfo=None)
51
+ .isoformat()
48
52
  )
49
53
  image_list.append(image_obj)
50
54
  elif item == "queued_images":