Skip to content

Commit 2be3596

Browse files
committed
Add block storage cleanup command
This patch adds the ``block storage cleanup`` command that allow operators to cleanup resources (volumes and snapshots) with failed operations by requesting services in other hosts in the same cluster to cleanup resources of a failed service. Change-Id: I1375223f525021db5201fa0a9f9a647d17dd01f7
1 parent a9e3049 commit 2be3596

File tree

7 files changed

+372
-1
lines changed

7 files changed

+372
-1
lines changed
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
=============
2+
block storage
3+
=============
4+
5+
Block Storage v3
6+
7+
.. autoprogram-cliff:: openstack.volume.v3
8+
:command: block storage cleanup

doc/source/cli/data/cinder.csv

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ type-update,volume type set,"Updates volume type name description and/or is_publ
140140
unmanage,volume delete --remote,Stop managing a volume.
141141
upload-to-image,image create --volume,Uploads volume to Image Service as an image.
142142
version-list,versions show --service block-storage,List all API versions. (Supported by API versions 3.0 - 3.latest)
143-
work-cleanup,,Request cleanup of services with optional filtering. (Supported by API versions 3.24 - 3.latest)
143+
work-cleanup,block storage cleanup,Request cleanup of services with optional filtering. (Supported by API versions 3.24 - 3.latest)
144144
bash-completion,complete,Prints arguments for bash_completion.
145145
help,help,Shows help about this program or one of its subcommands.
146146
list-extensions,extension list --volume,Lists all available os-api extensions.

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

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ def __init__(self, **kwargs):
4949
self.volume_types.resource_class = fakes.FakeResource(None, {})
5050
self.services = mock.Mock()
5151
self.services.resource_class = fakes.FakeResource(None, {})
52+
self.workers = mock.Mock()
53+
self.workers.resource_class = fakes.FakeResource(None, {})
5254

5355

