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:
parent
2c3eae272f
commit
487658abe0
25
actions.yaml
25
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
|
||||
|
1
actions/zap-disk
Symbolic link
1
actions/zap-disk
Symbolic link
@ -0,0 +1 @@
|
||||
zap_disk.py
|
91
actions/zap_disk.py
Executable file
91
actions/zap_disk.py
Executable 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()
|
129
unit_tests/test_actions_zap_disk.py
Normal file
129
unit_tests/test_actions_zap_disk.py
Normal 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")
|
Loading…
x
Reference in New Issue
Block a user