Skip to content

Commit cc7773f

Browse files
committed
Add support for managing volumes
This patch adds support for the cinder manage command to bring a volume under OpenStack management. Change-Id: I12b63bfc4f0c9bc29cf9d4efd9a5cd038ec00af3
1 parent 0d9ace6 commit cc7773f

3 files changed

Lines changed: 329 additions & 1 deletion

File tree

openstackclient/tests/unit/volume/v3/test_volume.py

Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,3 +198,232 @@ def test_volume_revert_to_snapshot(self):
198198
self.snapshot.id,
199199
ignore_missing=False,
200200
)
201+
202+
203+
class TestVolumeCreate(BaseVolumeTest):
204+
columns = (
205+
'attachments',
206+
'availability_zone',
207+
'consistency_group_id',
208+
'created_at',
209+
'description',
210+
'extended_replication_status',
211+
'group_id',
212+
'host',
213+
'id',
214+
'image_id',
215+
'is_bootable',
216+
'is_encrypted',
217+
'is_multiattach',
218+
'location',
219+
'metadata',
220+
'migration_id',
221+
'migration_status',
222+
'name',
223+
'project_id',
224+
'provider_id',
225+
'replication_driver_data',
226+
'replication_status',
227+
'scheduler_hints',
228+
'size',
229+
'snapshot_id',
230+
'source_volume_id',
231+
'status',
232+
'updated_at',
233+
'user_id',
234+
'volume_image_metadata',
235+
'volume_type',
236+
)
237+
238+
def setUp(self):
239+
super().setUp()
240+
241+
self.new_volume = sdk_fakes.generate_fake_resource(
242+
_volume.Volume, **{'size': 1}
243+
)
244+
245+
self.datalist = (
246+
self.new_volume.attachments,
247+
self.new_volume.availability_zone,
248+
self.new_volume.consistency_group_id,
249+
self.new_volume.created_at,
250+
self.new_volume.description,
251+
self.new_volume.extended_replication_status,
252+
self.new_volume.group_id,
253+
self.new_volume.host,
254+
self.new_volume.id,
255+
self.new_volume.image_id,
256+
self.new_volume.is_bootable,
257+
self.new_volume.is_encrypted,
258+
self.new_volume.is_multiattach,
259+
self.new_volume.location,
260+
self.new_volume.metadata,
261+
self.new_volume.migration_id,
262+
self.new_volume.migration_status,
263+
self.new_volume.name,
264+
self.new_volume.project_id,
265+
self.new_volume.provider_id,
266+
self.new_volume.replication_driver_data,
267+
self.new_volume.replication_status,
268+
self.new_volume.scheduler_hints,
269+
self.new_volume.size,
270+
self.new_volume.snapshot_id,
271+
self.new_volume.source_volume_id,
272+
self.new_volume.status,
273+
self.new_volume.updated_at,
274+
self.new_volume.user_id,
275+
self.new_volume.volume_image_metadata,
276+
self.new_volume.volume_type,
277+
)
278+
279+
# Get the command object to test
280+
self.cmd = volume.CreateVolume(self.app, None)
281+
282+
def test_volume_create_remote_source(self):
283+
self.volume_sdk_client.manage_volume.return_value = self.new_volume
284+
285+
arglist = [
286+
'--remote-source',
287+
'key=val',
288+
'--host',
289+
'fake_host',
290+
self.new_volume.name,
291+
]
292+
verifylist = [
293+
('remote_source', {'key': 'val'}),
294+
('host', 'fake_host'),
295+
('name', self.new_volume.name),
296+
]
297+
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
298+
299+
columns, data = self.cmd.take_action(parsed_args)
300+
301+
self.volume_sdk_client.manage_volume.assert_called_with(
302+
host='fake_host',
303+
ref={'key': 'val'},
304+
name=parsed_args.name,
305+
description=parsed_args.description,
306+
volume_type=parsed_args.type,
307+
availability_zone=parsed_args.availability_zone,
308+
metadata=parsed_args.property,
309+
bootable=parsed_args.bootable,
310+
cluster=getattr(parsed_args, 'cluster', None),
311+
)
312+
313+
self.assertEqual(self.columns, columns)
314+
self.assertCountEqual(self.datalist, data)
315+
316+
def test_volume_create_remote_source_pre_316(self):
317+
self._set_mock_microversion('3.15')
318+
arglist = [
319+
'--remote-source',
320+
'key=val',
321+
'--cluster',
322+
'fake_cluster',
323+
self.new_volume.name,
324+
]
325+
verifylist = [
326+
('remote_source', {'key': 'val'}),
327+
('cluster', 'fake_cluster'),
328+
('name', self.new_volume.name),
329+
]
330+
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
331+
332+
exc = self.assertRaises(
333+
exceptions.CommandError, self.cmd.take_action, parsed_args
334+
)
335+
self.assertIn(
336+
'--os-volume-api-version 3.16 or greater is required', str(exc)
337+
)
338+
339+
def test_volume_create_remote_source_host_and_cluster(self):
340+
self._set_mock_microversion('3.16')
341+
arglist = [
342+
'--remote-source',
343+
'key=val',
344+
'--host',
345+
'fake_host',
346+
'--cluster',
347+
'fake_cluster',
348+
self.new_volume.name,
349+
]
350+
verifylist = [
351+
('remote_source', {'key': 'val'}),
352+
('host', 'fake_host'),
353+
('cluster', 'fake_cluster'),
354+
('name', self.new_volume.name),
355+
]
356+
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
357+
358+
exc = self.assertRaises(
359+
exceptions.CommandError, self.cmd.take_action, parsed_args
360+
)
361+
self.assertIn(
362+
'Only one of --host or --cluster needs to be specified', str(exc)
363+
)
364+
365+
def test_volume_create_remote_source_no_host_or_cluster(self):
366+
arglist = [
367+
'--remote-source',
368+
'key=val',
369+
self.new_volume.name,
370+
]
371+
verifylist = [
372+
('remote_source', {'key': 'val'}),
373+
('name', self.new_volume.name),
374+
]
375+
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
376+
377+
exc = self.assertRaises(
378+
exceptions.CommandError, self.cmd.take_action, parsed_args
379+
)
380+
self.assertIn(
381+
'One of --host or --cluster needs to be specified to ', str(exc)
382+
)
383+
384+
def test_volume_create_remote_source_size(self):
385+
arglist = [
386+
'--size',
387+
str(self.new_volume.size),
388+
'--remote-source',
389+
'key=val',
390+
self.new_volume.name,
391+
]
392+
verifylist = [
393+
('size', self.new_volume.size),
394+
('remote_source', {'key': 'val'}),
395+
('name', self.new_volume.name),
396+
]
397+
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
398+
399+
exc = self.assertRaises(
400+
exceptions.CommandError, self.cmd.take_action, parsed_args
401+
)
402+
self.assertIn(
403+
'--size, --consistency-group, --hint, --read-only and '
404+
'--read-write options are not supported',
405+
str(exc),
406+
)
407+
408+
def test_volume_create_host_no_remote_source(self):
409+
arglist = [
410+
'--size',
411+
str(self.new_volume.size),
412+
'--host',
413+
'fake_host',
414+
self.new_volume.name,
415+
]
416+
verifylist = [
417+
('size', self.new_volume.size),
418+
('host', 'fake_host'),
419+
('name', self.new_volume.name),
420+
]
421+
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
422+
423+
exc = self.assertRaises(
424+
exceptions.CommandError, self.cmd.take_action, parsed_args
425+
)
426+
self.assertIn(
427+
'--host and --cluster options are only supported ',
428+
str(exc),
429+
)

