
This patch adds support for unmanaging a volume with the ``openstack volume delete --remote`` command. Change-Id: Id71681e817f6e56b4ef553079f0bcfac8252d3cf
531 lines
16 KiB
Python
531 lines
16 KiB
Python
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
# not use this file except in compliance with the License. You may obtain
|
|
# a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
# License for the specific language governing permissions and limitations
|
|
# under the License.
|
|
#
|
|
|
|
import copy
|
|
from unittest import mock
|
|
|
|
from cinderclient import api_versions
|
|
from openstack.block_storage.v3 import block_storage_summary as _summary
|
|
from openstack.block_storage.v3 import snapshot as _snapshot
|
|
from openstack.block_storage.v3 import volume as _volume
|
|
from openstack.test import fakes as sdk_fakes
|
|
from openstack import utils as sdk_utils
|
|
from osc_lib.cli import format_columns
|
|
from osc_lib import exceptions
|
|
|
|
from openstackclient.tests.unit.volume.v3 import fakes
|
|
from openstackclient.volume.v3 import volume
|
|
|
|
|
|
class BaseVolumeTest(fakes.TestVolume):
|
|
def setUp(self):
|
|
super().setUp()
|
|
|
|
patcher = mock.patch.object(
|
|
sdk_utils, 'supports_microversion', return_value=True
|
|
)
|
|
self.addCleanup(patcher.stop)
|
|
self.supports_microversion_mock = patcher.start()
|
|
self._set_mock_microversion(
|
|
self.volume_client.api_version.get_string()
|
|
)
|
|
|
|
def _set_mock_microversion(self, mock_v):
|
|
"""Set a specific microversion for the mock supports_microversion()."""
|
|
self.supports_microversion_mock.reset_mock(return_value=True)
|
|
self.supports_microversion_mock.side_effect = (
|
|
lambda _, v: api_versions.APIVersion(v)
|
|
<= api_versions.APIVersion(mock_v)
|
|
)
|
|
|
|
|
|
class TestVolumeSummary(BaseVolumeTest):
|
|
columns = [
|
|
'Total Count',
|
|
'Total Size',
|
|
]
|
|
|
|
def setUp(self):
|
|
super().setUp()
|
|
|
|
self.volume_a = sdk_fakes.generate_fake_resource(_volume.Volume)
|
|
self.volume_b = sdk_fakes.generate_fake_resource(_volume.Volume)
|
|
self.summary = sdk_fakes.generate_fake_resource(
|
|
_summary.BlockStorageSummary,
|
|
total_count=2,
|
|
total_size=self.volume_a.size + self.volume_b.size,
|
|
)
|
|
self.volume_sdk_client.summary.return_value = self.summary
|
|
|
|
# Get the command object to test
|
|
self.cmd = volume.VolumeSummary(self.app, None)
|
|
|
|
def test_volume_summary(self):
|
|
self._set_mock_microversion('3.12')
|
|
arglist = [
|
|
'--all-projects',
|
|
]
|
|
verifylist = [
|
|
('all_projects', True),
|
|
]
|
|
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
|
|
|
|
columns, data = self.cmd.take_action(parsed_args)
|
|
|
|
self.volume_sdk_client.summary.assert_called_once_with(True)
|
|
|
|
self.assertEqual(self.columns, columns)
|
|
|
|
datalist = (2, self.volume_a.size + self.volume_b.size)
|
|
self.assertCountEqual(datalist, tuple(data))
|
|
|
|
def test_volume_summary_pre_312(self):
|
|
arglist = [
|
|
'--all-projects',
|
|
]
|
|
verifylist = [
|
|
('all_projects', True),
|
|
]
|
|
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
|
|
|
|
exc = self.assertRaises(
|
|
exceptions.CommandError, self.cmd.take_action, parsed_args
|
|
)
|
|
self.assertIn(
|
|
'--os-volume-api-version 3.12 or greater is required', str(exc)
|
|
)
|
|
|
|
def test_volume_summary_with_metadata(self):
|
|
self._set_mock_microversion('3.36')
|
|
|
|
metadata = {**self.volume_a.metadata, **self.volume_b.metadata}
|
|
self.summary = sdk_fakes.generate_fake_resource(
|
|
_summary.BlockStorageSummary,
|
|
total_count=2,
|
|
total_size=self.volume_a.size + self.volume_b.size,
|
|
metadata=metadata,
|
|
)
|
|
self.volume_sdk_client.summary.return_value = self.summary
|
|
|
|
new_cols = copy.deepcopy(self.columns)
|
|
new_cols.extend(['Metadata'])
|
|
|
|
arglist = [
|
|
'--all-projects',
|
|
]
|
|
verifylist = [
|
|
('all_projects', True),
|
|
]
|
|
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
|
|
|
|
columns, data = self.cmd.take_action(parsed_args)
|
|
|
|
self.volume_sdk_client.summary.assert_called_once_with(True)
|
|
|
|
self.assertEqual(new_cols, columns)
|
|
|
|
datalist = (
|
|
2,
|
|
self.volume_a.size + self.volume_b.size,
|
|
format_columns.DictColumn(metadata),
|
|
)
|
|
self.assertCountEqual(datalist, tuple(data))
|
|
|
|
|
|
class TestVolumeRevertToSnapshot(BaseVolumeTest):
|
|
def setUp(self):
|
|
super().setUp()
|
|
|
|
self.volume = sdk_fakes.generate_fake_resource(_volume.Volume)
|
|
self.snapshot = sdk_fakes.generate_fake_resource(
|
|
_snapshot.Snapshot,
|
|
volume_id=self.volume.id,
|
|
)
|
|
self.volume_sdk_client.find_volume.return_value = self.volume
|
|
self.volume_sdk_client.find_snapshot.return_value = self.snapshot
|
|
|
|
# Get the command object to test
|
|
self.cmd = volume.VolumeRevertToSnapshot(self.app, None)
|
|
|
|
def test_volume_revert_to_snapshot_pre_340(self):
|
|
arglist = [
|
|
self.snapshot.id,
|
|
]
|
|
verifylist = [
|
|
('snapshot', self.snapshot.id),
|
|
]
|
|
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
|
|
|
|
exc = self.assertRaises(
|
|
exceptions.CommandError, self.cmd.take_action, parsed_args
|
|
)
|
|
self.assertIn(
|
|
'--os-volume-api-version 3.40 or greater is required', str(exc)
|
|
)
|
|
|
|
def test_volume_revert_to_snapshot(self):
|
|
self._set_mock_microversion('3.40')
|
|
arglist = [
|
|
self.snapshot.id,
|
|
]
|
|
verifylist = [
|
|
('snapshot', self.snapshot.id),
|
|
]
|
|
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
|
|
|
|
self.cmd.take_action(parsed_args)
|
|
|
|
self.volume_sdk_client.revert_volume_to_snapshot.assert_called_once_with(
|
|
self.volume,
|
|
self.snapshot,
|
|
)
|
|
self.volume_sdk_client.find_volume.assert_called_with(
|
|
self.volume.id,
|
|
ignore_missing=False,
|
|
)
|
|
self.volume_sdk_client.find_snapshot.assert_called_with(
|
|
self.snapshot.id,
|
|
ignore_missing=False,
|
|
)
|
|
|
|
|
|
class TestVolumeCreate(BaseVolumeTest):
|
|
columns = (
|
|
'attachments',
|
|
'availability_zone',
|
|
'consistency_group_id',
|
|
'created_at',
|
|
'description',
|
|
'extended_replication_status',
|
|
'group_id',
|
|
'host',
|
|
'id',
|
|
'image_id',
|
|
'is_bootable',
|
|
'is_encrypted',
|
|
'is_multiattach',
|
|
'location',
|
|
'metadata',
|
|
'migration_id',
|
|
'migration_status',
|
|
'name',
|
|
'project_id',
|
|
'provider_id',
|
|
'replication_driver_data',
|
|
'replication_status',
|
|
'scheduler_hints',
|
|
'size',
|
|
'snapshot_id',
|
|
'source_volume_id',
|
|
'status',
|
|
'updated_at',
|
|
'user_id',
|
|
'volume_image_metadata',
|
|
'volume_type',
|
|
)
|
|
|
|
def setUp(self):
|
|
super().setUp()
|
|
|
|
self.new_volume = sdk_fakes.generate_fake_resource(
|
|
_volume.Volume, **{'size': 1}
|
|
)
|
|
|
|
self.datalist = (
|
|
self.new_volume.attachments,
|
|
self.new_volume.availability_zone,
|
|
self.new_volume.consistency_group_id,
|
|
self.new_volume.created_at,
|
|
self.new_volume.description,
|
|
self.new_volume.extended_replication_status,
|
|
self.new_volume.group_id,
|
|
self.new_volume.host,
|
|
self.new_volume.id,
|
|
self.new_volume.image_id,
|
|
self.new_volume.is_bootable,
|
|
self.new_volume.is_encrypted,
|
|
self.new_volume.is_multiattach,
|
|
self.new_volume.location,
|
|
self.new_volume.metadata,
|
|
self.new_volume.migration_id,
|
|
self.new_volume.migration_status,
|
|
self.new_volume.name,
|
|
self.new_volume.project_id,
|
|
self.new_volume.provider_id,
|
|
self.new_volume.replication_driver_data,
|
|
self.new_volume.replication_status,
|
|
self.new_volume.scheduler_hints,
|
|
self.new_volume.size,
|
|
self.new_volume.snapshot_id,
|
|
self.new_volume.source_volume_id,
|
|
self.new_volume.status,
|
|
self.new_volume.updated_at,
|
|
self.new_volume.user_id,
|
|
self.new_volume.volume_image_metadata,
|
|
self.new_volume.volume_type,
|
|
)
|
|
|
|
# Get the command object to test
|
|
self.cmd = volume.CreateVolume(self.app, None)
|
|
|
|
def test_volume_create_remote_source(self):
|
|
self.volume_sdk_client.manage_volume.return_value = self.new_volume
|
|
|
|
arglist = [
|
|
'--remote-source',
|
|
'key=val',
|
|
'--host',
|
|
'fake_host',
|
|
self.new_volume.name,
|
|
]
|
|
verifylist = [
|
|
('remote_source', {'key': 'val'}),
|
|
('host', 'fake_host'),
|
|
('name', self.new_volume.name),
|
|
]
|
|
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
|
|
|
|
columns, data = self.cmd.take_action(parsed_args)
|
|
|
|
self.volume_sdk_client.manage_volume.assert_called_with(
|
|
host='fake_host',
|
|
ref={'key': 'val'},
|
|
name=parsed_args.name,
|
|
description=parsed_args.description,
|
|
volume_type=parsed_args.type,
|
|
availability_zone=parsed_args.availability_zone,
|
|
metadata=parsed_args.property,
|
|
bootable=parsed_args.bootable,
|
|
cluster=getattr(parsed_args, 'cluster', None),
|
|
)
|
|
|
|
self.assertEqual(self.columns, columns)
|
|
self.assertCountEqual(self.datalist, data)
|
|
|
|
def test_volume_create_remote_source_pre_316(self):
|
|
self._set_mock_microversion('3.15')
|
|
arglist = [
|
|
'--remote-source',
|
|
'key=val',
|
|
'--cluster',
|
|
'fake_cluster',
|
|
self.new_volume.name,
|
|
]
|
|
verifylist = [
|
|
('remote_source', {'key': 'val'}),
|
|
('cluster', 'fake_cluster'),
|
|
('name', self.new_volume.name),
|
|
]
|
|
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
|
|
|
|
exc = self.assertRaises(
|
|
exceptions.CommandError, self.cmd.take_action, parsed_args
|
|
)
|
|
self.assertIn(
|
|
'--os-volume-api-version 3.16 or greater is required', str(exc)
|
|
)
|
|
|
|
def test_volume_create_remote_source_host_and_cluster(self):
|
|
self._set_mock_microversion('3.16')
|
|
arglist = [
|
|
'--remote-source',
|
|
'key=val',
|
|
'--host',
|
|
'fake_host',
|
|
'--cluster',
|
|
'fake_cluster',
|
|
self.new_volume.name,
|
|
]
|
|
verifylist = [
|
|
('remote_source', {'key': 'val'}),
|
|
('host', 'fake_host'),
|
|
('cluster', 'fake_cluster'),
|
|
('name', self.new_volume.name),
|
|
]
|
|
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
|
|
|
|
exc = self.assertRaises(
|
|
exceptions.CommandError, self.cmd.take_action, parsed_args
|
|
)
|
|
self.assertIn(
|
|
'Only one of --host or --cluster needs to be specified', str(exc)
|
|
)
|
|
|
|
def test_volume_create_remote_source_no_host_or_cluster(self):
|
|
arglist = [
|
|
'--remote-source',
|
|
'key=val',
|
|
self.new_volume.name,
|
|
]
|
|
verifylist = [
|
|
('remote_source', {'key': 'val'}),
|
|
('name', self.new_volume.name),
|
|
]
|
|
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
|
|
|
|
exc = self.assertRaises(
|
|
exceptions.CommandError, self.cmd.take_action, parsed_args
|
|
)
|
|
self.assertIn(
|
|
'One of --host or --cluster needs to be specified to ', str(exc)
|
|
)
|
|
|
|
def test_volume_create_remote_source_size(self):
|
|
arglist = [
|
|
'--size',
|
|
str(self.new_volume.size),
|
|
'--remote-source',
|
|
'key=val',
|
|
self.new_volume.name,
|
|
]
|
|
verifylist = [
|
|
('size', self.new_volume.size),
|
|
('remote_source', {'key': 'val'}),
|
|
('name', self.new_volume.name),
|
|
]
|
|
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
|
|
|
|
exc = self.assertRaises(
|
|
exceptions.CommandError, self.cmd.take_action, parsed_args
|
|
)
|
|
self.assertIn(
|
|
'--size, --consistency-group, --hint, --read-only and '
|
|
'--read-write options are not supported',
|
|
str(exc),
|
|
)
|
|
|
|
def test_volume_create_host_no_remote_source(self):
|
|
arglist = [
|
|
'--size',
|
|
str(self.new_volume.size),
|
|
'--host',
|
|
'fake_host',
|
|
self.new_volume.name,
|
|
]
|
|
verifylist = [
|
|
('size', self.new_volume.size),
|
|
('host', 'fake_host'),
|
|
('name', self.new_volume.name),
|
|
]
|
|
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
|
|
|
|
exc = self.assertRaises(
|
|
exceptions.CommandError, self.cmd.take_action, parsed_args
|
|
)
|
|
self.assertIn(
|
|
'--host and --cluster options are only supported ',
|
|
str(exc),
|
|
)
|
|
|
|
|
|
class TestVolumeDelete(BaseVolumeTest):
|
|
def setUp(self):
|
|
super().setUp()
|
|
|
|
self.volumes_mock = self.volume_client.volumes
|
|
self.volumes_mock.reset_mock()
|
|
self.volume_sdk_client.unmanage_volume.return_value = None
|
|
|
|
# Get the command object to mock
|
|
self.cmd = volume.DeleteVolume(self.app, None)
|
|
|
|
def test_volume_delete_remote(self):
|
|
vol = sdk_fakes.generate_fake_resource(_volume.Volume, **{'size': 1})
|
|
self.volumes_mock.get.return_value = vol
|
|
|
|
arglist = ['--remote', vol.id]
|
|
verifylist = [
|
|
("remote", True),
|
|
("force", False),
|
|
("purge", False),
|
|
("volumes", [vol.id]),
|
|
]
|
|
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
|
|
|
|
result = self.cmd.take_action(parsed_args)
|
|
|
|
self.volume_sdk_client.unmanage_volume.assert_called_once_with(vol.id)
|
|
self.assertIsNone(result)
|
|
|
|
def test_volume_delete_multi_volumes_remote(self):
|
|
volumes = sdk_fakes.generate_fake_resources(
|
|
_volume.Volume, count=3, attrs={'size': 1}
|
|
)
|
|
|
|
arglist = ['--remote']
|
|
arglist += [v.id for v in volumes]
|
|
verifylist = [
|
|
('remote', True),
|
|
('force', False),
|
|
('purge', False),
|
|
('volumes', arglist[1:]),
|
|
]
|
|
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
|
|
|
|
result = self.cmd.take_action(parsed_args)
|
|
|
|
calls = [mock.call(v.id) for v in volumes]
|
|
self.volume_sdk_client.unmanage_volume.assert_has_calls(calls)
|
|
self.assertIsNone(result)
|
|
|
|
def test_volume_delete_remote_with_purge(self):
|
|
vol = sdk_fakes.generate_fake_resource(_volume.Volume, **{'size': 1})
|
|
|
|
arglist = [
|
|
'--remote',
|
|
'--purge',
|
|
vol.id,
|
|
]
|
|
verifylist = [
|
|
('remote', True),
|
|
('force', False),
|
|
('purge', True),
|
|
('volumes', [vol.id]),
|
|
]
|
|
|
|
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
|
|
exc = self.assertRaises(
|
|
exceptions.CommandError, self.cmd.take_action, parsed_args
|
|
)
|
|
self.assertIn(
|
|
"The --force and --purge options are not supported with the "
|
|
"--remote parameter.",
|
|
str(exc),
|
|
)
|
|
|
|
def test_volume_delete_remote_with_force(self):
|
|
vol = sdk_fakes.generate_fake_resource(_volume.Volume, **{'size': 1})
|
|
|
|
arglist = [
|
|
'--remote',
|
|
'--force',
|
|
vol.id,
|
|
]
|
|
verifylist = [
|
|
('remote', True),
|
|
('force', True),
|
|
('purge', False),
|
|
('volumes', [vol.id]),
|
|
]
|
|
|
|
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
|
|
exc = self.assertRaises(
|
|
exceptions.CommandError, self.cmd.take_action, parsed_args
|
|
)
|
|
self.assertIn(
|
|
"The --force and --purge options are not supported with the "
|
|
"--remote parameter.",
|
|
str(exc),
|
|
)
|