import re from ..udev import udev_device_is_disk from pyanaconda import iutil from pyanaconda.flags import flags from pyanaconda.anaconda_log import log_method_call import logging log = logging.getLogger("storage") def parseMultipathOutput(output): """ Parse output from "multipath -d" or "multipath -ll" and form a topology. Returns a dictionary: {'mpatha':['sdb','sdc'], 'mpathb': ['sdd', 'sde'], ... } The 'multipath -d' output looks like: create: mpathc (1ATA ST3120026AS 5M) undef ATA,ST3120026AS size=112G features='0' hwhandler='0' wp=undef `-+- policy='round-robin 0' prio=1 status=undef `- 2:0:0:0 sda 8:0 undef ready running create: mpathb (36006016092d21800703762872c60db11) undef DGC,RAID 5 size=10G features='1 queue_if_no_path' hwhandler='1 emc' wp=undef `-+- policy='round-robin 0' prio=2 status=undef |- 6:0:0:0 sdb 8:16 undef ready running `- 7:0:0:0 sdc 8:32 undef ready running create: mpatha (36001438005deb4710000500000270000) dm-0 HP,HSV400 size=20G features='0' hwhandler='0' wp=rw |-+- policy='round-robin 0' prio=-1 status=active | |- 7:0:0:1 sda 8:0 active undef running | `- 7:0:1:1 sdb 8:16 active undef running `-+- policy='round-robin 0' prio=-1 status=enabled |- 7:0:2:1 sdc 8:32 active undef running `- 7:0:3:1 sdd 8:48 active undef running (In anaconda, the first one there won't be included because we blacklist "ATA" as a vendor.) The 'multipath -ll' output looks like (notice the missing 'create' before 'mpatha'): mpatha (3600a0b800067fcc9000001694b557dd1) dm-0 IBM,1726-4xx FAStT size=360G features='0' hwhandler='1 rdac' wp=rw `-+- policy='round-robin 0' prio=3 status=active |- 2:0:0:0 sda 8:0 active ready running `- 3:0:0:0 sdb 8:16 active ready running """ mpaths = {} if output is None: return mpaths name = None devices = [] policy = re.compile('^[|+` -]+policy') device = re.compile('^[|+` -]+[0-9]+:[0-9]+:[0-9]+:[0-9]+ ([a-zA-Z0-9!/]+)') create = re.compile('^(create: )?(mpath\w+|[a-f0-9]+)') lines = output.split('\n') for line in lines: pmatch = policy.match(line) dmatch = device.match(line) cmatch = create.match(line) lexemes = line.split() if not lexemes: break if cmatch and cmatch.group(2): if name and devices: mpaths[name] = devices name = None devices = [] name = cmatch.group(2) elif lexemes[0].startswith('size='): pass elif pmatch: pass elif dmatch: devices.append(dmatch.groups()[0].replace('!','/')) if name and devices: mpaths[name] = devices return mpaths class MultipathTopology(object): def __init__(self, devices_list): self._devices = devices_list self._nondisks = [] self._singlepaths = [] self._multipaths = [] # mpath members self._devmap = {} self._build_topology() def _build_devmap(self): self._devmap = {} for dev in self._devices: self._devmap[dev['name']] = dev def _build_mpath_topology(self): with open("/etc/multipath.conf") as conf: log.debug("/etc/multipath.conf contents:") map(lambda line: log.debug(line.rstrip()), conf) log.debug("(end of /etc/multipath.conf)") self._mpath_topology = parseMultipathOutput( iutil.execWithCapture("multipath", ["-d",])) self._mpath_topology.update(parseMultipathOutput( iutil.execWithCapture("multipath", ["-ll",]))) delete_keys = [] for (mp, disks) in self._mpath_topology.items(): # single device mpath is not really an mpath, eliminate them: if len(disks) < 2: log.info("MultipathTopology: not a multipath: %s" % disks) delete_keys.append(mp) continue # some usb cardreaders use multiple lun's (for different slots) and # report a fake disk serial which is the same for all the lun's # (#517603). find those mpaths and eliminate them: only_non_usbs = [d for d in disks if self._devmap[d].get("ID_USB_DRIVER") != "usb-storage"] if len(only_non_usbs) == 0: log.info("DeviceToppology: found multi lun usb " "mass storage device: %s" % disks) delete_keys.append(mp) map(lambda key: self._mpath_topology.pop(key), delete_keys) def _build_topology(self): log_method_call(self) self._build_devmap() self._build_mpath_topology() for dev in self._devices: name = dev['name'] if not udev_device_is_disk(dev): self._nondisks.append(name) log.info("MultipathTopology: found non-disk device: %s" % name) continue mpath_name = self.multipath_name(name) if mpath_name: dev["ID_FS_TYPE"] = "multipath_member" dev["ID_MPATH_NAME"] = mpath_name log.info("MultipathTopology: found a multipath member of %s: %s " % (mpath_name, name)) continue # it's a disk and not a multipath member (can be a coalesced # multipath) self._singlepaths.append(name) log.info("MultipathTopology: found singlepath device: %s" % name) def devices_iter(self): """ Generator. Yields all disk devices, mpaths members, coalesced mpath devices and partitions. This property guarantees the order of the returned devices is the same as in the device list passed to the object's constructor. """ for device in self._devices: yield device def singlepaths_iter(self): """ Generator. Yields only the singlepath disks. """ for name in self._singlepaths: yield self._devmap[name] def multipath_name(self, mpath_member_name): """ If the mpath_member_name is a member of a multipath device return the name of the device (e.g. mpathc). Else return None. """ for (name, members) in self._mpath_topology.items(): if mpath_member_name in members: return name return None def multipaths_iter(self): """Generator. Yields all the multipath members, in a topology. Every iteration returns a list of mpath member devices forming a multipath. """ for disks in self._mpath_topology.values(): yield [self._devmap[d] for d in disks] class MultipathConfigWriter: def __init__(self): self.blacklist_devices = [] self.mpaths = [] def addBlacklistDevice(self, device): self.blacklist_devices.append(device) def addMultipathDevice(self, mpath): self.mpaths.append(mpath) def write(self, friendly_names): # if you add anything here, be sure and also add it to anaconda's # multipath.conf ret = '' ret += """\ # multipath.conf written by anaconda defaults { user_friendly_names %(friendly_names)s } blacklist { devnode "^(ram|raw|loop|fd|md|dm-|sr|scd|st)[0-9]*" devnode "^hd[a-z]" devnode "^dcssblk[0-9]*" device { vendor "DGC" product "LUNZ" } device { vendor "IBM" product "S/390.*" } # don't count normal SATA devices as multipaths device { vendor "ATA" } # don't count 3ware devices as multipaths device { vendor "3ware" } device { vendor "AMCC" } # nor highpoint devices device { vendor "HPT" } """ % {'friendly_names' : "yes" if friendly_names else "no"} for device in self.blacklist_devices: if device.serial: ret += '\twwid "%s"\n' % device.serial elif device.vendor and device.model: ret += '\tdevice {\n' ret += '\t\tvendor %s\n' % device.vendor ret += '\t\tproduct %s\n' % device.model ret += '\t}\n' if self.mpaths: ret += '\twwid "*"\n' ret += '}\n' ret += 'blacklist_exceptions {\n' for mpath in self.mpaths: for k,v in mpath.config.items(): if k == 'wwid': ret += '\twwid "%s"\n' % v ret += '}\n' ret += 'multipaths {\n' for mpath in self.mpaths: ret += '\tmultipath {\n' for k,v in mpath.config.items(): if k == 'wwid': ret += '\t\twwid "%s"\n' % v else: ret += '\t\t%s %s\n' % (k, v) ret += '\t}\n' ret += '}\n' return ret def writeConfig(self, friendly_names=True): if not flags.mpath: # not writing out a multipath.conf will effectively blacklist all # mpath which will prevent any of them from being activated during # install return cfg = self.write(friendly_names) with open("/etc/multipath.conf", "w+") as mpath_cfg: mpath_cfg.write(cfg) def flush_mpaths(): iutil.execWithRedirect("multipath", ["-F"]) check_output = iutil.execWithCapture("multipath", ["-ll"]).strip() if check_output: log.error("multipath: some devices could not be flushed")