openstackclient/volume/v3/volume.py

Lines changed: 94 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
from openstack import utils as sdk_utils
2020
from osc_lib.cli import format_columns
21+
from osc_lib.cli import parseractions
2122
from osc_lib.command import command
2223
from osc_lib import exceptions
2324
from osc_lib import utils
@@ -139,8 +140,100 @@ def _check_size_arg(args):
139140
raise exceptions.CommandError(msg)
140141

141142
def get_parser(self, prog_name):
142-
parser, _ = self._get_parser(prog_name)
143+
parser, source_group = self._get_parser(prog_name)
144+
145+
source_group.add_argument(
146+
"--remote-source",
147+
metavar="<key=value>",
148+
action=parseractions.KeyValueAction,
149+
help=_(
150+
"The attribute(s) of the existing remote volume "
151+
"(admin required) (repeat option to specify multiple "
152+
"attributes) e.g.: '--remote-source source-name=test_name "
153+
"--remote-source source-id=test_id'"
154+
),
155+
)
156+
parser.add_argument(
157+
"--host",
158+
metavar="<host>",
159+
help=_(
160+
"Cinder host on which the existing volume resides; "
161+
"takes the form: host@backend-name#pool. This is only "
162+
"used along with the --remote-source option."
163+
),
164+
)
165+
parser.add_argument(
166+
"--cluster",
167+
metavar="<cluster>",
168+
help=_(
169+
"Cinder cluster on which the existing volume resides; "
170+
"takes the form: cluster@backend-name#pool. This is only "
171+
"used along with the --remote-source option. "
172+
"(supported by --os-volume-api-version 3.16 or above)",
173+
),
174+
)
143175
return parser
144176

