Browse Source

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
changes/86/571486/10
Chris MacNaughton 1 year ago
parent
commit
487658abe0
4 changed files with 246 additions and 0 deletions
  1. 25
    0
      actions.yaml
  2. 1
    0
      actions/zap-disk
  3. 91
    0
      actions/zap_disk.py
  4. 129
    0
      unit_tests/test_actions_zap_disk.py

+ 25
- 0
actions.yaml View File

@@ -73,3 +73,28 @@ blacklist-remove-disk:
73 73
         Example: '/dev/vdb /var/tmp/test-osd'
74 74
   required:
75 75
     - osd-devices
76
+zap-disk:
77
+  description: |
78
+    Purge disk of all data and signatures for use by Ceph
79
+    .
80
+    This action can be necessary in cases where a Ceph cluster is being
81
+    redeployed as the charm defaults to skipping disks that look like Ceph
82
+    devices in order to preserve data. In order to forcibly redeploy, the
83
+    admin is required to perform this action for each disk to be re-consumed.
84
+    .
85
+    In addition to triggering this action, it is required to pass an additional
86
+    parameter option of `i-really-mean-it` to ensure that the
87
+    administrator is aware that this *will* cause data loss on the specified
88
+    device(s)
89
+  params:
90
+    devices:
91
+      type: string
92
+      description: |
93
+        A space-separated list of devices to remove the partition table from.
94
+    i-really-mean-it:
95
+      type: boolean
96
+      description: |
97
+        This must be toggled to enable actually performing this action
98
+  required:
99
+    - devices
100
+    - i-really-mean-it

+ 1
- 0
actions/zap-disk View File

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

+ 91
- 0
actions/zap_disk.py View File

@@ -0,0 +1,91 @@
1
+#!/usr/bin/env python3
2
+#
3
+# Copyright 2018 Canonical Ltd
4
+#
5
+# Licensed under the Apache License, Version 2.0 (the "License");
6
+# you may not use this file except in compliance with the License.
7
+# You may obtain a copy of the License at
8
+#
9
+#  http://www.apache.org/licenses/LICENSE-2.0
10
+#
11
+# Unless required by applicable law or agreed to in writing, software
12
+# distributed under the License is distributed on an "AS IS" BASIS,
13
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+# See the License for the specific language governing permissions and
15
+# limitations under the License.
16
+
17
+import os
18
+import sys
19
+
20
+sys.path.append('lib')
21
+sys.path.append('hooks')
22
+
23
+import charmhelpers.core.hookenv as hookenv
24
+from charmhelpers.contrib.storage.linux.utils import (
25
+    is_block_device,
26
+    is_device_mounted,
27
+    zap_disk,
28
+)
29
+from charmhelpers.core.unitdata import kv
30
+from ceph.utils import is_active_bluestore_device
31
+
32
+
33
+def get_devices():
34
+    """Parse 'devices' action parameter, returns list."""
35
+    devices = []
36
+    for path in hookenv.action_get('devices').split(' '):
37
+        path = path.strip()
38
+        if not os.path.isabs(path):
39
+            hookenv.action_fail('{}: Not absolute path.'.format(path))
40
+            raise
41
+        devices.append(path)
42
+    return devices
43
+
44
+
45
+def zap():
46
+    if not hookenv.action_get('i-really-mean-it'):
47
+        hookenv.action_fail('i-really-mean-it is a required parameter')
48
+        return
49
+
50
+    failed_devices = []
51
+    not_block_devices = []
52
+    devices = get_devices()
53
+    for device in devices:
54
+        if not is_block_device(device):
55
+            not_block_devices.append(device)
56
+        if is_device_mounted(device) or is_active_bluestore_device(device):
57
+            failed_devices.append(device)
58
+
59
+    if failed_devices or not_block_devices:
60
+        message = ""
61
+        if failed_devices:
62
+            message = "{} devices are mounted: {}".format(
63
+                len(failed_devices),
64
+                ", ".join(failed_devices))
65
+        if not_block_devices:
66
+            if message is not '':
67
+                message += "\n\n"
68
+            message += "{} devices are not block devices: {}".format(
69
+                len(not_block_devices),
70
+                ", ".join(not_block_devices))
71
+        hookenv.action_fail(message)
72
+        return
73
+    db = kv()
74
+    used_devices = db.get('osd-devices', [])
75
+    for device in devices:
76
+        zap_disk(device)
77
+        if device in used_devices:
78
+            used_devices.remove(device)
79
+    db.set('osd-devices', used_devices)
80
+    db.flush()
81
+    hookenv.action_set({
82
+        'message': "{} disk(s) have been zapped, to use them as OSDs, run: \n"
83
+                   "juju run-action {} add-disk osd-devices=\"{}\"".format(
84
+                       len(devices),
85
+                       hookenv.local_unit(),
86
+                       " ".join(devices))
87
+    })
88
+
89
+
90
+if __name__ == "__main__":
91
+    zap()

+ 129
- 0
unit_tests/test_actions_zap_disk.py View File

