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
This commit is contained in:
Chris MacNaughton 2018-05-31 15:50:06 +02:00
parent 2c3eae272f
commit 487658abe0
4 changed files with 246 additions and 0 deletions

View File

@ -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

1
actions/zap-disk Symbolic link
View File

@ -0,0 +1 @@
zap_disk.py

91
actions/zap_disk.py Executable file
View File

@ -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()

View File

@ -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")