diff --git a/ironic-lib/0005-Software-RAID-setup-implementation.patch b/ironic-lib/0005-Software-RAID-setup-implementation.patch new file mode 100644 index 0000000..fa507ed --- /dev/null +++ b/ironic-lib/0005-Software-RAID-setup-implementation.patch @@ -0,0 +1,516 @@ +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 +