5456
class TestVolume(utils.TestCommand):
@@ -455,3 +457,33 @@ def create_service_log_level_entry(attrs=None):
455457
service_log_level = fakes.FakeResource(
456458
None, service_log_level_info, loaded=True)
457459
return service_log_level
460+
461+
462+
def create_cleanup_records():
463+
"""Create fake service cleanup records.
464+
465+
:return: A list of FakeResource objects
466+
"""
467+
cleaning_records = []
468+
unavailable_records = []
469+
cleaning_work_info = {
470+
'id': 1,
471+
'host': 'devstack@fakedriver-1',
472+
'binary': 'cinder-volume',
473+
'cluster_name': 'fake_cluster',
474+
}
475+
unavailable_work_info = {
476+
'id': 2,
477+
'host': 'devstack@fakedriver-2',
478+
'binary': 'cinder-scheduler',
479+
'cluster_name': 'new_cluster',
480+
}
481+
cleaning_records.append(cleaning_work_info)
482+
unavailable_records.append(unavailable_work_info)
483+
484+
cleaning = [fakes.FakeResource(
485+
None, obj, loaded=True) for obj in cleaning_records]
486+
unavailable = [fakes.FakeResource(
487+
None, obj, loaded=True) for obj in unavailable_records]
488+
489+
return cleaning, unavailable
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
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+
import uuid
14+
15+
from cinderclient import api_versions
16+
from osc_lib import exceptions
17+
18+
from openstackclient.tests.unit.volume.v3 import fakes as volume_fakes
19+
from openstackclient.volume.v3 import block_storage_cleanup
20+
21+
22+
class TestBlockStorage(volume_fakes.TestVolume):
23+
24+
def setUp(self):
25+
super().setUp()
26+
27+
# Get a shortcut to the BlockStorageWorkerManager Mock
28+
self.worker_mock = self.app.client_manager.volume.workers
29+
self.worker_mock.reset_mock()
30+
31+
32+
class TestBlockStorageCleanup(TestBlockStorage):
33+
34+
cleaning, unavailable = volume_fakes.create_cleanup_records()
35+
36+
def setUp(self):
37+
super().setUp()
38+
39+
self.worker_mock.clean.return_value = (self.cleaning, self.unavailable)
40+
41+
# Get the command object to test
42+
self.cmd = \
43+
block_storage_cleanup.BlockStorageCleanup(self.app, None)
44+
45+
def test_cleanup(self):
46+
self.app.client_manager.volume.api_version = \
47+
api_versions.APIVersion('3.24')
48+
49+
arglist = [
50+
]
51+
verifylist = [
52+
('cluster', None),
53+
('host', None),
54+
('binary', None),
55+
('is_up', None),
56+
('disabled', None),
57+
('resource_id', None),
58+
('resource_type', None),
59+
('service_id', None),
60+
]
61+
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
62+
63+
expected_columns = ('ID', 'Cluster Name', 'Host', 'Binary', 'Status')
64+
cleaning_data = tuple(
65+
(
66+
obj.id,
67+
obj.cluster_name,
68+
obj.host,
69+
obj.binary,
70+
'Cleaning'
71+
) for obj in self.cleaning
72+
)
73+
unavailable_data = tuple(
74+
(
75+
obj.id,
76+
obj.cluster_name,
77+
obj.host,
78+
obj.binary,
79+
'Unavailable'
80+
) for obj in self.unavailable
81+
)
82+
expected_data = cleaning_data + unavailable_data
83+
columns, data = self.cmd.take_action(parsed_args)
84+
85+
self.assertEqual(expected_columns, columns)
86+
self.assertEqual(expected_data, tuple(data))
87+
88+
# checking if proper call was made to cleanup resources
89+
# Since we ignore all parameters with None value, we don't
90+
# have any arguments passed to the API
91+
self.worker_mock.clean.assert_called_once_with()
92+
93+
def test_block_storage_cleanup_pre_324(self):
94+
arglist = [
95+
]
96+
verifylist = [
97+
('cluster', None),
98+
('host', None),
99+
('binary', None),
100+
('is_up', None),
101+
('disabled', None),
102+
('resource_id', None),
103+
('resource_type', None),
104+
('service_id', None),
105+
]
106+
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
107+
exc = self.assertRaises(exceptions.CommandError, self.cmd.take_action,
108+
parsed_args)
109+
self.assertIn(
110+
'--os-volume-api-version 3.24 or greater is required', str(exc))
111+
112+
def test_cleanup_with_args(self):
113+
self.app.client_manager.volume.api_version = \
114+
api_versions.APIVersion('3.24')
115+
116+
fake_cluster = 'fake-cluster'
117+
fake_host = 'fake-host'
118+
fake_binary = 'fake-service'
119+
fake_resource_id = str(uuid.uuid4())
120+
fake_resource_type = 'Volume'
121+
fake_service_id = 1
122+
arglist = [
123+
'--cluster', fake_cluster,
124+
'--host', fake_host,
125+
'--binary', fake_binary,
126+
'--down',
127+
'--enabled',
128+
'--resource-id', fake_resource_id,
129+
'--resource-type', fake_resource_type,
130+
'--service-id', str(fake_service_id),
131+
]
132+
verifylist = [
133+
('cluster', fake_cluster),
134+
('host', fake_host),
135+
('binary', fake_binary),
136+
('is_up', False),
137+
('disabled', False),
138+
('resource_id', fake_resource_id),
139+
('resource_type', fake_resource_type),
140+
('service_id', fake_service_id),
141+
]
142+
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
143+
144+
expected_columns = ('ID', 'Cluster Name', 'Host', 'Binary', 'Status')
145+
cleaning_data = tuple(
146+
(
147+
obj.id,
148+
obj.cluster_name,
149+
obj.host,
150+
obj.binary,
151+
'Cleaning'
152+
) for obj in self.cleaning
153+
)
154+
unavailable_data = tuple(
155+
(
156+
obj.id,
157+
obj.cluster_name,
158+
obj.host,
159+
obj.binary,
160+
'Unavailable'
161+
) for obj in self.unavailable
162+
)
163+
expected_data = cleaning_data + unavailable_data
164+
columns, data = self.cmd.take_action(parsed_args)
165+
166+
self.assertEqual(expected_columns, columns)
167+
self.assertEqual(expected_data, tuple(data))
168+
169+
# checking if proper call was made to cleanup resources
170+
self.worker_mock.clean.assert_called_once_with(
171+
cluster_name=fake_cluster,
172+
host=fake_host,
173+
binary=fake_binary,
174+
is_up=False,
175+
disabled=False,
176+
resource_id=fake_resource_id,
177+
resource_type=fake_resource_type,
178+
service_id=fake_service_id)
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
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+
from cinderclient import api_versions
14+
from osc_lib.command import command
15+
from osc_lib import exceptions
16+
17+
from openstackclient.i18n import _
18+
19+
20+
def _format_cleanup_response(cleaning, unavailable):
21+
column_headers = (
22+
'ID',
23+
'Cluster Name',
24+
'Host',
25+
'Binary',
26+
'Status',
27+
)
28+
combined_data = []
29+
for obj in cleaning:
30+
details = (obj.id, obj.cluster_name, obj.host, obj.binary, 'Cleaning')
31+
combined_data.append(details)
32+
33+
for obj in unavailable:
34+
details = (obj.id, obj.cluster_name, obj.host, obj.binary,
35+
'Unavailable')
36+
combined_data.append(details)
37+
38+
return (column_headers, combined_data)
39+
40+
41+
class BlockStorageCleanup(command.Lister):
42+
"""Do block storage cleanup.
43+
44+
This command requires ``--os-volume-api-version`` 3.24 or greater.
45+
"""
46+
47+
def get_parser(self, prog_name):
48+
parser = super().get_parser(prog_name)
49+
parser.add_argument(
50+
'--cluster',
51+
metavar='<cluster>',
52+
help=_('Name of block storage cluster in which cleanup needs '
53+
'to be performed (name only)')
54+
)
55+
parser.add_argument(
56+
"--host",
57+
metavar="<host>",
58+
default=None,
59+
help=_("Host where the service resides. (name only)")
60+
)
61+
parser.add_argument(
62+
'--binary',
63+
metavar='<binary>',
64+
default=None,
65+
help=_("Name of the service binary.")
66+
)
67+
service_up_parser = parser.add_mutually_exclusive_group()
68+
service_up_parser.add_argument(
69+
'--up',
70+
dest='is_up',
71+
action='store_true',
72+
default=None,
73+
help=_(
74+
'Filter by up status. If this is set, services need to be up.'
75+
)
76+
)
77+
service_up_parser.add_argument(
78+
'--down',
79+
dest='is_up',
80+
action='store_false',
81+
help=_(
82+
'Filter by down status. If this is set, services need to be '
83+
'down.'
84+
)
85+
)
86+
service_disabled_parser = parser.add_mutually_exclusive_group()
87+
service_disabled_parser.add_argument(
88+
'--disabled',
89+
dest='disabled',
90+
action='store_true',
91+
default=None,
92+
help=_('Filter by disabled status.')
93+
)
94+
service_disabled_parser.add_argument(
95+
'--enabled',
96+
dest='disabled',
97+
action='store_false',
98+
help=_('Filter by enabled status.')
99+
)
100+
parser.add_argument(
101+
'--resource-id',
102+
metavar='<resource-id>',
103+
default=None,
104+
help=_('UUID of a resource to cleanup.')
105+
)
106+
parser.add_argument(
107+
'--resource-type',
108+
metavar='<Volume|Snapshot>',
109+
choices=('Volume', 'Snapshot'),
110+
help=_('Type of resource to cleanup.')
111+
)
112+
parser.add_argument(
113+
'--service-id',
114+
type=int,
115+
default=None,
116+
help=_(
117+
'The service ID field from the DB, not the UUID of the '
118+
'service.'
119+
)
120+
)
121+
return parser
122+
123+
def take_action(self, parsed_args):
124+
volume_client = self.app.client_manager.volume
125+
126+
if volume_client.api_version < api_versions.APIVersion('3.24'):
127+
msg = _(
128+
"--os-volume-api-version 3.24 or greater is required to "
129+
"support the 'block storage cleanup' command"
130+
)
131+
raise exceptions.CommandError(msg)
132+
133+
filters = {
134+
'cluster_name': parsed_args.cluster,
135+
'host': parsed_args.host,
136+
'binary': parsed_args.binary,
137+
'is_up': parsed_args.is_up,
138+
'disabled': parsed_args.disabled,
139+
'resource_id': parsed_args.resource_id,
140+
'resource_type': parsed_args.resource_type,
141+
'service_id': parsed_args.service_id
142+
}
143+
144+
filters = {k: v for k, v in filters.items() if v is not None}
145+
cleaning, unavailable = volume_client.workers.clean(**filters)
146+
return _format_cleanup_response(cleaning, unavailable)
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
features:
3+
- |
4+
Added ``block storage cleanup`` command that allows cleanup
5+
of resources (volumes and snapshots) by services in other nodes
6+
in a cluster in an Active-Active deployments.

setup.cfg

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -824,3 +824,4 @@ openstack.volume.v3 =
824824
volume_revert = openstackclient.volume.v3.volume:VolumeRevertToSnapshot
825825
block_storage_log_level_list = openstackclient.volume.v3.block_storage_log_level:BlockStorageLogLevelList
826826
block_storage_log_level_set = openstackclient.volume.v3.block_storage_log_level:BlockStorageLogLevelSet
827+
block_storage_cleanup = openstackclient.volume.v3.block_storage_cleanup:BlockStorageCleanup

0 commit comments

Comments
 (0)