All but the last are cherry-picks from master.
When I posted the last patch for master/f2-branch, I thought that the error that was seen was due to the switch to pyudev. But, since it's in f21-branch that can not be the case. Nonetheless, uncanonicalized UUIDs are somehow getting in, so the best step is to canonicalize them on access from udev, which is what the last patch does.
The last patch is for both master and f21-branch.
mulhern (14): Add a docstring to mdraid.mdexamine Factor canonicalize_UUID() into separate method. Be more robust in the face of possible changes to mdadm's UUIDs. Extend mdadm() to capture output Add a method to extract information about an mdraid array Refactor mdraid tests. Split mdadd into separate functions. Break once metadata value is found. Fix mdnominate error message. Use long messages for unittest errors. Still attempt to destroy even if remove failed. Add a test for mddetail on containers. Add a test for activation. Canonicalize MD_UUID* values in udev.py (#1147087)
blivet/devicelibs/mdraid.py | 125 +++++++++--- blivet/devices.py | 2 +- blivet/udev.py | 5 +- blivet/util.py | 20 ++ tests/devicelibs_test/mdraid_interrogate_test.py | 240 +++++++++++++++++++++++ tests/devicelibs_test/mdraid_test.py | 201 ++++++++++--------- 6 files changed, 464 insertions(+), 129 deletions(-) create mode 100755 tests/devicelibs_test/mdraid_interrogate_test.py
Signed-off-by: mulhern amulhern@redhat.com --- blivet/devicelibs/mdraid.py | 6 ++++++ 1 file changed, 6 insertions(+)
diff --git a/blivet/devicelibs/mdraid.py b/blivet/devicelibs/mdraid.py index c71dd84..e757923 100644 --- a/blivet/devicelibs/mdraid.py +++ b/blivet/devicelibs/mdraid.py @@ -222,6 +222,12 @@ def mddeactivate(device): raise MDRaidError("mddeactivate failed for %s: %s" % (device, msg))
def mdexamine(device): + """ Run mdadm --examine to obtain information about an array member. + + :param str device: path of the member device + :rtype: a dict of strings + :returns: a dict containing labels and values extracted from output + """ _vars = util.capture_output(["mdadm", "--examine", "--export", device]).split() _bvars = util.capture_output(["mdadm",
Signed-off-by: mulhern amulhern@redhat.com --- blivet/devicelibs/mdraid.py | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-)
diff --git a/blivet/devicelibs/mdraid.py b/blivet/devicelibs/mdraid.py index e757923..5bb6226 100644 --- a/blivet/devicelibs/mdraid.py +++ b/blivet/devicelibs/mdraid.py @@ -221,6 +221,22 @@ def mddeactivate(device): except MDRaidError as msg: raise MDRaidError("mddeactivate failed for %s: %s" % (device, msg))
+def canonicalize_UUID(a_uuid): + """ Converts uuids to canonical form. + + :param str a_uuid: the UUID + + :returns: a canonicalized UUID + :rtype: str + + mdadm's UUIDs are actual 128 bit uuids, but it formats them strangely. + This converts the uuids to canonical form. + Example: + mdadm UUID: '3386ff85:f5012621:4a435f06:1eb47236' + canonical UUID: '3386ff85-f501-2621-4a43-5f061eb47236' + """ + return str(uuid.UUID(a_uuid.replace(':', ''))) + def mdexamine(device): """ Run mdadm --examine to obtain information about an array member.
@@ -254,13 +270,8 @@ def mdexamine(device): if name == "metadata": info["MD_METADATA"] = value
- # mdadm's UUIDs are actual 128 bit uuids, but it formats them strangely. - # This converts the uuids to canonical form. - # Example: - # mdadm UUID: '3386ff85:f5012621:4a435f06:1eb47236' - # canonical UUID: '3386ff85-f501-2621-4a43-5f061eb47236' for k, v in ((k,v) for (k,v) in info.iteritems() if k.endswith("UUID")): - info[k] = str(uuid.UUID(v.replace(':', ''))) + info[k] = canonicalize_UUID(v)
return info
Signed-off-by: mulhern amulhern@redhat.com --- blivet/devicelibs/mdraid.py | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-)
diff --git a/blivet/devicelibs/mdraid.py b/blivet/devicelibs/mdraid.py index 5bb6226..aaf4567 100644 --- a/blivet/devicelibs/mdraid.py +++ b/blivet/devicelibs/mdraid.py @@ -21,6 +21,7 @@ #
import os +import re import uuid
from .. import util @@ -237,6 +238,24 @@ def canonicalize_UUID(a_uuid): """ return str(uuid.UUID(a_uuid.replace(':', '')))
+def process_UUIDS(info, UUID_keys): + """ Extract and convert expected UUIDs to canonical form. + Reassign canonicalized UUIDs to corresponding keys. + + :param dict info: a dictionary of key/value pairs + :param tuple UUID_keys: a list of keys known to be UUIDs + """ + for k, v in ((k, info[k]) for k in UUID_keys if k in info): + try: + # extract mdadm UUID, e.g., '3386ff85:f5012621:4a435f06:1eb47236' + the_uuid = re.match(r"(([a-f0-9]){8}:){3}([a-f0-9]){8}", v) + + info[k] = canonicalize_UUID(the_uuid.group()) + except (ValueError, AttributeError) as e: + # the unlikely event that mdadm's UUIDs change their format + log.warning('uuid value %s could not be canonicalized: %s', v, e) + info[k] = v # record the value, since mdadm provided something + def mdexamine(device): """ Run mdadm --examine to obtain information about an array member.
@@ -270,8 +289,7 @@ def mdexamine(device): if name == "metadata": info["MD_METADATA"] = value
- for k, v in ((k,v) for (k,v) in info.iteritems() if k.endswith("UUID")): - info[k] = canonicalize_UUID(v) + process_UUIDS(info, ('MD_UUID', 'MD_DEV_UUID'))
return info
Use it in mdexamine()
Signed-off-by: mulhern amulhern@redhat.com --- blivet/devicelibs/mdraid.py | 25 ++++++++++++++++++------- tests/devicelibs_test/mdraid_test.py | 12 ++++++------ 2 files changed, 24 insertions(+), 13 deletions(-)
diff --git a/blivet/devicelibs/mdraid.py b/blivet/devicelibs/mdraid.py index aaf4567..d269c83 100644 --- a/blivet/devicelibs/mdraid.py +++ b/blivet/devicelibs/mdraid.py @@ -79,10 +79,20 @@ def get_raid_superblock_size(size, version=None): log.info("Using %s superBlockSize", headroom) return headroom
-def mdadm(args): - ret = util.run_program(["mdadm"] + args) +def mdadm(args, capture=False): + """ Run mdadm with specified arguments. + + :param bool capture: if True, return the output of the command + :returns: the output of the command or None + :rtype: str or NoneType + :raises: MDRaidError if command fails + """ + argv = ["mdadm"] + args + (ret, out) = util.run_program_and_capture_output(argv) if ret: - raise MDRaidError("running mdadm " + " ".join(args) + " failed") + raise MDRaidError(ret) + if capture: + return out
def mdcreate(device, level, disks, spares=0, metadataVer=None, bitmap=False): """ Create an mdarray from a list of devices. @@ -263,10 +273,11 @@ def mdexamine(device): :rtype: a dict of strings :returns: a dict containing labels and values extracted from output """ - _vars = util.capture_output(["mdadm", - "--examine", "--export", device]).split() - _bvars = util.capture_output(["mdadm", - "--examine", "--brief", device]).split() + try: + _vars = mdadm(["--examine", "--export", device], capture=True).split() + _bvars = mdadm(["--examine", "--brief", device], capture=True).split() + except MDRaidError as e: + raise MDRaidError("mdexamine failed for %s: %s" % (device, e))
info = {} if len(_bvars) > 1 and _bvars[1].startswith("/dev/md"): diff --git a/tests/devicelibs_test/mdraid_test.py b/tests/devicelibs_test/mdraid_test.py index 107423e..d6ae644 100755 --- a/tests/devicelibs_test/mdraid_test.py +++ b/tests/devicelibs_test/mdraid_test.py @@ -101,14 +101,14 @@ class MDRaidAsRootTestCase(loopbackedtestcase.LoopBackedTestCase): # wait for raid to settle time.sleep(2)
- # examining the array itself yield no data - info = mdraid.mdexamine(self._dev_name) - self.assertEqual(info, {}) + # invoking mdexamine on the array itself raises an error + with self.assertRaisesRegexp(MDRaidError, "mdexamine failed"): + mdraid.mdexamine(self._dev_name)
def testMDExamineNonMDRaid(self): - # invoking mdexamine on a device that is not an array member yields {} - info = mdraid.mdexamine(self.loopDevices[0]) - self.assertEqual(info, {}) + # invoking mdexamine on any non-array member raises an error + with self.assertRaisesRegexp(MDRaidError, "mdexamine failed"): + mdraid.mdexamine(self.loopDevices[0])
def _testMDExamine(self, names, metadataVersion=None, level=None): """ Test mdexamine for a specified metadataVersion.
mdadm --detail can give useful information about the number of spare devices and other things that mdadm --examine can not. According to the man page --examine applies to devices which are components of an array, while --detail applies to a whole array which is currently active.
Signed-off-by: mulhern amulhern@redhat.com --- blivet/devicelibs/mdraid.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+)
diff --git a/blivet/devicelibs/mdraid.py b/blivet/devicelibs/mdraid.py index d269c83..c4c4264 100644 --- a/blivet/devicelibs/mdraid.py +++ b/blivet/devicelibs/mdraid.py @@ -304,6 +304,35 @@ def mdexamine(device):
return info
+def mddetail(device): + """Run mdadm --detail in order to read information about an array. + + Note: The --export flag is not used. According to the man pages + the export flag just formats the output as key=value pairs for + easy import, but in the case of --detail it also omits the majority + of the information, including information of real use like the + number of spares in the array. + + :param str device: path of the array device + :rtype: a dict of strings + :returns: a dict containing labels and values extracted from output + """ + try: + lines = mdadm(["--detail", device], capture=True).split("\n") + except MDRaidError as e: + raise MDRaidError("mddetail failed for %s: %s" % (device, e)) + + info = {} + for (name, colon, value) in (line.strip().partition(":") for line in lines): + value = value.strip() + name = name.strip().upper() + if colon and value and name: + info[name] = value + + process_UUIDS(info, ('UUID',)) + + return info + def md_node_from_name(name): named_path = "/dev/md/" + name try:
Also, add some mddetail test cases and extend tests for mdremove and mdadd.
Signed-off-by: mulhern amulhern@redhat.com --- tests/devicelibs_test/mdraid_interrogate_test.py | 228 +++++++++++++++++++++++ tests/devicelibs_test/mdraid_test.py | 169 +++++++---------- 2 files changed, 299 insertions(+), 98 deletions(-) create mode 100755 tests/devicelibs_test/mdraid_interrogate_test.py
diff --git a/tests/devicelibs_test/mdraid_interrogate_test.py b/tests/devicelibs_test/mdraid_interrogate_test.py new file mode 100755 index 0000000..0909fe3 --- /dev/null +++ b/tests/devicelibs_test/mdraid_interrogate_test.py @@ -0,0 +1,228 @@ +#!/usr/bin/python +import unittest +import time +import uuid + +import blivet.devicelibs.raid as raid +import blivet.devicelibs.mdraid as mdraid + +from . import mdraid_test +from blivet.errors import MDRaidError + +class MDRaidInterrogateTestCase(mdraid_test.MDRaidAsRootTestCase): + + def _matchNames(self, found, expected, transient=None): + """ Match names found against expected names. + + :param found: a list of names found in result + :type found: list of str + :param expected: a list of expected names + :type expected: list of str + :param transient: a list of names that only sometimes appear + :type transient: list of str + """ + transient = transient or [] + + for n in (n for n in expected if n not in transient): + self.assertIn(n, found, msg="name '%s' not in info" % n) + + for n in (n for n in found if n not in transient): + self.assertIn(n, expected, msg="unexpected name '%s' in info" % n) + + +class MDExamineTestCase(MDRaidInterrogateTestCase): + + def __init__(self, methodName='runTest'): + super(MDExamineTestCase, self).__init__(methodName=methodName, deviceSpec=[102400, 102400, 102400]) + + names_0 = [ + 'DEVICE', + 'MD_DEVICES', + 'MD_EVENTS', + 'MD_LEVEL', + 'MD_UPDATE_TIME', + 'MD_UUID' + ] + + names_1 = [ + 'DEVICE', + 'MD_ARRAY_SIZE', + 'MD_DEV_UUID', + 'MD_DEVICES', + 'MD_EVENTS', + 'MD_LEVEL', + 'MD_METADATA', + 'MD_NAME', + 'MD_UPDATE_TIME', + 'MD_UUID' + ] + + names_container = [ + 'MD_DEVICES', + 'MD_LEVEL', + 'MD_METADATA', + 'MD_UUID' + ] + + def testMDExamineMDRaidArray(self): + mdraid.mdcreate(self._dev_name, raid.RAID1, self.loopDevices) + time.sleep(2) # wait for raid to settle + + # invoking mdexamine on the array itself raises an error + with self.assertRaisesRegexp(MDRaidError, "mdexamine failed"): + mdraid.mdexamine(self._dev_name) + + def testMDExamineNonMDRaid(self): + # invoking mdexamine on any non-array member raises an error + with self.assertRaisesRegexp(MDRaidError, "mdexamine failed"): + mdraid.mdexamine(self.loopDevices[0]) + + def _testMDExamine(self, names, metadataVersion=None, level=None, spares=0): + """ Test mdexamine for a specified metadataVersion. + + :param list names: mdexamine's expected list of names to return + :param str metadataVersion: the metadata version for the array + :param object level: any valid RAID level descriptor + :param int spares: the number of spares in this array + + Verifies that: + - exactly the predicted names are returned by mdexamine + - RAID level and number of devices are correct + - UUIDs have canonical form + """ + level = mdraid.RAID_levels.raidLevel(level or raid.RAID1) + mdraid.mdcreate(self._dev_name, level, self.loopDevices, metadataVer=metadataVersion, spares=spares) + time.sleep(2) # wait for raid to settle + + info = mdraid.mdexamine(self.loopDevices[0]) + + self._matchNames(info.keys(), names) + + # check names with predictable values + self.assertEqual(info['MD_DEVICES'], str(len(self.loopDevices) - spares)) + self.assertEqual(info['MD_LEVEL'], str(level)) + + # verify that uuids are in canonical form + for name in (k for k in iter(info.keys()) if k.endswith('UUID')): + self.assertEqual(str(uuid.UUID(info[name])), info[name]) + + def testMDExamineContainerDefault(self): + self._testMDExamine(self.names_container, level="container") + + def testMDExamineDefault(self): + self._testMDExamine(self.names_1) + + def testMDExamineSpares(self): + self._testMDExamine(self.names_1, spares=1) + + def testMDExamine0(self): + self._testMDExamine(self.names_0, metadataVersion='0') + + def testMDExamine0_90(self): + self._testMDExamine(self.names_0, metadataVersion='0.90') + + def testMDExamine1(self): + self._testMDExamine(self.names_1, metadataVersion='1') + + def testMDExamine1_2(self): + self._testMDExamine(self.names_1, metadataVersion='1.2') + +class MDDetailTestCase(MDRaidInterrogateTestCase): + + def __init__(self, methodName='runTest'): + super(MDDetailTestCase, self).__init__(methodName=methodName, deviceSpec=[102400, 102400, 102400]) + + names = [ + 'ACTIVE DEVICES', + 'ARRAY SIZE', + 'CREATION TIME', + 'EVENTS', + 'FAILED DEVICES', + 'NAME', + 'PERSISTENCE', + 'RAID DEVICES', + 'RAID LEVEL', + 'RESYNC STATUS', + 'SPARE DEVICES', + 'STATE', + 'TOTAL DEVICES', + 'UPDATE TIME', + 'USED DEV SIZE', + 'UUID', + 'VERSION', + 'WORKING DEVICES' + ] + + names_0 = [ + 'ACTIVE DEVICES', + 'ARRAY SIZE', + 'CREATION TIME', + 'EVENTS', + 'FAILED DEVICES', + 'PERSISTENCE', + 'PREFERRED MINOR', + 'RAID DEVICES', + 'RAID LEVEL', + 'RESYNC STATUS', + 'SPARE DEVICES', + 'STATE', + 'TOTAL DEVICES', + 'UPDATE TIME', + 'USED DEV SIZE', + 'UUID', + 'VERSION', + 'WORKING DEVICES' + ] + + def _testMDDetail(self, names, metadataVersion=None, level=None, spares=0): + """ Test mddetail for a specified metadataVersion. + + :param list names: mdexamine's expected list of names to return + :param str metadataVersion: the metadata version for the array + :param object level: any valid RAID level descriptor + :param int spares: the number of spares for this array + + Verifies that: + - exactly the predicted names are returned by mddetail + - UUIDs have canonical form + """ + level = mdraid.RAID_levels.raidLevel(level) if level is not None else raid.RAID1 + + mdraid.mdcreate(self._dev_name, level, self.loopDevices, metadataVer=metadataVersion, spares=spares) + time.sleep(2) # wait for raid to settle + + info = mdraid.mddetail(self._dev_name) + + # info contains values for exactly names + self._matchNames(info.keys(), names, ['RESYNC STATUS']) + + # check names with predictable values + self.assertEqual(info['ACTIVE DEVICES'], str(len(self.loopDevices) - spares)) + self.assertEqual(info['FAILED DEVICES'], '0') + self.assertEqual(info['RAID LEVEL'], str(level)) + self.assertEqual(info['SPARE DEVICES'], str(spares)) + self.assertEqual(info['TOTAL DEVICES'], str(len(self.loopDevices))) + self.assertEqual(info['WORKING DEVICES'], str(len(self.loopDevices))) + + # verify that uuid is in canonical form + self.assertEqual(str(uuid.UUID(info['UUID'])), info['UUID']) + + def testMDDetail(self): + self._testMDDetail(self.names) + + def testMDDetailSpares(self): + self._testMDDetail(self.names, spares=1) + + def testMDDetail0_90(self): + self._testMDDetail(self.names_0, metadataVersion='0.90') + + def testMDDetailMDDevice(self): + mdraid.mdcreate(self._dev_name, raid.RAID1, self.loopDevices) + time.sleep(2) # wait for raid to settle + + # invoking mddetail on a device raises an error + with self.assertRaisesRegexp(MDRaidError, "mddetail failed"): + mdraid.mddetail(self.loopDevices[0]) + +if __name__ == "__main__": + unittest.main() diff --git a/tests/devicelibs_test/mdraid_test.py b/tests/devicelibs_test/mdraid_test.py index d6ae644..2ed7a2a 100755 --- a/tests/devicelibs_test/mdraid_test.py +++ b/tests/devicelibs_test/mdraid_test.py @@ -1,7 +1,6 @@ #!/usr/bin/python import unittest import time -import uuid
import blivet.devicelibs.raid as raid import blivet.devicelibs.mdraid as mdraid @@ -49,149 +48,123 @@ class MDRaidTestCase(unittest.TestCase): version="version"), mdraid.MD_SUPERBLOCK_SIZE)
- class MDRaidAsRootTestCase(loopbackedtestcase.LoopBackedTestCase):
- names_0 = [ - 'DEVICE', - 'MD_DEVICES', - 'MD_EVENTS', - 'MD_LEVEL', - 'MD_UPDATE_TIME', - 'MD_UUID' - ] - - names_1 = [ - 'DEVICE', - 'MD_ARRAY_SIZE', - 'MD_DEV_UUID', - 'MD_DEVICES', - 'MD_EVENTS', - 'MD_LEVEL', - 'MD_METADATA', - 'MD_NAME', - 'MD_UPDATE_TIME', - 'MD_UUID' - ] - - names_container = [ - 'MD_DEVICES', - 'MD_LEVEL', - 'MD_METADATA', - 'MD_UUID' - ] - - def __init__(self, methodName='runTest'): - """Set up the structure of the mdraid array.""" - super(MDRaidAsRootTestCase, self).__init__(methodName=methodName) + def __init__(self, methodName='runTest', deviceSpec=None): + super(MDRaidAsRootTestCase, self).__init__(methodName=methodName, deviceSpec=deviceSpec) self._dev_name = "/dev/md0"
def tearDown(self): try: mdraid.mddeactivate(self._dev_name) for dev in self.loopDevices: + mdraid.mdremove(self._dev_name, dev, fail=True) mdraid.mddestroy(dev) except MDRaidError: pass
super(MDRaidAsRootTestCase, self).tearDown()
- def testMDExamineMDRaidArray(self): - mdraid.mdcreate(self._dev_name, raid.RAID1, self.loopDevices) - # wait for raid to settle - time.sleep(2) - - # invoking mdexamine on the array itself raises an error - with self.assertRaisesRegexp(MDRaidError, "mdexamine failed"): - mdraid.mdexamine(self._dev_name) +class RAID0Test(MDRaidAsRootTestCase):
- def testMDExamineNonMDRaid(self): - # invoking mdexamine on any non-array member raises an error - with self.assertRaisesRegexp(MDRaidError, "mdexamine failed"): - mdraid.mdexamine(self.loopDevices[0]) - - def _testMDExamine(self, names, metadataVersion=None, level=None): - """ Test mdexamine for a specified metadataVersion. - - :param list names: mdexamine's expected list of names to return - :param str metadataVersion: the metadata version for the array - :param object level: any valid RAID level descriptor + def __init__(self, methodName='runTest'): + super(RAID0Test, self).__init__(methodName=methodName, deviceSpec=[102400, 102400, 102400])
- Verifies that: - - exactly the predicted names are returned by mdexamine - - RAID level and number of devices are correct - - UUIDs have canonical form - """ - level = mdraid.RAID_levels.raidLevel(level or raid.RAID1) - mdraid.mdcreate(self._dev_name, level, self.loopDevices, metadataVer=metadataVersion) - # wait for raid to settle - time.sleep(2) + def testGrow(self): + self.assertIsNone(mdraid.mdcreate(self._dev_name, raid.RAID0, self.loopDevices[:2])) + time.sleep(2) # wait for raid to settle
- info = mdraid.mdexamine(self.loopDevices[0]) + # it is not possible to add a managed device to a non-redundant array, + # but it can be grown, by specifying the desired number of members + self.assertIsNone(mdraid.mdadd(self._dev_name, self.loopDevices[2], raid_devices=3))
- # info contains values for exactly names - for n in names: - self.assertIn(n, info, msg="name '%s' not in info" % n) + def testGrowRAID1(self): + self.assertIsNone(mdraid.mdcreate(self._dev_name, raid.RAID1, self.loopDevices[:2])) + time.sleep(2) # wait for raid to settle
- for n in info.keys(): - self.assertIn(n, names, msg="unexpected name '%s' in info" % n) + # a RAID1 array can be grown as well as a RAID0 array + self.assertIsNone(mdraid.mdadd(self._dev_name, self.loopDevices[2], raid_devices=3))
- # check names with predictable values - self.assertEqual(info['MD_DEVICES'], '2') - self.assertEqual(info['MD_LEVEL'], str(level)) + def testGrowTooBig(self): + self.assertIsNone(mdraid.mdcreate(self._dev_name, raid.RAID0, self.loopDevices[:2])) + time.sleep(2) # wait for raid to settle
- # verify that uuids are in canonical form - for name in (k for k in iter(info.keys()) if k.endswith('UUID')): - self.assertTrue(str(uuid.UUID(info[name])) == info[name]) + # if more devices are specified than are available after the + # addition an error is raised + with self.assertRaises(MDRaidError): + mdraid.mdadd(self._dev_name, self.loopDevices[2], raid_devices=4)
- def testMDExamineContainerDefault(self): - self._testMDExamine(self.names_container, level="container") + def testGrowSmaller(self): + self.assertIsNone(mdraid.mdcreate(self._dev_name, raid.RAID0, self.loopDevices[:2])) + time.sleep(2) # wait for raid to settle
- def testMDExamineDefault(self): - self._testMDExamine(self.names_1) + # it is ok to grow an array smaller than its devices + self.assertIsNone(mdraid.mdadd(self._dev_name, self.loopDevices[2], raid_devices=2))
- def testMDExamine0(self): - self._testMDExamine(self.names_0, metadataVersion='0') + def testGrowSimple(self): + self.assertIsNone(mdraid.mdcreate(self._dev_name, raid.RAID0, self.loopDevices[:2])) + time.sleep(2) # wait for raid to settle
- def testMDExamine0_90(self): - self._testMDExamine(self.names_0, metadataVersion='0.90') + # try to simply add a device and things go wrong + with self.assertRaises(MDRaidError): + mdraid.mdadd(self._dev_name, self.loopDevices[2])
- def testMDExamine1(self): - self._testMDExamine(self.names_1, metadataVersion='1') +class SimpleRaidTest(MDRaidAsRootTestCase):
- def testMDExamine1_2(self): - self._testMDExamine(self.names_1, metadataVersion='1.2') + def __init__(self, methodName='runTest'): + super(SimpleRaidTest, self).__init__(methodName=methodName, deviceSpec=[102400, 102400, 102400])
def testMDRaidAsRoot(self): ## ## mdcreate ## # pass - self.assertEqual(mdraid.mdcreate(self._dev_name, raid.RAID1, self.loopDevices), None) + self.assertIsNone(mdraid.mdcreate(self._dev_name, raid.RAID1, self.loopDevices)) + time.sleep(2) # wait for raid to settle
# fail with self.assertRaises(MDRaidError): mdraid.mdcreate("/dev/md1", "raid1", ["/not/existing/dev0", "/not/existing/dev1"])
- ## - ## mddeactivate - ## - # pass - self.assertEqual(mdraid.mddeactivate(self._dev_name), None) - # fail with self.assertRaises(MDRaidError): - mdraid.mddeactivate("/not/existing/md") + mdraid.mdadd(self._dev_name, "/not/existing/device") + time.sleep(2) # wait for raid to settle + + # removing and re-adding a component device should succeed + self.assertIsNone(mdraid.mdremove(self._dev_name, self.loopDevices[2], fail=True)) + time.sleep(2) # wait for raid to settle + self.assertIsNone(mdraid.mdadd(self._dev_name, self.loopDevices[2])) + time.sleep(3) # wait for raid to settle + + # it is not possible to add a device that has already been added + with self.assertRaises(MDRaidError): + mdraid.mdadd(self._dev_name, self.loopDevices[2]) + + self.assertIsNone(mdraid.mdremove(self._dev_name, self.loopDevices[2], fail=True)) + time.sleep(2) # wait for raid to settle + + # can not re-add incrementally, because the array is active + with self.assertRaises(MDRaidError): + mdraid.mdadd(None, self.loopDevices[2], incremental=True)
## - ## mdadd + ## mddeactivate ## # pass - # TODO + self.assertIsNone(mdraid.mddeactivate(self._dev_name)) + + # once the array is deactivated, can add incrementally + self.assertIsNone(mdraid.mdadd(None, self.loopDevices[2], incremental=True)) + + # but cannot re-add twice + with self.assertRaises(MDRaidError): + mdraid.mdadd(None, self.loopDevices[2], incremental=True)
# fail with self.assertRaises(MDRaidError): - mdraid.mdadd(self._dev_name, "/not/existing/device") + mdraid.mddeactivate("/not/existing/md") +
## ## mdactivate @@ -207,7 +180,7 @@ class MDRaidAsRootTestCase(loopbackedtestcase.LoopBackedTestCase): ## # pass for dev in self.loopDevices: - self.assertEqual(mdraid.mddestroy(dev), None) + self.assertIsNone(mdraid.mddestroy(dev))
# pass # Note that these should fail because mdadm is unable to locate the
Incremental mode is really considerably different from add mode.
For one thing, it may cause the array to be started if it was not already, based on the number of devices in the array and the number that ought to be in the array.
Signed-off-by: mulhern amulhern@redhat.com --- blivet/devicelibs/mdraid.py | 36 ++++++++++++++++++++++++++---------- blivet/devices.py | 2 +- tests/devicelibs_test/mdraid_test.py | 10 +++++----- 3 files changed, 32 insertions(+), 16 deletions(-)
diff --git a/blivet/devicelibs/mdraid.py b/blivet/devicelibs/mdraid.py index c4c4264..a391c3f 100644 --- a/blivet/devicelibs/mdraid.py +++ b/blivet/devicelibs/mdraid.py @@ -135,13 +135,35 @@ def mddestroy(device): except MDRaidError as msg: raise MDRaidError("mddestroy failed for %s: %s" % (device, msg))
-def mdadd(array, device, incremental=False, raid_devices=None): +def mdnominate(device): + """ Attempt to add a device to the array to which it belongs. + + Belonging is determined by mdadm's rules. + + May start the array once a sufficient number of devices are added + to the array. + + :param str device: path to the device to add + :rtype: NoneType + :raises: MDRaidError + + .. seealso:: mdadd + """ + args = ['--incremental', '--quiet', device] + + try: + mdadm(args) + except MDRaidError as msg: + raise MDRaidError("mdadd failed for %s: %s" % (device, msg)) + +def mdadd(array, device, raid_devices=None): """ Add a device to an array.
:param str array: path to the array to add the device to :param str device: path to the device to add to the array - :keyword bool incremental: add the device incrementally (see note below) :keyword int raid_devices: the number of active member devices + :rtype: NoneType + :raises: MDRaidError
The raid_devices parameter is used when adding devices to a raid array that has no actual redundancy. In this case it is necessary @@ -151,15 +173,9 @@ def mdadd(array, device, incremental=False, raid_devices=None): Whether the new device will be added as a spare or an active member is decided by mdadm.
- .. note:: - - Incremental add is used during block device discovery and is a - different operation than changing the member set of an array. - + .. seealso:: mdnominate """ - if incremental: - args = ["--incremental", "--quiet"] - elif raid_devices is None: + if raid_devices is None: args = [array, "--add"] else: args = ["--grow", array, "--raid-devices", str(raid_devices), "--add"] diff --git a/blivet/devices.py b/blivet/devices.py index 2a53ad9..476c384 100644 --- a/blivet/devices.py +++ b/blivet/devices.py @@ -3907,7 +3907,7 @@ class MDRaidArrayDevice(ContainerDevice):
if self.spares <= 0: try: - mdraid.mdadd(None, member.path, incremental=True) + mdraid.mdnominate(member.path) # mdadd causes udev events udev.settle() except errors.MDRaidError as e: diff --git a/tests/devicelibs_test/mdraid_test.py b/tests/devicelibs_test/mdraid_test.py index 2ed7a2a..042f1eb 100755 --- a/tests/devicelibs_test/mdraid_test.py +++ b/tests/devicelibs_test/mdraid_test.py @@ -144,9 +144,9 @@ class SimpleRaidTest(MDRaidAsRootTestCase): self.assertIsNone(mdraid.mdremove(self._dev_name, self.loopDevices[2], fail=True)) time.sleep(2) # wait for raid to settle
- # can not re-add incrementally, because the array is active + # can not re-add in incremental mode because the array is active with self.assertRaises(MDRaidError): - mdraid.mdadd(None, self.loopDevices[2], incremental=True) + mdraid.mdnominate(self.loopDevices[2])
## ## mddeactivate @@ -154,12 +154,12 @@ class SimpleRaidTest(MDRaidAsRootTestCase): # pass self.assertIsNone(mdraid.mddeactivate(self._dev_name))
- # once the array is deactivated, can add incrementally - self.assertIsNone(mdraid.mdadd(None, self.loopDevices[2], incremental=True)) + # once the array is deactivated, can add in incremental mode + self.assertIsNone(mdraid.mdnominate(self.loopDevices[2]))
# but cannot re-add twice with self.assertRaises(MDRaidError): - mdraid.mdadd(None, self.loopDevices[2], incremental=True) + mdraid.mdnominate(self.loopDevices[2])
# fail with self.assertRaises(MDRaidError):
Signed-off-by: mulhern amulhern@redhat.com --- blivet/devicelibs/mdraid.py | 1 + 1 file changed, 1 insertion(+)
diff --git a/blivet/devicelibs/mdraid.py b/blivet/devicelibs/mdraid.py index a391c3f..21578a2 100644 --- a/blivet/devicelibs/mdraid.py +++ b/blivet/devicelibs/mdraid.py @@ -315,6 +315,7 @@ def mdexamine(device):
if name == "metadata": info["MD_METADATA"] = value + break
process_UUIDS(info, ('MD_UUID', 'MD_DEV_UUID'))
Signed-off-by: mulhern amulhern@redhat.com --- blivet/devicelibs/mdraid.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/blivet/devicelibs/mdraid.py b/blivet/devicelibs/mdraid.py index 21578a2..f77ef0a 100644 --- a/blivet/devicelibs/mdraid.py +++ b/blivet/devicelibs/mdraid.py @@ -154,7 +154,7 @@ def mdnominate(device): try: mdadm(args) except MDRaidError as msg: - raise MDRaidError("mdadd failed for %s: %s" % (device, msg)) + raise MDRaidError("mdnominate failed for %s: %s" % (device, msg))
def mdadd(array, device, raid_devices=None): """ Add a device to an array.
--- tests/devicelibs_test/mdraid_test.py | 1 + 1 file changed, 1 insertion(+)
diff --git a/tests/devicelibs_test/mdraid_test.py b/tests/devicelibs_test/mdraid_test.py index 042f1eb..4081bd2 100755 --- a/tests/devicelibs_test/mdraid_test.py +++ b/tests/devicelibs_test/mdraid_test.py @@ -113,6 +113,7 @@ class SimpleRaidTest(MDRaidAsRootTestCase):
def __init__(self, methodName='runTest'): super(SimpleRaidTest, self).__init__(methodName=methodName, deviceSpec=[102400, 102400, 102400]) + self.longMessage = True
def testMDRaidAsRoot(self): ##
--- tests/devicelibs_test/mdraid_test.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-)
diff --git a/tests/devicelibs_test/mdraid_test.py b/tests/devicelibs_test/mdraid_test.py index 4081bd2..0daa56e 100755 --- a/tests/devicelibs_test/mdraid_test.py +++ b/tests/devicelibs_test/mdraid_test.py @@ -57,11 +57,17 @@ class MDRaidAsRootTestCase(loopbackedtestcase.LoopBackedTestCase): def tearDown(self): try: mdraid.mddeactivate(self._dev_name) - for dev in self.loopDevices: - mdraid.mdremove(self._dev_name, dev, fail=True) - mdraid.mddestroy(dev) except MDRaidError: pass + for dev in self.loopDevices: + try: + mdraid.mdremove(self._dev_name, dev, fail=True) + except MDRaidError: + pass + try: + mdraid.mddestroy(dev) + except MDRaidError: + pass
super(MDRaidAsRootTestCase, self).tearDown()
Signed-off-by: mulhern amulhern@redhat.com --- tests/devicelibs_test/mdraid_interrogate_test.py | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-)
diff --git a/tests/devicelibs_test/mdraid_interrogate_test.py b/tests/devicelibs_test/mdraid_interrogate_test.py index 0909fe3..2178d79 100755 --- a/tests/devicelibs_test/mdraid_interrogate_test.py +++ b/tests/devicelibs_test/mdraid_interrogate_test.py @@ -174,6 +174,13 @@ class MDDetailTestCase(MDRaidInterrogateTestCase): 'WORKING DEVICES' ]
+ names_container = [ + 'RAID LEVEL', + 'WORKING DEVICES', + 'VERSION', + 'TOTAL DEVICES' + ] + def _testMDDetail(self, names, metadataVersion=None, level=None, spares=0): """ Test mddetail for a specified metadataVersion.
@@ -197,15 +204,17 @@ class MDDetailTestCase(MDRaidInterrogateTestCase): self._matchNames(info.keys(), names, ['RESYNC STATUS'])
# check names with predictable values - self.assertEqual(info['ACTIVE DEVICES'], str(len(self.loopDevices) - spares)) - self.assertEqual(info['FAILED DEVICES'], '0') self.assertEqual(info['RAID LEVEL'], str(level)) - self.assertEqual(info['SPARE DEVICES'], str(spares)) self.assertEqual(info['TOTAL DEVICES'], str(len(self.loopDevices))) self.assertEqual(info['WORKING DEVICES'], str(len(self.loopDevices)))
- # verify that uuid is in canonical form - self.assertEqual(str(uuid.UUID(info['UUID'])), info['UUID']) + if level is not raid.Container: + self.assertEqual(info['ACTIVE DEVICES'], str(len(self.loopDevices) - spares)) + self.assertEqual(info['FAILED DEVICES'], '0') + self.assertEqual(info['SPARE DEVICES'], str(spares)) + + # verify that uuid is in canonical form + self.assertEqual(str(uuid.UUID(info['UUID'])), info['UUID'])
def testMDDetail(self): self._testMDDetail(self.names) @@ -224,5 +233,8 @@ class MDDetailTestCase(MDRaidInterrogateTestCase): with self.assertRaisesRegexp(MDRaidError, "mddetail failed"): mdraid.mddetail(self.loopDevices[0])
+ def testMDDetailContainerDefault(self): + self._testMDDetail(self.names_container, level="container") + if __name__ == "__main__": unittest.main()
--- tests/devicelibs_test/mdraid_test.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+)
diff --git a/tests/devicelibs_test/mdraid_test.py b/tests/devicelibs_test/mdraid_test.py index 0daa56e..e6fdf55 100755 --- a/tests/devicelibs_test/mdraid_test.py +++ b/tests/devicelibs_test/mdraid_test.py @@ -155,6 +155,8 @@ class SimpleRaidTest(MDRaidAsRootTestCase): with self.assertRaises(MDRaidError): mdraid.mdnominate(self.loopDevices[2])
+ info_pre = mdraid.mddetail(self._dev_name) + ## ## mddeactivate ## @@ -182,6 +184,23 @@ class SimpleRaidTest(MDRaidAsRootTestCase): with self.assertRaises(MDRaidError): mdraid.mdactivate("/dev/md1")
+ self.assertIsNone(mdraid.mdactivate(self._dev_name, array_uuid=info_pre['UUID'])) + time.sleep(2) + info_post = mdraid.mddetail(self._dev_name) + + # the array should remain mostly the same across activations + changeable_values = ( + 'ACTIVE DEVICES', + 'STATE', + 'TOTAL DEVICES', + 'WORKING DEVICES' + ) + for k in (k for k in info_pre.keys() if k not in changeable_values): + self.assertEqual(info_pre[k], info_post[k], msg="key: %s" % k) + + # deactivating the array one more time + self.assertIsNone(mdraid.mddeactivate(self._dev_name)) + ## ## mddestroy ##
Resolves: fed#1147087
Sometimes, MD_UUID* are obtained, not from mdraid methods, but from udev methods. Since these did not pass through the mdraid methods, they are not canonicalized, leading to uncanonicalized forms being used for lookup. Now, they are canonicalized on lookup in blivet.util.py. Since the canonicalize_UUID method is shared, it is moved up into blivet.utils.py.
Signed-off-by: mulhern amulhern@redhat.com --- blivet/devicelibs/mdraid.py | 19 +------------------ blivet/udev.py | 5 +++-- blivet/util.py | 20 ++++++++++++++++++++ 3 files changed, 24 insertions(+), 20 deletions(-)
diff --git a/blivet/devicelibs/mdraid.py b/blivet/devicelibs/mdraid.py index f77ef0a..f4ed875 100644 --- a/blivet/devicelibs/mdraid.py +++ b/blivet/devicelibs/mdraid.py @@ -22,7 +22,6 @@
import os import re -import uuid
from .. import util from ..errors import MDRaidError @@ -248,22 +247,6 @@ def mddeactivate(device): except MDRaidError as msg: raise MDRaidError("mddeactivate failed for %s: %s" % (device, msg))
-def canonicalize_UUID(a_uuid): - """ Converts uuids to canonical form. - - :param str a_uuid: the UUID - - :returns: a canonicalized UUID - :rtype: str - - mdadm's UUIDs are actual 128 bit uuids, but it formats them strangely. - This converts the uuids to canonical form. - Example: - mdadm UUID: '3386ff85:f5012621:4a435f06:1eb47236' - canonical UUID: '3386ff85-f501-2621-4a43-5f061eb47236' - """ - return str(uuid.UUID(a_uuid.replace(':', ''))) - def process_UUIDS(info, UUID_keys): """ Extract and convert expected UUIDs to canonical form. Reassign canonicalized UUIDs to corresponding keys. @@ -276,7 +259,7 @@ def process_UUIDS(info, UUID_keys): # extract mdadm UUID, e.g., '3386ff85:f5012621:4a435f06:1eb47236' the_uuid = re.match(r"(([a-f0-9]){8}:){3}([a-f0-9]){8}", v)
- info[k] = canonicalize_UUID(the_uuid.group()) + info[k] = util.canonicalize_UUID(the_uuid.group()) except (ValueError, AttributeError) as e: # the unlikely event that mdadm's UUIDs change their format log.warning('uuid value %s could not be canonicalized: %s', v, e) diff --git a/blivet/udev.py b/blivet/udev.py index 57cf0a8..4f12d5f 100644 --- a/blivet/udev.py +++ b/blivet/udev.py @@ -411,7 +411,7 @@ def device_get_md_uuid(info): # Value for MD_UUID known to be obtained from: # * pyudev/libudev # * mdraid/mdadm (all numeric metadata versions and container default) - return info["MD_UUID"] + return util.canonicalize_UUID(info["MD_UUID"])
def device_get_md_container(info): """ @@ -456,7 +456,8 @@ def device_get_md_device_uuid(info): # Value for MD_UUID known to be obtained from: # * pyudev/libudev # * mdraid/mdadm (only 1.x metadata versions) - return info.get('MD_DEV_UUID') + md_device_uuid = info.get('MD_DEV_UUID') + return util.canonicalize_UUID(md_device_uuid) if md_device_uuid else None
def device_get_vg_name(info): return info['LVM2_VG_NAME'] diff --git a/blivet/util.py b/blivet/util.py index 1e28ddc..d89fee6 100644 --- a/blivet/util.py +++ b/blivet/util.py @@ -8,6 +8,7 @@ import subprocess import re import sys import tempfile +import uuid from decimal import Decimal
import six @@ -361,6 +362,25 @@ class ObjectID(object): self.id = self._newid_gen() # pylint: disable=attribute-defined-outside-init return self
+def canonicalize_UUID(a_uuid): + """ Converts uuids to canonical form. + + :param str a_uuid: the UUID + + :returns: a canonicalized UUID + :rtype: str + + mdadm's UUIDs are actual 128 bit uuids, but it formats them strangely. + This converts the uuids to canonical form. + Example: + mdadm UUID: '3386ff85:f5012621:4a435f06:1eb47236' + canonical UUID: '3386ff85-f501-2621-4a43-5f061eb47236' + + If the UUID is already in canonical form, the conversion + is equivalent to the identity. + """ + return str(uuid.UUID(a_uuid.replace(':', ''))) + ## ## Convenience functions for examples and tests ##
On 09/29/2014 12:22 PM, mulhern wrote:
All but the last are cherry-picks from master.
When I posted the last patch for master/f2-branch, I thought that the error that was seen was due to the switch to pyudev. But, since it's in f21-branch that can not be the case. Nonetheless, uncanonicalized UUIDs are somehow getting in, so the best step is to canonicalize them on access from udev, which is what the last patch does.
The last patch is for both master and f21-branch.
These look fine to me.
David
mulhern (14): Add a docstring to mdraid.mdexamine Factor canonicalize_UUID() into separate method. Be more robust in the face of possible changes to mdadm's UUIDs. Extend mdadm() to capture output Add a method to extract information about an mdraid array Refactor mdraid tests. Split mdadd into separate functions. Break once metadata value is found. Fix mdnominate error message. Use long messages for unittest errors. Still attempt to destroy even if remove failed. Add a test for mddetail on containers. Add a test for activation. Canonicalize MD_UUID* values in udev.py (#1147087)
blivet/devicelibs/mdraid.py | 125 +++++++++--- blivet/devices.py | 2 +- blivet/udev.py | 5 +- blivet/util.py | 20 ++ tests/devicelibs_test/mdraid_interrogate_test.py | 240 +++++++++++++++++++++++ tests/devicelibs_test/mdraid_test.py | 201 ++++++++++--------- 6 files changed, 464 insertions(+), 129 deletions(-) create mode 100755 tests/devicelibs_test/mdraid_interrogate_test.py
anaconda-patches@lists.fedorahosted.org