145177
def take_action(self, parsed_args):
178+
CreateVolume._check_size_arg(parsed_args)
179+
180+
volume_client_sdk = self.app.client_manager.sdk_connection.volume
181+
182+
if (
183+
parsed_args.host or parsed_args.cluster
184+
) and not parsed_args.remote_source:
185+
msg = _(
186+
"The --host and --cluster options are only supported "
187+
"with --remote-source parameter."
188+
)
189+
raise exceptions.CommandError(msg)
190+
191+
if parsed_args.remote_source:
192+
if (
193+
parsed_args.size
194+
or parsed_args.consistency_group
195+
or parsed_args.hint
196+
or parsed_args.read_only
197+
or parsed_args.read_write
198+
):
199+
msg = _(
200+
"The --size, --consistency-group, --hint, --read-only "
201+
"and --read-write options are not supported with the "
202+
"--remote-source parameter."
203+
)
204+
raise exceptions.CommandError(msg)
205+
if parsed_args.cluster:
206+
if not sdk_utils.supports_microversion(
207+
volume_client_sdk, '3.16'
208+
):
209+
msg = _(
210+
"--os-volume-api-version 3.16 or greater is required "
211+
"to support the cluster parameter."
212+
)
213+
raise exceptions.CommandError(msg)
214+
if parsed_args.cluster and parsed_args.host:
215+
msg = _(
216+
"Only one of --host or --cluster needs to be specified "
217+
"to manage a volume."
218+
)
219+
raise exceptions.CommandError(msg)
220+
if not parsed_args.cluster and not parsed_args.host:
221+
msg = _(
222+
"One of --host or --cluster needs to be specified to "
223+
"manage a volume."
224+
)
225+
raise exceptions.CommandError(msg)
226+
volume = volume_client_sdk.manage_volume(
227+
host=parsed_args.host,
228+
cluster=parsed_args.cluster,
229+
ref=parsed_args.remote_source,
230+
name=parsed_args.name,
231+
description=parsed_args.description,
232+
volume_type=parsed_args.type,
233+
availability_zone=parsed_args.availability_zone,
234+
metadata=parsed_args.property,
235+
bootable=parsed_args.bootable,
236+
)
237+
return zip(*sorted(volume.items()))
238+
146239
return self._take_action(parsed_args)
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
features:
3+
- |
4+
Add support for managing volumes with
5+
``openstack volume create --remote-source <key=val>
6+
--host <host>`` command.

0 commit comments

Comments
 (0)