1
0
mirror of https://github.com/gryf/openstack.git synced 2025-12-17 03:20:25 +01:00

Merge pull request #10 from xek/swraidsetup

disk_partitioner software RAID setup implementation
This commit is contained in:
Jim Rollenhagen
2018-04-26 11:56:03 -04:00
committed by GitHub

View File

@@ -0,0 +1,516 @@
From 9ea7095e9bc352089d44d8b79494aa9321378c31 Mon Sep 17 00:00:00 2001
From: "Grzegorz Grasza (xek)" <grzegorz.grasza@intel.com>
Date: Fri, 12 Jan 2018 10:48:48 +0100
Subject: [PATCH 05/11] Software RAID setup implementation
Co-Authored-By: Grzegorz Grasza <grzegorz.grasza@intel.com>
Co-Authored-By: Marta Mucek <marta.mucek@intel.com>
Co-Authored-By: Piotr Prokop <piotr.prokop@intel.com>
---
ironic_lib/system_installer/swraidsetup.py | 167 +++++++++++++
.../examples/swraid_outputs/mdstat_raid10.txt | 26 ++
ironic_lib/tests/test_swraidsetup.py | 270 +++++++++++++++++++++
3 files changed, 463 insertions(+)
create mode 100644 ironic_lib/tests/examples/swraid_outputs/mdstat_raid10.txt
create mode 100644 ironic_lib/tests/test_swraidsetup.py
diff --git a/ironic_lib/system_installer/swraidsetup.py b/ironic_lib/system_installer/swraidsetup.py
index 2965aaa..6a5ca2a 100644
--- a/ironic_lib/system_installer/swraidsetup.py
+++ b/ironic_lib/system_installer/swraidsetup.py
@@ -13,7 +13,17 @@
# See the License for the specific language governing permissions and
# limitations under the License.
+import os
+
+from ironic_lib import utils
+from oslo_concurrency import processutils
+from oslo_log import log
+
from ironic_lib.system_installer.base import Setup
+from ironic_lib.system_installer import exceptions
+
+
+LOG = log.getLogger()
class SwRaidSetup(Setup):
@@ -21,9 +31,166 @@ class SwRaidSetup(Setup):
conf_key = 'swraid'
+ def validate_conf(self):
+ for raid in self.conf.values():
+ disks = len(raid['partitions'])
+ raid = raid['raidtype']
+ if (raid == 0 and (disks == 0 or (disks % 2))) \
+ or (raid == 1 and disks < 2) \
+ or (raid in (4, 5) and disks < 3) \
+ or (raid == 6 and disks < 4):
+ raise exceptions.ConfError(
+ "Wrong amount of disks for SW RAID {}.".format(raid))
+
def get_src_names(self):
raid_partitions = [r['partitions'] for r in self.conf.values()]
return sum(raid_partitions, [])
def get_dst_names(self):
return list(self.conf)
+
+ def is_raid10(self, name, partitions):
+ if self.conf[name]['raidtype'] != 1:
+ return False
+
+ for partition in partitions:
+ if self.conf.get(partition):
+ if self.conf[partition]['raidtype'] != 0:
+ return False
+
+ return True
+
+ def setup_disks(self, devices):
+ """Create RAID configuration on the bare metal"""
+ for name, conf in self.conf.items():
+ if name in devices or '/dev/' + name in devices:
+ continue # only overwrite disks not in devices
+
+ partitions = [devices.get(p, p) for p in conf['partitions']]
+ cmd = "yes | mdadm --create /dev/{name} " \
+ "--level={raidtype} --raid-devices={number} " \
+ "{partitions} --force".format(name=name,
+ raidtype=conf['raidtype'],
+ number=len(partitions),
+ partitions=' '.join(
+ partitions))
+ if self.is_raid10(name, partitions):
+ try:
+ out, err = utils.execute(cmd, shell=True)
+ except processutils.ProcessExecutionError as e:
+ # After successful creation this returns
+ # "mdadm: error opening /dev/md3:
+ # No such file or directory"
+ # with error code 2, which we can safely ignore
+ if e.exit_code != 2:
+ raise
+
+ else:
+ out, err = utils.execute(cmd, shell=True)
+
+ # making sure raid is created before moving any further
+ utils.execute('udevadm settle', shell=True)
+
+ LOG.debug("Debug mdadm create stdout: {}".format(out))
+ LOG.debug("Debug mdadm create stderr: {}".format(err))
+ devices[name] = '/dev/' + name
+ return devices
+
+ @classmethod
+ def raid_partitions(self):
+ if not os.path.exists('/proc/mdstat'):
+ return {}
+
+ raid_part = dict()
+ with open('/proc/mdstat', 'r') as mdstat:
+ for line in mdstat.readlines():
+ if 'active raid' in line:
+ raid_part['/dev/{}'.format(line.split()[0])] =\
+ ['/dev/{}'.format(part.partition('[')[0])
+ for part in line.split()[4:]]
+
+ return raid_part
+
+ @classmethod
+ def get_raid_10(self):
+ raids = self.raid_partitions()
+ raids_10 = []
+ for raid in raids:
+ for partition in raids[raid]:
+ if partition in raids.keys():
+ raids_10.append(raid)
+ continue
+
+ # remove duplicates
+ return list(set(raids_10))
+
+ @classmethod
+ def clean_disks(self, devices):
+ """Deletes all RAID configuration (from CERN hardware-manager)"""
+
+ devices_set = set(devices.values())
+ raids = self.raid_partitions()
+ raids_10 = self.get_raid_10()
+ # first we need to delete raids 10 if exists
+ for raid in raids_10:
+ # delete RAID
+ self.delete_raid(raid, devices_set)
+ # pop this RAID from raids
+ del raids[raid]
+
+ for device in raids:
+ if device in devices_set:
+ continue # free only disks not in devices
+ self.delete_raid(device, devices_set)
+
+ @classmethod
+ def delete_raid(self, device, devices_set):
+ try:
+ detail, err = utils.execute(
+ "mdadm --detail {}".format(device), shell=True)
+ component_devices = [l.split()[-1] for l in detail.splitlines()
+ if 'active sync' in l]
+ LOG.debug("Component devices for {}: {}".format(
+ device, component_devices))
+
+ if err:
+ raise processutils.ProcessExecutionError(err)
+ except (processutils.ProcessExecutionError, OSError) as e:
+ raise exceptions.EnvError(
+ "Error getting details of RAID device {}. {}".format(
+ device, e))
+ if devices_set.intersection(set(component_devices)):
+ return
+ try:
+ # Positive output of the following goes into stderr, thus
+ # we don't want to check its content
+ utils.execute("mdadm --stop {}".format(device), shell=True)
+
+ except (processutils.ProcessExecutionError, OSError) as e:
+ raise exceptions.EnvError(
+ "Error stopping RAID device {}. {}".format(device, e))
+
+ try:
+ utils.execute("mdadm --remove {}".format(device), shell=True)
+ except processutils.ProcessExecutionError as e:
+ # After successful stop this returns
+ # "mdadm: error opening /dev/md3: No such file or directory"
+ # with error code 1, which we can safely ignore
+ if e.exit_code != 1:
+ raise
+
+ for device in component_devices:
+ try:
+ _, err = utils.execute("mdadm --examine {}".format(device),
+ shell=True)
+ if "No md superblock detected" in err:
+ continue
+
+ _, err = utils.execute("mdadm --zero-superblock {}".format(
+ device), shell=True)
+ if err:
+ raise processutils.ProcessExecutionError(err)
+ except (processutils.ProcessExecutionError, OSError) as e:
+ raise exceptions.EnvError(
+ "Error erasing superblock for device {}. {}".format(
+ device, e))
diff --git a/ironic_lib/tests/examples/swraid_outputs/mdstat_raid10.txt b/ironic_lib/tests/examples/swraid_outputs/mdstat_raid10.txt
new file mode 100644
index 0000000..34ef378
--- /dev/null
+++ b/ironic_lib/tests/examples/swraid_outputs/mdstat_raid10.txt
@@ -0,0 +1,26 @@
+Personalities : [raid0] [raid1] [raid6] [raid5] [raid4]
+md3 : active raid0 sdb4[1] sda4[0]
+ 2319208448 blocks super 1.2 512k chunks
+
+md0 : active raid1 sdb1[1] sda1[0]
+ 3903488 blocks super 1.2 [2/2] [UU]
+ [=======>.............] resync = 37.1% (1451200/3903488) finish=0.8min speed=46812K/sec
+
+md1 : active raid5 sdd2[3] sdc2[2] sdb2[1] sda2[0]
+ 11710464 blocks super 1.2 level 5, 512k chunk, algorithm 2 [4/4] [UUUU]
+ resync=DELAYED
+
+md4 : active raid0 sdd4[1] sdc4[0]
+ 2319208448 blocks super 1.2 512k chunks
+
+md5 : active raid1 md4[1] md3[0]
+ 2319077376 blocks super 1.2 [2/2] [UU]
+ [>....................] resync = 0.2% (5174208/2319077376) finish=186.3min speed=206968K/sec
+ bitmap: 18/18 pages [72KB], 65536KB chunk
+
+md2 : active raid5 sdd3[3] sdc3[2] sdb3[1] sda3[0]
+ 11710464 blocks super 1.2 level 5, 512k chunk, algorithm 2 [4/4] [UUUU]
+ resync=DELAYED
+
+unused devices: <none>
+
diff --git a/ironic_lib/tests/test_swraidsetup.py b/ironic_lib/tests/test_swraidsetup.py
new file mode 100644
index 0000000..8b743c3
--- /dev/null
+++ b/ironic_lib/tests/test_swraidsetup.py
@@ -0,0 +1,270 @@
+# Copyright (c) 2018 Intel Corporation
+#
+# 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.
+
+from collections import OrderedDict
+import unittest
+
+import mock
+from oslo_concurrency import processutils
+
+from ironic_lib.system_installer import exceptions
+from ironic_lib.system_installer import swraidsetup
+
+
+class SwRaidSetupTestCase(unittest.TestCase):
+
+ def test_validate_conf(self):
+ swraid_setup = swraidsetup.SwRaidSetup(
+ {'swraid': {'md0': {
+ 'raidtype': 1,
+ 'partitions': ['d0p1', 'd0p2']},
+ 'md1': {
+ 'raidtype': 2,
+ 'partitions': ['d1p1', 'd1p2']}}})
+ swraid_setup.validate_conf()
+
+ def test_validate_conf_error_raid4(self):
+ swraid_setup = swraidsetup.SwRaidSetup(
+ {'swraid': {'md0': {
+ 'raidtype': 4,
+ 'partitions': ['d0p1', 'd0p2']}}})
+ with self.assertRaises(exceptions.ConfError):
+ swraid_setup.validate_conf()
+
+ def test_validate_conf_error_raid6(self):
+ swraid_setup = swraidsetup.SwRaidSetup(
+ {'swraid': {'md1': {
+ 'raidtype': 6,
+ 'partitions': ['d1p1', 'd1p2', 'd1p2']}}})
+ with self.assertRaises(exceptions.ConfError):
+ swraid_setup.validate_conf()
+
+ def test_get_src_names(self):
+ swraid_setup = swraidsetup.SwRaidSetup(
+ {'swraid': {'md0': {
+ 'raidtype': 1,
+ 'partitions': ['d0p1', 'd0p2']},
+ 'md1': {
+ 'raidtype': 1,
+ 'partitions': ['d1p1', 'd1p2']}}})
+ self.assertEqual(set(swraid_setup.get_src_names()),
+ {'d0p1', 'd0p2', 'd1p1', 'd1p2'})
+
+ def test_get_dst_names(self):
+ swraid_setup = swraidsetup.SwRaidSetup(
+ {'swraid': {'md0': {
+ 'raidtype': 1,
+ 'partitions': ['d0p1', 'd0p2']},
+ 'md1': {
+ 'raidtype': 1,
+ 'partitions': ['d1p1', 'd1p2']}}})
+ self.assertEqual(set(swraid_setup.get_dst_names()),
+ {'md0', 'md1'})
+
+ def test_is_raid10_true(self):
+ swraid_setup = swraidsetup.SwRaidSetup(
+ {'swraid': {'md0': {
+ 'raidtype': 0,
+ 'partitions': ['d0p1', 'd0p2']},
+ 'md1': {
+ 'raidtype': 0,
+ 'partitions': ['d1p1', 'd1p2']},
+ 'md2': {
+ 'raidtype': 1,
+ 'partitions': ['md0', 'md1']}}})
+ self.assertTrue(swraid_setup.is_raid10('md2', ['md0', 'md1']))
+
+ def test_is_raid_false(self):
+ swraid_setup = swraidsetup.SwRaidSetup(
+ {'swraid': {'md0': {
+ 'raidtype': 0,
+ 'partitions': ['d0p1', 'd0p2']},
+ 'md1': {
+ 'raidtype': 1,
+ 'partitions': ['d1p1', 'd1p2']},
+ 'md2': {
+ 'raidtype': 1,
+ 'partitions': ['md0', 'md1']}}})
+ self.assertFalse(swraid_setup.is_raid10('md2', ['md0', 'md1']))
+
+ @mock.patch('ironic_lib.utils.execute')
+ def test_setup_disks_raise(self, exe):
+ exe.side_effect = processutils.ProcessExecutionError(None, None,
+ 3, None, 'foo')
+ swraid_setup = swraidsetup.SwRaidSetup({'swraid': OrderedDict([
+ ('md0', {'raidtype': 1,
+ 'partitions': ['d0p1', 'd0p2']}),
+ ('md1', {'raidtype': 2,
+ 'partitions': ['d1p1', 'd1p2']})])})
+ with self.assertRaises(processutils.ProcessExecutionError):
+ swraid_setup.setup_disks({})
+
+ def test_setup_disks(self):
+ with mock.patch('ironic_lib.utils.execute',
+ return_value=('', '')) as exe_mock:
+
+ swraid_setup = swraidsetup.SwRaidSetup({'swraid': OrderedDict([
+ ('md0', {
+ 'raidtype': 1,
+ 'partitions': ['d0p1', 'd0p2']}),
+ ('md1', {
+ 'raidtype': 2,
+ 'partitions': ['d1p1', 'd1p2']})])})
+ result = swraid_setup.setup_disks({'md0': '/dev/md0'})
+ self.assertDictEqual(
+ result, {'md0': '/dev/md0', 'md1': '/dev/md1'})
+ self.assertNotIn(mock.call(
+ 'yes | '
+ 'mdadm --create /dev/md0 --level=1 --raid-devices=2'
+ ' d0p1 d0p2 --force',
+ shell=True), exe_mock.mock_calls)
+ result = swraid_setup.setup_disks({})
+ self.assertDictEqual(
+ result, {'md0': '/dev/md0', 'md1': '/dev/md1'})
+ exe_mock.assert_any_call(
+ 'yes | '
+ 'mdadm --create /dev/md0 --level=1 --raid-devices=2'
+ ' d0p1 d0p2 --force',
+ shell=True)
+ exe_mock.assert_any_call(
+ 'yes | '
+ 'mdadm --create /dev/md1 --level=2 --raid-devices=2'
+ ' d1p1 d1p2 --force',
+ shell=True)
+
+ @mock.patch.object(swraidsetup.SwRaidSetup, 'get_raid_10')
+ @mock.patch.object(swraidsetup.SwRaidSetup, 'raid_partitions')
+ def test_clean_disks(self, raid, raid10):
+ raid.return_value = {'/dev/md0': ['/dev/md0']}
+ raid10.return_value = []
+ detail_output = """/dev/md0:
+ Version : 1.2
+ Creation Time : Mon Aug 8 21:19:06 2016
+ Raid Level : raid10
+ Array Size : 209584128 (199.88 GiB 214.61 GB)
+ Used Dev Size : 104792064 (99.94 GiB 107.31 GB)
+ Raid Devices : 4
+ Total Devices : 4
+ Persistence : Superblock is persistent
+
+ Update Time : Mon Aug 8 21:36:36 2016
+ State : active
+ Active Devices : 4
+Working Devices : 4
+ Failed Devices : 0
+ Spare Devices : 0
+
+ Layout : near=2
+ Chunk Size : 512K
+
+ Name : mdadmwrite:0 (local to host mdadmwrite)
+ UUID : 0dc2e687:1dfe70ac:d440b2ac:5828d61d
+ Events : 18
+
+ Number Major Minor RaidDevice State
+ 0 8 0 0 active sync set-A /dev/sda
+ 1 8 16 1 active sync set-B /dev/sdb
+ 2 8 32 2 active sync set-A /dev/sdc
+ 3 8 48 3 active sync set-B /dev/sdd
+"""
+
+ def execute_result(command, *args, **kwargs):
+ if '--detail' in command:
+ return detail_output, ''
+ return '', ''
+ with mock.patch('ironic_lib.utils.execute',
+ side_effect=execute_result) as exe_mock:
+
+ swraid_setup = swraidsetup.SwRaidSetup({'swraid': {}})
+ swraid_setup.clean_disks({'raid0': '/dev/md0'})
+ self.assertFalse(exe_mock.called)
+ swraid_setup.clean_disks({'raid0': '/dev/sda'})
+ exe_mock.assert_called_once_with('mdadm --detail /dev/md0',
+ shell=True)
+ self.assertNotIn(mock.call('mdadm --stop /dev/md0', shell=True),
+ exe_mock.mock_calls)
+ swraid_setup.clean_disks({})
+ exe_mock.assert_any_call('mdadm --stop /dev/md0', shell=True)
+ exe_mock.assert_any_call('mdadm --remove /dev/md0', shell=True)
+ exe_mock.assert_any_call(
+ 'mdadm --zero-superblock /dev/sda', shell=True)
+ exe_mock.assert_any_call(
+ 'mdadm --zero-superblock /dev/sdb', shell=True)
+ exe_mock.assert_any_call(
+ 'mdadm --zero-superblock /dev/sdc', shell=True)
+ exe_mock.assert_any_call(
+ 'mdadm --zero-superblock /dev/sdd', shell=True)
+
+ @mock.patch('os.path.exists')
+ @mock.patch('ironic_lib.system_installer.swraidsetup.open')
+ def test_raid_partitions(self, open_mock, exists):
+ exists.return_value = True
+ with open('ironic_lib/tests/examples/'
+ 'swraid_outputs/mdstat_raid10.txt') as f:
+ mdstat = f.read()
+
+ open_mock.return_value = mock.mock_open(read_data=mdstat).return_value
+ expected_output = {'/dev/md3': ['/dev/sdb4', '/dev/sda4'],
+ '/dev/md0': ['/dev/sdb1', '/dev/sda1'],
+ '/dev/md1': ['/dev/sdd2', '/dev/sdc2', '/dev/sdb2',
+ '/dev/sda2'],
+ '/dev/md4': ['/dev/sdd4', '/dev/sdc4'],
+ '/dev/md5': ['/dev/md4', '/dev/md3'],
+ '/dev/md2': ['/dev/sdd3', '/dev/sdc3', '/dev/sdb3',
+ '/dev/sda3']}
+
+ swraid_setup = swraidsetup.SwRaidSetup({'swraid': {}})
+ self.assertDictEqual(expected_output, swraid_setup.raid_partitions())
+
+ @mock.patch.object(swraidsetup.SwRaidSetup, 'raid_partitions')
+ def test_get_raid_10(self, partitions):
+ partitions.return_value = {'/dev/md3': ['/dev/sdb4', '/dev/sda4'],
+ '/dev/md0': ['/dev/sdb1', '/dev/sda1'],
+ '/dev/md1': ['/dev/sdd2', '/dev/sdc2',
+ '/dev/sdb2', '/dev/sda2'],
+ '/dev/md4': ['/dev/sdd4', '/dev/sdc4'],
+ '/dev/md5': ['/dev/md4', '/dev/md3'],
+ '/dev/md2': ['/dev/sdd3', '/dev/sdc3',
+ '/dev/sdb3', '/dev/sda3']}
+
+ swraid_setup = swraidsetup.SwRaidSetup({'swraid': {}})
+ self.assertListEqual(['/dev/md5'], swraid_setup.get_raid_10())
+
+ @mock.patch.object(swraidsetup.SwRaidSetup, 'raid_partitions')
+ def test_get_raid_10_empty(self, partitions):
+ partitions.return_value = {'/dev/md3': ['/dev/sdb4', '/dev/sda4'],
+ '/dev/md0': ['/dev/sdb1', '/dev/sda1'],
+ '/dev/md1': ['/dev/sdd2', '/dev/sdc2',
+ '/dev/sdb2', '/dev/sda2'],
+ '/dev/md4': ['/dev/sdd4', '/dev/sdc4'],
+ '/dev/md2': ['/dev/sdd3', '/dev/sdc3',
+ '/dev/sdb3', '/dev/sda3']}
+
+ swraid_setup = swraidsetup.SwRaidSetup({'swraid': {}})
+ self.assertListEqual([], swraid_setup.get_raid_10())
+
+ @mock.patch.object(swraidsetup.SwRaidSetup, 'raid_partitions')
+ @mock.patch.object(swraidsetup.SwRaidSetup, 'get_raid_10')
+ @mock.patch.object(swraidsetup.SwRaidSetup, 'delete_raid')
+ def test_clean_disks_raid10(self, delete, raid10, raids):
+ raid10.return_value = ['/dev/md5']
+ raids.return_value = {'/dev/md3': ['/dev/sdb4', '/dev/sda4'],
+ '/dev/md4': ['/dev/sdd4', '/dev/sdc4'],
+ '/dev/md5': ['/dev/md4', '/dev/md3']}
+ swraid_setup = swraidsetup.SwRaidSetup({'swraid': {}})
+ swraid_setup.clean_disks({})
+ delete.assert_any_call('/dev/md5', set([]))
+ delete.assert_any_call('/dev/md4', set([]))
+ delete.assert_any_call('/dev/md3', set([]))
--
2.14.1