From 487658abe0d098bcedfd3eb2ea297ae7f6067923 Mon Sep 17 00:00:00 2001 From: Chris MacNaughton Date: Thu, 31 May 2018 15:50:06 +0200 Subject: [PATCH] Add action to zap disk(s) This action includes configuration for disk(s) to zap, as well as an additional required flag for the administrator to acknowledge pending data loss Change-Id: I3106e2f10cf132a628aad025f73161b04215598e Related-Bug: #1698154 --- actions.yaml | 25 ++++++ actions/zap-disk | 1 + actions/zap_disk.py | 91 ++++++++++++++++++++ unit_tests/test_actions_zap_disk.py | 129 ++++++++++++++++++++++++++++ 4 files changed, 246 insertions(+) create mode 120000 actions/zap-disk create mode 100755 actions/zap_disk.py create mode 100644 unit_tests/test_actions_zap_disk.py diff --git a/actions.yaml b/actions.yaml index 731de13e..994506cd 100644 --- a/actions.yaml +++ b/actions.yaml @@ -73,3 +73,28 @@ blacklist-remove-disk: Example: '/dev/vdb /var/tmp/test-osd' required: - osd-devices +zap-disk: + description: | + Purge disk of all data and signatures for use by Ceph + . + This action can be necessary in cases where a Ceph cluster is being + redeployed as the charm defaults to skipping disks that look like Ceph + devices in order to preserve data. In order to forcibly redeploy, the + admin is required to perform this action for each disk to be re-consumed. + . + In addition to triggering this action, it is required to pass an additional + parameter option of `i-really-mean-it` to ensure that the + administrator is aware that this *will* cause data loss on the specified + device(s) + params: + devices: + type: string + description: | + A space-separated list of devices to remove the partition table from. + i-really-mean-it: + type: boolean + description: | + This must be toggled to enable actually performing this action + required: + - devices + - i-really-mean-it diff --git a/actions/zap-disk b/actions/zap-disk new file mode 120000 index 00000000..0814a432 --- /dev/null +++ b/actions/zap-disk @@ -0,0 +1 @@ +zap_disk.py \ No newline at end of file diff --git a/actions/zap_disk.py b/actions/zap_disk.py new file mode 100755 index 00000000..aae9896c --- /dev/null +++ b/actions/zap_disk.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python3 +# +# Copyright 2018 Canonical Ltd +# +# 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 os +import sys + +sys.path.append('lib') +sys.path.append('hooks') + +import charmhelpers.core.hookenv as hookenv +from charmhelpers.contrib.storage.linux.utils import ( + is_block_device, + is_device_mounted, + zap_disk, +) +from charmhelpers.core.unitdata import kv +from ceph.utils import is_active_bluestore_device + + +def get_devices(): + """Parse 'devices' action parameter, returns list.""" + devices = [] + for path in hookenv.action_get('devices').split(' '): + path = path.strip() + if not os.path.isabs(path): + hookenv.action_fail('{}: Not absolute path.'.format(path)) + raise + devices.append(path) + return devices + + +def zap(): + if not hookenv.action_get('i-really-mean-it'): + hookenv.action_fail('i-really-mean-it is a required parameter') + return + + failed_devices = [] + not_block_devices = [] + devices = get_devices() + for device in devices: + if not is_block_device(device): + not_block_devices.append(device) + if is_device_mounted(device) or is_active_bluestore_device(device): + failed_devices.append(device) + + if failed_devices or not_block_devices: + message = "" + if failed_devices: + message = "{} devices are mounted: {}".format( + len(failed_devices), + ", ".join(failed_devices)) + if not_block_devices: + if message is not '': + message += "\n\n" + message += "{} devices are not block devices: {}".format( + len(not_block_devices), + ", ".join(not_block_devices)) + hookenv.action_fail(message) + return + db = kv() + used_devices = db.get('osd-devices', []) + for device in devices: + zap_disk(device) + if device in used_devices: + used_devices.remove(device) + db.set('osd-devices', used_devices) + db.flush() + hookenv.action_set({ + 'message': "{} disk(s) have been zapped, to use them as OSDs, run: \n" + "juju run-action {} add-disk osd-devices=\"{}\"".format( + len(devices), + hookenv.local_unit(), + " ".join(devices)) + }) + + +if __name__ == "__main__": + zap() diff --git a/unit_tests/test_actions_zap_disk.py b/unit_tests/test_actions_zap_disk.py new file mode 100644 index 00000000..47f71e7f --- /dev/null +++ b/unit_tests/test_actions_zap_disk.py @@ -0,0 +1,129 @@ +# Copyright 2018 Canonical Ltd +# +# 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 mock + +from actions import zap_disk + +from test_utils import CharmTestCase + + +class ZapDiskActionTests(CharmTestCase): + def setUp(self): + super(ZapDiskActionTests, self).setUp( + zap_disk, ['hookenv', + 'is_block_device', + 'is_device_mounted', + 'is_active_bluestore_device', + 'kv']) + self.is_device_mounted.return_value = False + self.is_block_device.return_value = True + self.is_active_bluestore_device.return_value = False + self.kv.return_value = self.kv + self.hookenv.local_unit.return_value = "ceph-osd-test/0" + + @mock.patch.object(zap_disk, 'zap_disk') + def test_authorized_zap_single_disk(self, + _zap_disk): + """Will zap disk with extra config set""" + def side_effect(arg): + return { + 'devices': '/dev/vdb', + 'i-really-mean-it': True, + }.get(arg) + self.hookenv.action_get.side_effect = side_effect + self.kv.get.return_value = ['/dev/vdb', '/dev/vdz'] + zap_disk.zap() + _zap_disk.assert_called_with('/dev/vdb') + self.kv.get.assert_called_with('osd-devices', []) + self.kv.set.assert_called_with('osd-devices', ['/dev/vdz']) + self.hookenv.action_set.assert_called_with({ + 'message': "1 disk(s) have been zapped, to use " + "them as OSDs, run: \njuju " + "run-action ceph-osd-test/0 add-disk " + "osd-devices=\"/dev/vdb\"" + }) + + @mock.patch.object(zap_disk, 'zap_disk') + def test_authorized_zap_multiple_disks(self, + _zap_disk): + """Will zap disk with extra config set""" + def side_effect(arg): + return { + 'devices': '/dev/vdb /dev/vdc', + 'i-really-mean-it': True, + }.get(arg) + self.hookenv.action_get.side_effect = side_effect + self.kv.get.return_value = ['/dev/vdb', '/dev/vdz'] + zap_disk.zap() + _zap_disk.assert_has_calls([ + mock.call('/dev/vdb'), + mock.call('/dev/vdc'), + ]) + self.kv.get.assert_called_with('osd-devices', []) + self.kv.set.assert_called_with('osd-devices', ['/dev/vdz']) + self.hookenv.action_set.assert_called_with({ + 'message': "2 disk(s) have been zapped, to use " + "them as OSDs, run: \njuju " + "run-action ceph-osd-test/0 add-disk " + "osd-devices=\"/dev/vdb /dev/vdc\"" + }) + + @mock.patch.object(zap_disk, 'zap_disk') + def test_wont_zap_non_block_device(self, + _zap_disk,): + """Will not zap a disk that isn't a block device""" + def side_effect(arg): + return { + 'devices': '/dev/vdb', + 'i-really-mean-it': True, + }.get(arg) + self.hookenv.action_get.side_effect = side_effect + self.is_block_device.return_value = False + zap_disk.zap() + _zap_disk.assert_not_called() + self.hookenv.action_fail.assert_called_with( + "1 devices are not block devices: /dev/vdb") + + @mock.patch.object(zap_disk, 'zap_disk') + def test_wont_zap_mounted_block_device(self, + _zap_disk): + """Will not zap a disk that is mounted""" + def side_effect(arg): + return { + 'devices': '/dev/vdb', + 'i-really-mean-it': True, + }.get(arg) + self.hookenv.action_get.side_effect = side_effect + self.is_device_mounted.return_value = True + zap_disk.zap() + _zap_disk.assert_not_called() + self.hookenv.action_fail.assert_called_with( + "1 devices are mounted: /dev/vdb") + + @mock.patch.object(zap_disk, 'zap_disk') + def test_wont_zap__mounted_bluestore_device(self, + _zap_disk): + """Will not zap a disk that is mounted""" + def side_effect(arg): + return { + 'devices': '/dev/vdb', + 'i-really-mean-it': True, + }.get(arg) + self.hookenv.action_get.side_effect = side_effect + self.is_active_bluestore_device.return_value = True + zap_disk.zap() + _zap_disk.assert_not_called() + self.hookenv.action_fail.assert_called_with( + "1 devices are mounted: /dev/vdb")