@@ -0,0 +1,129 @@
1
+# Copyright 2018 Canonical Ltd
2
+#
3
+# Licensed under the Apache License, Version 2.0 (the "License");
4
+# you may not use this file except in compliance with the License.
5
+# You may obtain a copy of the License at
6
+#
7
+#  http://www.apache.org/licenses/LICENSE-2.0
8
+#
9
+# Unless required by applicable law or agreed to in writing, software
10
+# distributed under the License is distributed on an "AS IS" BASIS,
11
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+# See the License for the specific language governing permissions and
13
+# limitations under the License.
14
+
15
+import mock
16
+
17
+from actions import zap_disk
18
+
19
+from test_utils import CharmTestCase
20
+
21
+
22
+class ZapDiskActionTests(CharmTestCase):
23
+    def setUp(self):
24
+        super(ZapDiskActionTests, self).setUp(
25
+            zap_disk, ['hookenv',
26
+                       'is_block_device',
27
+                       'is_device_mounted',
28
+                       'is_active_bluestore_device',
29
+                       'kv'])
30
+        self.is_device_mounted.return_value = False
31
+        self.is_block_device.return_value = True
32
+        self.is_active_bluestore_device.return_value = False
33
+        self.kv.return_value = self.kv
34
+        self.hookenv.local_unit.return_value = "ceph-osd-test/0"
35
+
36
+    @mock.patch.object(zap_disk, 'zap_disk')
37
+    def test_authorized_zap_single_disk(self,
38
+                                        _zap_disk):
39
+        """Will zap disk with extra config set"""
40
+        def side_effect(arg):
41
+            return {
42
+                'devices': '/dev/vdb',
43
+                'i-really-mean-it': True,
44
+            }.get(arg)
45
+        self.hookenv.action_get.side_effect = side_effect
46
+        self.kv.get.return_value = ['/dev/vdb', '/dev/vdz']
47
+        zap_disk.zap()
48
+        _zap_disk.assert_called_with('/dev/vdb')
49
+        self.kv.get.assert_called_with('osd-devices', [])
50
+        self.kv.set.assert_called_with('osd-devices', ['/dev/vdz'])
51
+        self.hookenv.action_set.assert_called_with({
52
+            'message': "1 disk(s) have been zapped, to use "
53
+                       "them as OSDs, run: \njuju "
54
+                       "run-action ceph-osd-test/0 add-disk "
55
+                       "osd-devices=\"/dev/vdb\""
56
+        })
57
+
58
+    @mock.patch.object(zap_disk, 'zap_disk')
59
+    def test_authorized_zap_multiple_disks(self,
60
+                                           _zap_disk):
61
+        """Will zap disk with extra config set"""
62
+        def side_effect(arg):
63
+            return {
64
+                'devices': '/dev/vdb /dev/vdc',
65
+                'i-really-mean-it': True,
66
+            }.get(arg)
67
+        self.hookenv.action_get.side_effect = side_effect
68
+        self.kv.get.return_value = ['/dev/vdb', '/dev/vdz']
69
+        zap_disk.zap()
70
+        _zap_disk.assert_has_calls([
71
+            mock.call('/dev/vdb'),
72
+            mock.call('/dev/vdc'),
73
+        ])
74
+        self.kv.get.assert_called_with('osd-devices', [])
75
+        self.kv.set.assert_called_with('osd-devices', ['/dev/vdz'])
76
+        self.hookenv.action_set.assert_called_with({
77
+            'message': "2 disk(s) have been zapped, to use "
78
+                       "them as OSDs, run: \njuju "
79
+                       "run-action ceph-osd-test/0 add-disk "
80
+                       "osd-devices=\"/dev/vdb /dev/vdc\""
81
+        })
82
+
83
+    @mock.patch.object(zap_disk, 'zap_disk')
84
+    def test_wont_zap_non_block_device(self,
85
+                                       _zap_disk,):
86
+        """Will not zap a disk that isn't a block device"""
87
+        def side_effect(arg):
88
+            return {
89
+                'devices': '/dev/vdb',
90
+                'i-really-mean-it': True,
91
+            }.get(arg)
92
+        self.hookenv.action_get.side_effect = side_effect
93
+        self.is_block_device.return_value = False
94
+        zap_disk.zap()
95
+        _zap_disk.assert_not_called()
96
+        self.hookenv.action_fail.assert_called_with(
97
+            "1 devices are not block devices: /dev/vdb")
98
+
99
+    @mock.patch.object(zap_disk, 'zap_disk')
100
+    def test_wont_zap_mounted_block_device(self,
101
+                                           _zap_disk):
102
+        """Will not zap a disk that is mounted"""
103
+        def side_effect(arg):
104
+            return {
105
+                'devices': '/dev/vdb',
106
+                'i-really-mean-it': True,
107
+            }.get(arg)
108
+        self.hookenv.action_get.side_effect = side_effect
109
+        self.is_device_mounted.return_value = True
110
+        zap_disk.zap()
111
+        _zap_disk.assert_not_called()
112
+        self.hookenv.action_fail.assert_called_with(
113
+            "1 devices are mounted: /dev/vdb")
114
+
115
+    @mock.patch.object(zap_disk, 'zap_disk')
116
+    def test_wont_zap__mounted_bluestore_device(self,
117
+                                                _zap_disk):
118
+        """Will not zap a disk that is mounted"""
119
+        def side_effect(arg):
120
+            return {
121
+                'devices': '/dev/vdb',
122
+                'i-really-mean-it': True,
123
+            }.get(arg)
124
+        self.hookenv.action_get.side_effect = side_effect
125
+        self.is_active_bluestore_device.return_value = True
126
+        zap_disk.zap()
127
+        _zap_disk.assert_not_called()
128
+        self.hookenv.action_fail.assert_called_with(
129
+            "1 devices are mounted: /dev/vdb")

Loading…
Cancel
Save