From 9ea7095e9bc352089d44d8b79494aa9321378c31 Mon Sep 17 00:00:00 2001 From: "Grzegorz Grasza (xek)" Date: Fri, 12 Jan 2018 10:48:48 +0100 Subject: [PATCH 05/11] Software RAID setup implementation Co-Authored-By: Grzegorz Grasza Co-Authored-By: Marta Mucek Co-Authored-By: Piotr Prokop --- 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: + 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