4 # Copyright (C) 2006, 2007, 2010, 2011 Google Inc.
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 2 of the License, or
9 # (at your option) any later version.
11 # This program is distributed in the hope that it will be useful, but
12 # WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 # General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
22 """Block device abstraction"""
28 import pyparsing as pyp
32 from ganeti import utils
33 from ganeti import errors
34 from ganeti import constants
35 from ganeti import objects
36 from ganeti import compat
37 from ganeti import netutils
40 # Size of reads in _CanReadDevice
41 _DEVICE_READ_SIZE = 128 * 1024
44 def _IgnoreError(fn, *args, **kwargs):
45 """Executes the given function, ignoring BlockDeviceErrors.
47 This is used in order to simplify the execution of cleanup or
51 @return: True when fn didn't raise an exception, False otherwise
57 except errors.BlockDeviceError, err:
58 logging.warning("Caught BlockDeviceError but ignoring: %s", str(err))
62 def _ThrowError(msg, *args):
63 """Log an error to the node daemon and the raise an exception.
66 @param msg: the text of the exception
67 @raise errors.BlockDeviceError
73 raise errors.BlockDeviceError(msg)
76 def _CanReadDevice(path):
77 """Check if we can read from the given device.
79 This tries to read the first 128k of the device.
83 utils.ReadFile(path, size=_DEVICE_READ_SIZE)
85 except EnvironmentError:
86 logging.warning("Can't read from device %s", path, exc_info=True)
90 class BlockDev(object):
91 """Block device abstract class.
93 A block device can be in the following states:
94 - not existing on the system, and by `Create()` it goes into:
95 - existing but not setup/not active, and by `Assemble()` goes into:
96 - active read-write and by `Open()` it goes into
97 - online (=used, or ready for use)
99 A device can also be online but read-only, however we are not using
100 the readonly state (LV has it, if needed in the future) and we are
101 usually looking at this like at a stack, so it's easier to
102 conceptualise the transition from not-existing to online and back
105 The many different states of the device are due to the fact that we
106 need to cover many device types:
107 - logical volumes are created, lvchange -a y $lv, and used
108 - drbd devices are attached to a local disk/remote peer and made primary
110 A block device is identified by three items:
111 - the /dev path of the device (dynamic)
112 - a unique ID of the device (static)
113 - it's major/minor pair (dynamic)
115 Not all devices implement both the first two as distinct items. LVM
116 logical volumes have their unique ID (the pair volume group, logical
117 volume name) in a 1-to-1 relation to the dev path. For DRBD devices,
118 the /dev path is again dynamic and the unique id is the pair (host1,
119 dev1), (host2, dev2).
121 You can get to a device in two ways:
122 - creating the (real) device, which returns you
123 an attached instance (lvcreate)
124 - attaching of a python instance to an existing (real) device
126 The second point, the attachement to a device, is different
127 depending on whether the device is assembled or not. At init() time,
128 we search for a device with the same unique_id as us. If found,
129 good. It also means that the device is already assembled. If not,
130 after assembly we'll have our correct major/minor.
133 def __init__(self, unique_id, children, size, params):
134 self._children = children
136 self.unique_id = unique_id
139 self.attached = False
144 """Assemble the device from its components.
146 Implementations of this method by child classes must ensure that:
147 - after the device has been assembled, it knows its major/minor
148 numbers; this allows other devices (usually parents) to probe
149 correctly for their children
150 - calling this method on an existing, in-use device is safe
151 - if the device is already configured (and in an OK state),
152 this method is idempotent
158 """Find a device which matches our config and attach to it.
161 raise NotImplementedError
164 """Notifies that the device will no longer be used for I/O.
167 raise NotImplementedError
170 def Create(cls, unique_id, children, size, params):
171 """Create the device.
173 If the device cannot be created, it will return None
174 instead. Error messages go to the logging system.
176 Note that for some devices, the unique_id is used, and for other,
177 the children. The idea is that these two, taken together, are
178 enough for both creation and assembly (later).
181 raise NotImplementedError
184 """Remove this device.
186 This makes sense only for some of the device types: LV and file
187 storage. Also note that if the device can't attach, the removal
191 raise NotImplementedError
193 def Rename(self, new_id):
194 """Rename this device.
196 This may or may not make sense for a given device type.
199 raise NotImplementedError
201 def Open(self, force=False):
202 """Make the device ready for use.
204 This makes the device ready for I/O. For now, just the DRBD
207 The force parameter signifies that if the device has any kind of
208 --force thing, it should be used, we know what we are doing.
211 raise NotImplementedError
214 """Shut down the device, freeing its children.
216 This undoes the `Assemble()` work, except for the child
217 assembling; as such, the children on the device are still
218 assembled after this call.
221 raise NotImplementedError
223 def SetSyncSpeed(self, speed):
224 """Adjust the sync speed of the mirror.
226 In case this is not a mirroring device, this is no-op.
231 for child in self._children:
232 result = result and child.SetSyncSpeed(speed)
235 def PauseResumeSync(self, pause):
236 """Pause/Resume the sync of the mirror.
238 In case this is not a mirroring device, this is no-op.
240 @param pause: Wheater to pause or resume
245 for child in self._children:
246 result = result and child.PauseResumeSync(pause)
249 def GetSyncStatus(self):
250 """Returns the sync status of the device.
252 If this device is a mirroring device, this function returns the
253 status of the mirror.
255 If sync_percent is None, it means the device is not syncing.
257 If estimated_time is None, it means we can't estimate
258 the time needed, otherwise it's the time left in seconds.
260 If is_degraded is True, it means the device is missing
261 redundancy. This is usually a sign that something went wrong in
262 the device setup, if sync_percent is None.
264 The ldisk parameter represents the degradation of the local
265 data. This is only valid for some devices, the rest will always
266 return False (not degraded).
268 @rtype: objects.BlockDevStatus
271 return objects.BlockDevStatus(dev_path=self.dev_path,
277 ldisk_status=constants.LDS_OKAY)
279 def CombinedSyncStatus(self):
280 """Calculate the mirror status recursively for our children.
282 The return value is the same as for `GetSyncStatus()` except the
283 minimum percent and maximum time are calculated across our
286 @rtype: objects.BlockDevStatus
289 status = self.GetSyncStatus()
291 min_percent = status.sync_percent
292 max_time = status.estimated_time
293 is_degraded = status.is_degraded
294 ldisk_status = status.ldisk_status
297 for child in self._children:
298 child_status = child.GetSyncStatus()
300 if min_percent is None:
301 min_percent = child_status.sync_percent
302 elif child_status.sync_percent is not None:
303 min_percent = min(min_percent, child_status.sync_percent)
306 max_time = child_status.estimated_time
307 elif child_status.estimated_time is not None:
308 max_time = max(max_time, child_status.estimated_time)
310 is_degraded = is_degraded or child_status.is_degraded
312 if ldisk_status is None:
313 ldisk_status = child_status.ldisk_status
314 elif child_status.ldisk_status is not None:
315 ldisk_status = max(ldisk_status, child_status.ldisk_status)
317 return objects.BlockDevStatus(dev_path=self.dev_path,
320 sync_percent=min_percent,
321 estimated_time=max_time,
322 is_degraded=is_degraded,
323 ldisk_status=ldisk_status)
325 def SetInfo(self, text):
326 """Update metadata with info text.
328 Only supported for some device types.
331 for child in self._children:
334 def Grow(self, amount, dryrun):
335 """Grow the block device.
337 @type amount: integer
338 @param amount: the amount (in mebibytes) to grow with
339 @type dryrun: boolean
340 @param dryrun: whether to execute the operation in simulation mode
341 only, without actually increasing the size
344 raise NotImplementedError
346 def GetActualSize(self):
347 """Return the actual disk size.
349 @note: the device needs to be active when this is called
352 assert self.attached, "BlockDevice not attached in GetActualSize()"
353 result = utils.RunCmd(["blockdev", "--getsize64", self.dev_path])
355 _ThrowError("blockdev failed (%s): %s",
356 result.fail_reason, result.output)
358 sz = int(result.output.strip())
359 except (ValueError, TypeError), err:
360 _ThrowError("Failed to parse blockdev output: %s", str(err))
364 return ("<%s: unique_id: %s, children: %s, %s:%s, %s>" %
365 (self.__class__, self.unique_id, self._children,
366 self.major, self.minor, self.dev_path))
369 class LogicalVolume(BlockDev):
370 """Logical Volume block device.
373 _VALID_NAME_RE = re.compile("^[a-zA-Z0-9+_.-]*$")
374 _INVALID_NAMES = frozenset([".", "..", "snapshot", "pvmove"])
375 _INVALID_SUBSTRINGS = frozenset(["_mlog", "_mimage"])
377 def __init__(self, unique_id, children, size, params):
378 """Attaches to a LV device.
380 The unique_id is a tuple (vg_name, lv_name)
383 super(LogicalVolume, self).__init__(unique_id, children, size, params)
384 if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
385 raise ValueError("Invalid configuration data %s" % str(unique_id))
386 self._vg_name, self._lv_name = unique_id
387 self._ValidateName(self._vg_name)
388 self._ValidateName(self._lv_name)
389 self.dev_path = utils.PathJoin("/dev", self._vg_name, self._lv_name)
390 self._degraded = True
391 self.major = self.minor = self.pe_size = self.stripe_count = None
395 def Create(cls, unique_id, children, size, params):
396 """Create a new logical volume.
399 if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
400 raise errors.ProgrammerError("Invalid configuration data %s" %
402 vg_name, lv_name = unique_id
403 cls._ValidateName(vg_name)
404 cls._ValidateName(lv_name)
405 pvs_info = cls.GetPVInfo([vg_name])
407 _ThrowError("Can't compute PV info for vg %s", vg_name)
411 pvlist = [pv[1] for pv in pvs_info]
412 if compat.any(":" in v for v in pvlist):
413 _ThrowError("Some of your PVs have the invalid character ':' in their"
414 " name, this is not supported - please filter them out"
415 " in lvm.conf using either 'filter' or 'preferred_names'")
416 free_size = sum([pv[0] for pv in pvs_info])
417 current_pvs = len(pvlist)
418 stripes = min(current_pvs, constants.LVM_STRIPECOUNT)
420 # The size constraint should have been checked from the master before
421 # calling the create function.
423 _ThrowError("Not enough free space: required %s,"
424 " available %s", size, free_size)
425 cmd = ["lvcreate", "-L%dm" % size, "-n%s" % lv_name]
426 # If the free space is not well distributed, we won't be able to
427 # create an optimally-striped volume; in that case, we want to try
428 # with N, N-1, ..., 2, and finally 1 (non-stripped) number of
430 for stripes_arg in range(stripes, 0, -1):
431 result = utils.RunCmd(cmd + ["-i%d" % stripes_arg] + [vg_name] + pvlist)
432 if not result.failed:
435 _ThrowError("LV create failed (%s): %s",
436 result.fail_reason, result.output)
437 return LogicalVolume(unique_id, children, size, params)
440 def _GetVolumeInfo(lvm_cmd, fields):
441 """Returns LVM Volumen infos using lvm_cmd
443 @param lvm_cmd: Should be one of "pvs", "vgs" or "lvs"
444 @param fields: Fields to return
445 @return: A list of dicts each with the parsed fields
449 raise errors.ProgrammerError("No fields specified")
452 cmd = [lvm_cmd, "--noheadings", "--nosuffix", "--units=m", "--unbuffered",
453 "--separator=%s" % sep, "-o%s" % ",".join(fields)]
455 result = utils.RunCmd(cmd)
457 raise errors.CommandError("Can't get the volume information: %s - %s" %
458 (result.fail_reason, result.output))
461 for line in result.stdout.splitlines():
462 splitted_fields = line.strip().split(sep)
464 if len(fields) != len(splitted_fields):
465 raise errors.CommandError("Can't parse %s output: line '%s'" %
468 data.append(splitted_fields)
473 def GetPVInfo(cls, vg_names, filter_allocatable=True):
474 """Get the free space info for PVs in a volume group.
476 @param vg_names: list of volume group names, if empty all will be returned
477 @param filter_allocatable: whether to skip over unallocatable PVs
480 @return: list of tuples (free_space, name) with free_space in mebibytes
484 info = cls._GetVolumeInfo("pvs", ["pv_name", "vg_name", "pv_free",
486 except errors.GenericError, err:
487 logging.error("Can't get PV information: %s", err)
491 for pv_name, vg_name, pv_free, pv_attr in info:
492 # (possibly) skip over pvs which are not allocatable
493 if filter_allocatable and pv_attr[0] != "a":
495 # (possibly) skip over pvs which are not in the right volume group(s)
496 if vg_names and vg_name not in vg_names:
498 data.append((float(pv_free), pv_name, vg_name))
503 def GetVGInfo(cls, vg_names, filter_readonly=True):
504 """Get the free space info for specific VGs.
506 @param vg_names: list of volume group names, if empty all will be returned
507 @param filter_readonly: whether to skip over readonly VGs
510 @return: list of tuples (free_space, total_size, name) with free_space in
515 info = cls._GetVolumeInfo("vgs", ["vg_name", "vg_free", "vg_attr",
517 except errors.GenericError, err:
518 logging.error("Can't get VG information: %s", err)
522 for vg_name, vg_free, vg_attr, vg_size in info:
523 # (possibly) skip over vgs which are not writable
524 if filter_readonly and vg_attr[0] == "r":
526 # (possibly) skip over vgs which are not in the right volume group(s)
527 if vg_names and vg_name not in vg_names:
529 data.append((float(vg_free), float(vg_size), vg_name))
534 def _ValidateName(cls, name):
535 """Validates that a given name is valid as VG or LV name.
537 The list of valid characters and restricted names is taken out of
538 the lvm(8) manpage, with the simplification that we enforce both
539 VG and LV restrictions on the names.
542 if (not cls._VALID_NAME_RE.match(name) or
543 name in cls._INVALID_NAMES or
544 compat.any(substring in name for substring in cls._INVALID_SUBSTRINGS)):
545 _ThrowError("Invalid LVM name '%s'", name)
548 """Remove this logical volume.
551 if not self.minor and not self.Attach():
552 # the LV does not exist
554 result = utils.RunCmd(["lvremove", "-f", "%s/%s" %
555 (self._vg_name, self._lv_name)])
557 _ThrowError("Can't lvremove: %s - %s", result.fail_reason, result.output)
559 def Rename(self, new_id):
560 """Rename this logical volume.
563 if not isinstance(new_id, (tuple, list)) or len(new_id) != 2:
564 raise errors.ProgrammerError("Invalid new logical id '%s'" % new_id)
565 new_vg, new_name = new_id
566 if new_vg != self._vg_name:
567 raise errors.ProgrammerError("Can't move a logical volume across"
568 " volume groups (from %s to to %s)" %
569 (self._vg_name, new_vg))
570 result = utils.RunCmd(["lvrename", new_vg, self._lv_name, new_name])
572 _ThrowError("Failed to rename the logical volume: %s", result.output)
573 self._lv_name = new_name
574 self.dev_path = utils.PathJoin("/dev", self._vg_name, self._lv_name)
577 """Attach to an existing LV.
579 This method will try to see if an existing and active LV exists
580 which matches our name. If so, its major/minor will be
584 self.attached = False
585 result = utils.RunCmd(["lvs", "--noheadings", "--separator=,",
586 "--units=m", "--nosuffix",
587 "-olv_attr,lv_kernel_major,lv_kernel_minor,"
588 "vg_extent_size,stripes", self.dev_path])
590 logging.error("Can't find LV %s: %s, %s",
591 self.dev_path, result.fail_reason, result.output)
593 # the output can (and will) have multiple lines for multi-segment
594 # LVs, as the 'stripes' parameter is a segment one, so we take
595 # only the last entry, which is the one we're interested in; note
596 # that with LVM2 anyway the 'stripes' value must be constant
597 # across segments, so this is a no-op actually
598 out = result.stdout.splitlines()
599 if not out: # totally empty result? splitlines() returns at least
600 # one line for any non-empty string
601 logging.error("Can't parse LVS output, no lines? Got '%s'", str(out))
603 out = out[-1].strip().rstrip(",")
606 logging.error("Can't parse LVS output, len(%s) != 5", str(out))
609 status, major, minor, pe_size, stripes = out
611 logging.error("lvs lv_attr is not 6 characters (%s)", status)
617 except (TypeError, ValueError), err:
618 logging.error("lvs major/minor cannot be parsed: %s", str(err))
621 pe_size = int(float(pe_size))
622 except (TypeError, ValueError), err:
623 logging.error("Can't parse vg extent size: %s", err)
627 stripes = int(stripes)
628 except (TypeError, ValueError), err:
629 logging.error("Can't parse the number of stripes: %s", err)
634 self.pe_size = pe_size
635 self.stripe_count = stripes
636 self._degraded = status[0] == "v" # virtual volume, i.e. doesn't backing
642 """Assemble the device.
644 We always run `lvchange -ay` on the LV to ensure it's active before
645 use, as there were cases when xenvg was not active after boot
646 (also possibly after disk issues).
649 result = utils.RunCmd(["lvchange", "-ay", self.dev_path])
651 _ThrowError("Can't activate lv %s: %s", self.dev_path, result.output)
654 """Shutdown the device.
656 This is a no-op for the LV device type, as we don't deactivate the
662 def GetSyncStatus(self):
663 """Returns the sync status of the device.
665 If this device is a mirroring device, this function returns the
666 status of the mirror.
668 For logical volumes, sync_percent and estimated_time are always
669 None (no recovery in progress, as we don't handle the mirrored LV
670 case). The is_degraded parameter is the inverse of the ldisk
673 For the ldisk parameter, we check if the logical volume has the
674 'virtual' type, which means it's not backed by existing storage
675 anymore (read from it return I/O error). This happens after a
676 physical disk failure and subsequent 'vgreduce --removemissing' on
679 The status was already read in Attach, so we just return it.
681 @rtype: objects.BlockDevStatus
685 ldisk_status = constants.LDS_FAULTY
687 ldisk_status = constants.LDS_OKAY
689 return objects.BlockDevStatus(dev_path=self.dev_path,
694 is_degraded=self._degraded,
695 ldisk_status=ldisk_status)
697 def Open(self, force=False):
698 """Make the device ready for I/O.
700 This is a no-op for the LV device type.
706 """Notifies that the device will no longer be used for I/O.
708 This is a no-op for the LV device type.
713 def Snapshot(self, size):
714 """Create a snapshot copy of an lvm block device.
716 @returns: tuple (vg, lv)
719 snap_name = self._lv_name + ".snap"
721 # remove existing snapshot if found
722 snap = LogicalVolume((self._vg_name, snap_name), None, size, self.params)
723 _IgnoreError(snap.Remove)
725 vg_info = self.GetVGInfo([self._vg_name])
727 _ThrowError("Can't compute VG info for vg %s", self._vg_name)
728 free_size, _, _ = vg_info[0]
730 _ThrowError("Not enough free space: required %s,"
731 " available %s", size, free_size)
733 result = utils.RunCmd(["lvcreate", "-L%dm" % size, "-s",
734 "-n%s" % snap_name, self.dev_path])
736 _ThrowError("command: %s error: %s - %s",
737 result.cmd, result.fail_reason, result.output)
739 return (self._vg_name, snap_name)
741 def SetInfo(self, text):
742 """Update metadata with info text.
745 BlockDev.SetInfo(self, text)
747 # Replace invalid characters
748 text = re.sub("^[^A-Za-z0-9_+.]", "_", text)
749 text = re.sub("[^-A-Za-z0-9_+.]", "_", text)
751 # Only up to 128 characters are allowed
754 result = utils.RunCmd(["lvchange", "--addtag", text,
757 _ThrowError("Command: %s error: %s - %s", result.cmd, result.fail_reason,
760 def Grow(self, amount, dryrun):
761 """Grow the logical volume.
764 if self.pe_size is None or self.stripe_count is None:
765 if not self.Attach():
766 _ThrowError("Can't attach to LV during Grow()")
767 full_stripe_size = self.pe_size * self.stripe_count
768 rest = amount % full_stripe_size
770 amount += full_stripe_size - rest
771 cmd = ["lvextend", "-L", "+%dm" % amount]
774 # we try multiple algorithms since the 'best' ones might not have
775 # space available in the right place, but later ones might (since
776 # they have less constraints); also note that only recent LVM
778 for alloc_policy in "contiguous", "cling", "normal":
779 result = utils.RunCmd(cmd + ["--alloc", alloc_policy, self.dev_path])
780 if not result.failed:
782 _ThrowError("Can't grow LV %s: %s", self.dev_path, result.output)
785 class DRBD8Status(object):
786 """A DRBD status representation class.
788 Note that this doesn't support unconfigured devices (cs:Unconfigured).
791 UNCONF_RE = re.compile(r"\s*[0-9]+:\s*cs:Unconfigured$")
792 LINE_RE = re.compile(r"\s*[0-9]+:\s*cs:(\S+)\s+(?:st|ro):([^/]+)/(\S+)"
793 "\s+ds:([^/]+)/(\S+)\s+.*$")
794 SYNC_RE = re.compile(r"^.*\ssync'ed:\s*([0-9.]+)%.*"
795 # Due to a bug in drbd in the kernel, introduced in
796 # commit 4b0715f096 (still unfixed as of 2011-08-22)
798 "finish: ([0-9]+):([0-9]+):([0-9]+)\s.*$")
800 CS_UNCONFIGURED = "Unconfigured"
801 CS_STANDALONE = "StandAlone"
802 CS_WFCONNECTION = "WFConnection"
803 CS_WFREPORTPARAMS = "WFReportParams"
804 CS_CONNECTED = "Connected"
805 CS_STARTINGSYNCS = "StartingSyncS"
806 CS_STARTINGSYNCT = "StartingSyncT"
807 CS_WFBITMAPS = "WFBitMapS"
808 CS_WFBITMAPT = "WFBitMapT"
809 CS_WFSYNCUUID = "WFSyncUUID"
810 CS_SYNCSOURCE = "SyncSource"
811 CS_SYNCTARGET = "SyncTarget"
812 CS_PAUSEDSYNCS = "PausedSyncS"
813 CS_PAUSEDSYNCT = "PausedSyncT"
814 CSET_SYNC = frozenset([
827 DS_DISKLESS = "Diskless"
828 DS_ATTACHING = "Attaching" # transient state
829 DS_FAILED = "Failed" # transient state, next: diskless
830 DS_NEGOTIATING = "Negotiating" # transient state
831 DS_INCONSISTENT = "Inconsistent" # while syncing or after creation
832 DS_OUTDATED = "Outdated"
833 DS_DUNKNOWN = "DUnknown" # shown for peer disk when not connected
834 DS_CONSISTENT = "Consistent"
835 DS_UPTODATE = "UpToDate" # normal state
837 RO_PRIMARY = "Primary"
838 RO_SECONDARY = "Secondary"
839 RO_UNKNOWN = "Unknown"
841 def __init__(self, procline):
842 u = self.UNCONF_RE.match(procline)
844 self.cstatus = self.CS_UNCONFIGURED
845 self.lrole = self.rrole = self.ldisk = self.rdisk = None
847 m = self.LINE_RE.match(procline)
849 raise errors.BlockDeviceError("Can't parse input data '%s'" % procline)
850 self.cstatus = m.group(1)
851 self.lrole = m.group(2)
852 self.rrole = m.group(3)
853 self.ldisk = m.group(4)
854 self.rdisk = m.group(5)
856 # end reading of data from the LINE_RE or UNCONF_RE
858 self.is_standalone = self.cstatus == self.CS_STANDALONE
859 self.is_wfconn = self.cstatus == self.CS_WFCONNECTION
860 self.is_connected = self.cstatus == self.CS_CONNECTED
861 self.is_primary = self.lrole == self.RO_PRIMARY
862 self.is_secondary = self.lrole == self.RO_SECONDARY
863 self.peer_primary = self.rrole == self.RO_PRIMARY
864 self.peer_secondary = self.rrole == self.RO_SECONDARY
865 self.both_primary = self.is_primary and self.peer_primary
866 self.both_secondary = self.is_secondary and self.peer_secondary
868 self.is_diskless = self.ldisk == self.DS_DISKLESS
869 self.is_disk_uptodate = self.ldisk == self.DS_UPTODATE
871 self.is_in_resync = self.cstatus in self.CSET_SYNC
872 self.is_in_use = self.cstatus != self.CS_UNCONFIGURED
874 m = self.SYNC_RE.match(procline)
876 self.sync_percent = float(m.group(1))
877 hours = int(m.group(2))
878 minutes = int(m.group(3))
879 seconds = int(m.group(4))
880 self.est_time = hours * 3600 + minutes * 60 + seconds
882 # we have (in this if branch) no percent information, but if
883 # we're resyncing we need to 'fake' a sync percent information,
884 # as this is how cmdlib determines if it makes sense to wait for
886 if self.is_in_resync:
887 self.sync_percent = 0
889 self.sync_percent = None
893 class BaseDRBD(BlockDev): # pylint: disable=W0223
896 This class contains a few bits of common functionality between the
897 0.7 and 8.x versions of DRBD.
900 _VERSION_RE = re.compile(r"^version: (\d+)\.(\d+)\.(\d+)(?:\.\d+)?"
901 r" \(api:(\d+)/proto:(\d+)(?:-(\d+))?\)")
902 _VALID_LINE_RE = re.compile("^ *([0-9]+): cs:([^ ]+).*$")
903 _UNUSED_LINE_RE = re.compile("^ *([0-9]+): cs:Unconfigured$")
906 _ST_UNCONFIGURED = "Unconfigured"
907 _ST_WFCONNECTION = "WFConnection"
908 _ST_CONNECTED = "Connected"
910 _STATUS_FILE = "/proc/drbd"
911 _USERMODE_HELPER_FILE = "/sys/module/drbd/parameters/usermode_helper"
914 def _GetProcData(filename=_STATUS_FILE):
915 """Return data from /proc/drbd.
919 data = utils.ReadFile(filename).splitlines()
920 except EnvironmentError, err:
921 if err.errno == errno.ENOENT:
922 _ThrowError("The file %s cannot be opened, check if the module"
923 " is loaded (%s)", filename, str(err))
925 _ThrowError("Can't read the DRBD proc file %s: %s", filename, str(err))
927 _ThrowError("Can't read any data from %s", filename)
931 def _MassageProcData(cls, data):
932 """Transform the output of _GetProdData into a nicer form.
934 @return: a dictionary of minor: joined lines from /proc/drbd
939 old_minor = old_line = None
941 if not line: # completely empty lines, as can be returned by drbd8.0+
943 lresult = cls._VALID_LINE_RE.match(line)
944 if lresult is not None:
945 if old_minor is not None:
946 results[old_minor] = old_line
947 old_minor = int(lresult.group(1))
950 if old_minor is not None:
951 old_line += " " + line.strip()
953 if old_minor is not None:
954 results[old_minor] = old_line
958 def _GetVersion(cls, proc_data):
959 """Return the DRBD version.
961 This will return a dict with keys:
967 - proto2 (only on drbd > 8.2.X)
970 first_line = proc_data[0].strip()
971 version = cls._VERSION_RE.match(first_line)
973 raise errors.BlockDeviceError("Can't parse DRBD version from '%s'" %
976 values = version.groups()
977 retval = {"k_major": int(values[0]),
978 "k_minor": int(values[1]),
979 "k_point": int(values[2]),
980 "api": int(values[3]),
981 "proto": int(values[4]),
983 if values[5] is not None:
984 retval["proto2"] = values[5]
989 def GetUsermodeHelper(filename=_USERMODE_HELPER_FILE):
990 """Returns DRBD usermode_helper currently set.
994 helper = utils.ReadFile(filename).splitlines()[0]
995 except EnvironmentError, err:
996 if err.errno == errno.ENOENT:
997 _ThrowError("The file %s cannot be opened, check if the module"
998 " is loaded (%s)", filename, str(err))
1000 _ThrowError("Can't read DRBD helper file %s: %s", filename, str(err))
1002 _ThrowError("Can't read any data from %s", filename)
1006 def _DevPath(minor):
1007 """Return the path to a drbd device for a given minor.
1010 return "/dev/drbd%d" % minor
1013 def GetUsedDevs(cls):
1014 """Compute the list of used DRBD devices.
1017 data = cls._GetProcData()
1021 match = cls._VALID_LINE_RE.match(line)
1024 minor = int(match.group(1))
1025 state = match.group(2)
1026 if state == cls._ST_UNCONFIGURED:
1028 used_devs[minor] = state, line
1032 def _SetFromMinor(self, minor):
1033 """Set our parameters based on the given minor.
1035 This sets our minor variable and our dev_path.
1039 self.minor = self.dev_path = None
1040 self.attached = False
1043 self.dev_path = self._DevPath(minor)
1044 self.attached = True
1047 def _CheckMetaSize(meta_device):
1048 """Check if the given meta device looks like a valid one.
1050 This currently only check the size, which must be around
1054 result = utils.RunCmd(["blockdev", "--getsize", meta_device])
1056 _ThrowError("Failed to get device size: %s - %s",
1057 result.fail_reason, result.output)
1059 sectors = int(result.stdout)
1060 except (TypeError, ValueError):
1061 _ThrowError("Invalid output from blockdev: '%s'", result.stdout)
1062 num_bytes = sectors * 512
1063 if num_bytes < 128 * 1024 * 1024: # less than 128MiB
1064 _ThrowError("Meta device too small (%.2fMib)", (num_bytes / 1024 / 1024))
1065 # the maximum *valid* size of the meta device when living on top
1066 # of LVM is hard to compute: it depends on the number of stripes
1067 # and the PE size; e.g. a 2-stripe, 64MB PE will result in a 128MB
1068 # (normal size), but an eight-stripe 128MB PE will result in a 1GB
1069 # size meta device; as such, we restrict it to 1GB (a little bit
1070 # too generous, but making assumptions about PE size is hard)
1071 if num_bytes > 1024 * 1024 * 1024:
1072 _ThrowError("Meta device too big (%.2fMiB)", (num_bytes / 1024 / 1024))
1074 def Rename(self, new_id):
1077 This is not supported for drbd devices.
1080 raise errors.ProgrammerError("Can't rename a drbd device")
1083 class DRBD8(BaseDRBD):
1084 """DRBD v8.x block device.
1086 This implements the local host part of the DRBD device, i.e. it
1087 doesn't do anything to the supposed peer. If you need a fully
1088 connected DRBD pair, you need to use this class on both hosts.
1090 The unique_id for the drbd device is the (local_ip, local_port,
1091 remote_ip, remote_port) tuple, and it must have two children: the
1092 data device and the meta_device. The meta device is checked for
1093 valid size and is zeroed on create.
1100 _NET_RECONFIG_TIMEOUT = 60
1102 def __init__(self, unique_id, children, size, params):
1103 if children and children.count(None) > 0:
1105 if len(children) not in (0, 2):
1106 raise ValueError("Invalid configuration data %s" % str(children))
1107 if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 6:
1108 raise ValueError("Invalid configuration data %s" % str(unique_id))
1109 (self._lhost, self._lport,
1110 self._rhost, self._rport,
1111 self._aminor, self._secret) = unique_id
1113 if not _CanReadDevice(children[1].dev_path):
1114 logging.info("drbd%s: Ignoring unreadable meta device", self._aminor)
1116 super(DRBD8, self).__init__(unique_id, children, size, params)
1117 self.major = self._DRBD_MAJOR
1118 version = self._GetVersion(self._GetProcData())
1119 if version["k_major"] != 8:
1120 _ThrowError("Mismatch in DRBD kernel version and requested ganeti"
1121 " usage: kernel is %s.%s, ganeti wants 8.x",
1122 version["k_major"], version["k_minor"])
1124 if (self._lhost is not None and self._lhost == self._rhost and
1125 self._lport == self._rport):
1126 raise ValueError("Invalid configuration data, same local/remote %s" %
1131 def _InitMeta(cls, minor, dev_path):
1132 """Initialize a meta device.
1134 This will not work if the given minor is in use.
1137 # Zero the metadata first, in order to make sure drbdmeta doesn't
1138 # try to auto-detect existing filesystems or similar (see
1139 # http://code.google.com/p/ganeti/issues/detail?id=182); we only
1140 # care about the first 128MB of data in the device, even though it
1142 result = utils.RunCmd([constants.DD_CMD,
1143 "if=/dev/zero", "of=%s" % dev_path,
1144 "bs=1048576", "count=128", "oflag=direct"])
1146 _ThrowError("Can't wipe the meta device: %s", result.output)
1148 result = utils.RunCmd(["drbdmeta", "--force", cls._DevPath(minor),
1149 "v08", dev_path, "0", "create-md"])
1151 _ThrowError("Can't initialize meta device: %s", result.output)
1154 def _FindUnusedMinor(cls):
1155 """Find an unused DRBD device.
1157 This is specific to 8.x as the minors are allocated dynamically,
1158 so non-existing numbers up to a max minor count are actually free.
1161 data = cls._GetProcData()
1165 match = cls._UNUSED_LINE_RE.match(line)
1167 return int(match.group(1))
1168 match = cls._VALID_LINE_RE.match(line)
1170 minor = int(match.group(1))
1171 highest = max(highest, minor)
1172 if highest is None: # there are no minors in use at all
1174 if highest >= cls._MAX_MINORS:
1175 logging.error("Error: no free drbd minors!")
1176 raise errors.BlockDeviceError("Can't find a free DRBD minor")
1180 def _GetShowParser(cls):
1181 """Return a parser for `drbd show` output.
1183 This will either create or return an already-create parser for the
1184 output of the command `drbd show`.
1187 if cls._PARSE_SHOW is not None:
1188 return cls._PARSE_SHOW
1191 lbrace = pyp.Literal("{").suppress()
1192 rbrace = pyp.Literal("}").suppress()
1193 lbracket = pyp.Literal("[").suppress()
1194 rbracket = pyp.Literal("]").suppress()
1195 semi = pyp.Literal(";").suppress()
1196 colon = pyp.Literal(":").suppress()
1197 # this also converts the value to an int
1198 number = pyp.Word(pyp.nums).setParseAction(lambda s, l, t: int(t[0]))
1200 comment = pyp.Literal("#") + pyp.Optional(pyp.restOfLine)
1201 defa = pyp.Literal("_is_default").suppress()
1202 dbl_quote = pyp.Literal('"').suppress()
1204 keyword = pyp.Word(pyp.alphanums + '-')
1207 value = pyp.Word(pyp.alphanums + '_-/.:')
1208 quoted = dbl_quote + pyp.CharsNotIn('"') + dbl_quote
1209 ipv4_addr = (pyp.Optional(pyp.Literal("ipv4")).suppress() +
1210 pyp.Word(pyp.nums + ".") + colon + number)
1211 ipv6_addr = (pyp.Optional(pyp.Literal("ipv6")).suppress() +
1212 pyp.Optional(lbracket) + pyp.Word(pyp.hexnums + ":") +
1213 pyp.Optional(rbracket) + colon + number)
1214 # meta device, extended syntax
1215 meta_value = ((value ^ quoted) + lbracket + number + rbracket)
1216 # device name, extended syntax
1217 device_value = pyp.Literal("minor").suppress() + number
1220 stmt = (~rbrace + keyword + ~lbrace +
1221 pyp.Optional(ipv4_addr ^ ipv6_addr ^ value ^ quoted ^ meta_value ^
1223 pyp.Optional(defa) + semi +
1224 pyp.Optional(pyp.restOfLine).suppress())
1227 section_name = pyp.Word(pyp.alphas + "_")
1228 section = section_name + lbrace + pyp.ZeroOrMore(pyp.Group(stmt)) + rbrace
1230 bnf = pyp.ZeroOrMore(pyp.Group(section ^ stmt))
1233 cls._PARSE_SHOW = bnf
1238 def _GetShowData(cls, minor):
1239 """Return the `drbdsetup show` data for a minor.
1242 result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "show"])
1244 logging.error("Can't display the drbd config: %s - %s",
1245 result.fail_reason, result.output)
1247 return result.stdout
1250 def _GetDevInfo(cls, out):
1251 """Parse details about a given DRBD minor.
1253 This return, if available, the local backing device (as a path)
1254 and the local and remote (ip, port) information from a string
1255 containing the output of the `drbdsetup show` command as returned
1263 bnf = cls._GetShowParser()
1267 results = bnf.parseString(out)
1268 except pyp.ParseException, err:
1269 _ThrowError("Can't parse drbdsetup show output: %s", str(err))
1271 # and massage the results into our desired format
1272 for section in results:
1274 if sname == "_this_host":
1275 for lst in section[1:]:
1276 if lst[0] == "disk":
1277 data["local_dev"] = lst[1]
1278 elif lst[0] == "meta-disk":
1279 data["meta_dev"] = lst[1]
1280 data["meta_index"] = lst[2]
1281 elif lst[0] == "address":
1282 data["local_addr"] = tuple(lst[1:])
1283 elif sname == "_remote_host":
1284 for lst in section[1:]:
1285 if lst[0] == "address":
1286 data["remote_addr"] = tuple(lst[1:])
1289 def _MatchesLocal(self, info):
1290 """Test if our local config matches with an existing device.
1292 The parameter should be as returned from `_GetDevInfo()`. This
1293 method tests if our local backing device is the same as the one in
1294 the info parameter, in effect testing if we look like the given
1299 backend, meta = self._children
1301 backend = meta = None
1303 if backend is not None:
1304 retval = ("local_dev" in info and info["local_dev"] == backend.dev_path)
1306 retval = ("local_dev" not in info)
1308 if meta is not None:
1309 retval = retval and ("meta_dev" in info and
1310 info["meta_dev"] == meta.dev_path)
1311 retval = retval and ("meta_index" in info and
1312 info["meta_index"] == 0)
1314 retval = retval and ("meta_dev" not in info and
1315 "meta_index" not in info)
1318 def _MatchesNet(self, info):
1319 """Test if our network config matches with an existing device.
1321 The parameter should be as returned from `_GetDevInfo()`. This
1322 method tests if our network configuration is the same as the one
1323 in the info parameter, in effect testing if we look like the given
1327 if (((self._lhost is None and not ("local_addr" in info)) and
1328 (self._rhost is None and not ("remote_addr" in info)))):
1331 if self._lhost is None:
1334 if not ("local_addr" in info and
1335 "remote_addr" in info):
1338 retval = (info["local_addr"] == (self._lhost, self._lport))
1339 retval = (retval and
1340 info["remote_addr"] == (self._rhost, self._rport))
1344 def _AssembleLocal(cls, minor, backend, meta, size):
1345 """Configure the local part of a DRBD device.
1348 args = ["drbdsetup", cls._DevPath(minor), "disk",
1353 args.extend(["-d", "%sm" % size])
1354 if not constants.DRBD_BARRIERS: # disable barriers, if configured so
1355 version = cls._GetVersion(cls._GetProcData())
1356 # various DRBD versions support different disk barrier options;
1357 # what we aim here is to revert back to the 'drain' method of
1358 # disk flushes and to disable metadata barriers, in effect going
1359 # back to pre-8.0.7 behaviour
1360 vmaj = version["k_major"]
1361 vmin = version["k_minor"]
1362 vrel = version["k_point"]
1364 if vmin == 0: # 8.0.x
1366 args.extend(["-i", "-m"])
1367 elif vmin == 2: # 8.2.x
1369 args.extend(["-i", "-m"])
1370 elif vmaj >= 3: # 8.3.x or newer
1371 args.extend(["-i", "-a", "m"])
1372 result = utils.RunCmd(args)
1374 _ThrowError("drbd%d: can't attach local disk: %s", minor, result.output)
1377 def _AssembleNet(cls, minor, net_info, protocol,
1378 dual_pri=False, hmac=None, secret=None):
1379 """Configure the network part of the device.
1382 lhost, lport, rhost, rport = net_info
1383 if None in net_info:
1384 # we don't want network connection and actually want to make
1386 cls._ShutdownNet(minor)
1389 # Workaround for a race condition. When DRBD is doing its dance to
1390 # establish a connection with its peer, it also sends the
1391 # synchronization speed over the wire. In some cases setting the
1392 # sync speed only after setting up both sides can race with DRBD
1393 # connecting, hence we set it here before telling DRBD anything
1395 cls._SetMinorSyncSpeed(minor, constants.SYNC_SPEED)
1397 if netutils.IP6Address.IsValid(lhost):
1398 if not netutils.IP6Address.IsValid(rhost):
1399 _ThrowError("drbd%d: can't connect ip %s to ip %s" %
1400 (minor, lhost, rhost))
1402 elif netutils.IP4Address.IsValid(lhost):
1403 if not netutils.IP4Address.IsValid(rhost):
1404 _ThrowError("drbd%d: can't connect ip %s to ip %s" %
1405 (minor, lhost, rhost))
1408 _ThrowError("drbd%d: Invalid ip %s" % (minor, lhost))
1410 args = ["drbdsetup", cls._DevPath(minor), "net",
1411 "%s:%s:%s" % (family, lhost, lport),
1412 "%s:%s:%s" % (family, rhost, rport), protocol,
1413 "-A", "discard-zero-changes",
1420 args.extend(["-a", hmac, "-x", secret])
1421 result = utils.RunCmd(args)
1423 _ThrowError("drbd%d: can't setup network: %s - %s",
1424 minor, result.fail_reason, result.output)
1426 def _CheckNetworkConfig():
1427 info = cls._GetDevInfo(cls._GetShowData(minor))
1428 if not "local_addr" in info or not "remote_addr" in info:
1429 raise utils.RetryAgain()
1431 if (info["local_addr"] != (lhost, lport) or
1432 info["remote_addr"] != (rhost, rport)):
1433 raise utils.RetryAgain()
1436 utils.Retry(_CheckNetworkConfig, 1.0, 10.0)
1437 except utils.RetryTimeout:
1438 _ThrowError("drbd%d: timeout while configuring network", minor)
1440 def AddChildren(self, devices):
1441 """Add a disk to the DRBD device.
1444 if self.minor is None:
1445 _ThrowError("drbd%d: can't attach to dbrd8 during AddChildren",
1447 if len(devices) != 2:
1448 _ThrowError("drbd%d: need two devices for AddChildren", self.minor)
1449 info = self._GetDevInfo(self._GetShowData(self.minor))
1450 if "local_dev" in info:
1451 _ThrowError("drbd%d: already attached to a local disk", self.minor)
1452 backend, meta = devices
1453 if backend.dev_path is None or meta.dev_path is None:
1454 _ThrowError("drbd%d: children not ready during AddChildren", self.minor)
1457 self._CheckMetaSize(meta.dev_path)
1458 self._InitMeta(self._FindUnusedMinor(), meta.dev_path)
1460 self._AssembleLocal(self.minor, backend.dev_path, meta.dev_path, self.size)
1461 self._children = devices
1463 def RemoveChildren(self, devices):
1464 """Detach the drbd device from local storage.
1467 if self.minor is None:
1468 _ThrowError("drbd%d: can't attach to drbd8 during RemoveChildren",
1470 # early return if we don't actually have backing storage
1471 info = self._GetDevInfo(self._GetShowData(self.minor))
1472 if "local_dev" not in info:
1474 if len(self._children) != 2:
1475 _ThrowError("drbd%d: we don't have two children: %s", self.minor,
1477 if self._children.count(None) == 2: # we don't actually have children :)
1478 logging.warning("drbd%d: requested detach while detached", self.minor)
1480 if len(devices) != 2:
1481 _ThrowError("drbd%d: we need two children in RemoveChildren", self.minor)
1482 for child, dev in zip(self._children, devices):
1483 if dev != child.dev_path:
1484 _ThrowError("drbd%d: mismatch in local storage (%s != %s) in"
1485 " RemoveChildren", self.minor, dev, child.dev_path)
1487 self._ShutdownLocal(self.minor)
1491 def _SetMinorSyncSpeed(cls, minor, kbytes):
1492 """Set the speed of the DRBD syncer.
1494 This is the low-level implementation.
1497 @param minor: the drbd minor whose settings we change
1499 @param kbytes: the speed in kbytes/second
1501 @return: the success of the operation
1504 result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "syncer",
1505 "-r", "%d" % kbytes, "--create-device"])
1507 logging.error("Can't change syncer rate: %s - %s",
1508 result.fail_reason, result.output)
1509 return not result.failed
1511 def SetSyncSpeed(self, kbytes):
1512 """Set the speed of the DRBD syncer.
1515 @param kbytes: the speed in kbytes/second
1517 @return: the success of the operation
1520 if self.minor is None:
1521 logging.info("Not attached during SetSyncSpeed")
1523 children_result = super(DRBD8, self).SetSyncSpeed(kbytes)
1524 return self._SetMinorSyncSpeed(self.minor, kbytes) and children_result
1526 def PauseResumeSync(self, pause):
1527 """Pauses or resumes the sync of a DRBD device.
1529 @param pause: Wether to pause or resume
1530 @return: the success of the operation
1533 if self.minor is None:
1534 logging.info("Not attached during PauseSync")
1537 children_result = super(DRBD8, self).PauseResumeSync(pause)
1544 result = utils.RunCmd(["drbdsetup", self.dev_path, cmd])
1546 logging.error("Can't %s: %s - %s", cmd,
1547 result.fail_reason, result.output)
1548 return not result.failed and children_result
1550 def GetProcStatus(self):
1551 """Return device data from /proc.
1554 if self.minor is None:
1555 _ThrowError("drbd%d: GetStats() called while not attached", self._aminor)
1556 proc_info = self._MassageProcData(self._GetProcData())
1557 if self.minor not in proc_info:
1558 _ThrowError("drbd%d: can't find myself in /proc", self.minor)
1559 return DRBD8Status(proc_info[self.minor])
1561 def GetSyncStatus(self):
1562 """Returns the sync status of the device.
1565 If sync_percent is None, it means all is ok
1566 If estimated_time is None, it means we can't estimate
1567 the time needed, otherwise it's the time left in seconds.
1570 We set the is_degraded parameter to True on two conditions:
1571 network not connected or local disk missing.
1573 We compute the ldisk parameter based on whether we have a local
1576 @rtype: objects.BlockDevStatus
1579 if self.minor is None and not self.Attach():
1580 _ThrowError("drbd%d: can't Attach() in GetSyncStatus", self._aminor)
1582 stats = self.GetProcStatus()
1583 is_degraded = not stats.is_connected or not stats.is_disk_uptodate
1585 if stats.is_disk_uptodate:
1586 ldisk_status = constants.LDS_OKAY
1587 elif stats.is_diskless:
1588 ldisk_status = constants.LDS_FAULTY
1590 ldisk_status = constants.LDS_UNKNOWN
1592 return objects.BlockDevStatus(dev_path=self.dev_path,
1595 sync_percent=stats.sync_percent,
1596 estimated_time=stats.est_time,
1597 is_degraded=is_degraded,
1598 ldisk_status=ldisk_status)
1600 def Open(self, force=False):
1601 """Make the local state primary.
1603 If the 'force' parameter is given, the '-o' option is passed to
1604 drbdsetup. Since this is a potentially dangerous operation, the
1605 force flag should be only given after creation, when it actually
1609 if self.minor is None and not self.Attach():
1610 logging.error("DRBD cannot attach to a device during open")
1612 cmd = ["drbdsetup", self.dev_path, "primary"]
1615 result = utils.RunCmd(cmd)
1617 _ThrowError("drbd%d: can't make drbd device primary: %s", self.minor,
1621 """Make the local state secondary.
1623 This will, of course, fail if the device is in use.
1626 if self.minor is None and not self.Attach():
1627 _ThrowError("drbd%d: can't Attach() in Close()", self._aminor)
1628 result = utils.RunCmd(["drbdsetup", self.dev_path, "secondary"])
1630 _ThrowError("drbd%d: can't switch drbd device to secondary: %s",
1631 self.minor, result.output)
1633 def DisconnectNet(self):
1634 """Removes network configuration.
1636 This method shutdowns the network side of the device.
1638 The method will wait up to a hardcoded timeout for the device to
1639 go into standalone after the 'disconnect' command before
1640 re-configuring it, as sometimes it takes a while for the
1641 disconnect to actually propagate and thus we might issue a 'net'
1642 command while the device is still connected. If the device will
1643 still be attached to the network and we time out, we raise an
1647 if self.minor is None:
1648 _ThrowError("drbd%d: disk not attached in re-attach net", self._aminor)
1650 if None in (self._lhost, self._lport, self._rhost, self._rport):
1651 _ThrowError("drbd%d: DRBD disk missing network info in"
1652 " DisconnectNet()", self.minor)
1654 class _DisconnectStatus:
1655 def __init__(self, ever_disconnected):
1656 self.ever_disconnected = ever_disconnected
1658 dstatus = _DisconnectStatus(_IgnoreError(self._ShutdownNet, self.minor))
1660 def _WaitForDisconnect():
1661 if self.GetProcStatus().is_standalone:
1664 # retry the disconnect, it seems possible that due to a well-time
1665 # disconnect on the peer, my disconnect command might be ignored and
1667 dstatus.ever_disconnected = \
1668 _IgnoreError(self._ShutdownNet, self.minor) or dstatus.ever_disconnected
1670 raise utils.RetryAgain()
1673 start_time = time.time()
1676 # Start delay at 100 milliseconds and grow up to 2 seconds
1677 utils.Retry(_WaitForDisconnect, (0.1, 1.5, 2.0),
1678 self._NET_RECONFIG_TIMEOUT)
1679 except utils.RetryTimeout:
1680 if dstatus.ever_disconnected:
1681 msg = ("drbd%d: device did not react to the"
1682 " 'disconnect' command in a timely manner")
1684 msg = "drbd%d: can't shutdown network, even after multiple retries"
1686 _ThrowError(msg, self.minor)
1688 reconfig_time = time.time() - start_time
1689 if reconfig_time > (self._NET_RECONFIG_TIMEOUT * 0.25):
1690 logging.info("drbd%d: DisconnectNet: detach took %.3f seconds",
1691 self.minor, reconfig_time)
1693 def AttachNet(self, multimaster):
1694 """Reconnects the network.
1696 This method connects the network side of the device with a
1697 specified multi-master flag. The device needs to be 'Standalone'
1698 but have valid network configuration data.
1701 - multimaster: init the network in dual-primary mode
1704 if self.minor is None:
1705 _ThrowError("drbd%d: device not attached in AttachNet", self._aminor)
1707 if None in (self._lhost, self._lport, self._rhost, self._rport):
1708 _ThrowError("drbd%d: missing network info in AttachNet()", self.minor)
1710 status = self.GetProcStatus()
1712 if not status.is_standalone:
1713 _ThrowError("drbd%d: device is not standalone in AttachNet", self.minor)
1715 self._AssembleNet(self.minor,
1716 (self._lhost, self._lport, self._rhost, self._rport),
1717 constants.DRBD_NET_PROTOCOL, dual_pri=multimaster,
1718 hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1721 """Check if our minor is configured.
1723 This doesn't do any device configurations - it only checks if the
1724 minor is in a state different from Unconfigured.
1726 Note that this function will not change the state of the system in
1727 any way (except in case of side-effects caused by reading from
1731 used_devs = self.GetUsedDevs()
1732 if self._aminor in used_devs:
1733 minor = self._aminor
1737 self._SetFromMinor(minor)
1738 return minor is not None
1741 """Assemble the drbd.
1744 - if we have a configured device, we try to ensure that it matches
1746 - if not, we create it from zero
1747 - anyway, set the device parameters
1750 super(DRBD8, self).Assemble()
1753 if self.minor is None:
1754 # local device completely unconfigured
1755 self._FastAssemble()
1757 # we have to recheck the local and network status and try to fix
1759 self._SlowAssemble()
1761 self.SetSyncSpeed(constants.SYNC_SPEED)
1763 def _SlowAssemble(self):
1764 """Assembles the DRBD device from a (partially) configured device.
1766 In case of partially attached (local device matches but no network
1767 setup), we perform the network attach. If successful, we re-test
1768 the attach if can return success.
1771 # TODO: Rewrite to not use a for loop just because there is 'break'
1772 # pylint: disable=W0631
1773 net_data = (self._lhost, self._lport, self._rhost, self._rport)
1774 for minor in (self._aminor,):
1775 info = self._GetDevInfo(self._GetShowData(minor))
1776 match_l = self._MatchesLocal(info)
1777 match_r = self._MatchesNet(info)
1779 if match_l and match_r:
1780 # everything matches
1783 if match_l and not match_r and "local_addr" not in info:
1784 # disk matches, but not attached to network, attach and recheck
1785 self._AssembleNet(minor, net_data, constants.DRBD_NET_PROTOCOL,
1786 hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1787 if self._MatchesNet(self._GetDevInfo(self._GetShowData(minor))):
1790 _ThrowError("drbd%d: network attach successful, but 'drbdsetup"
1791 " show' disagrees", minor)
1793 if match_r and "local_dev" not in info:
1794 # no local disk, but network attached and it matches
1795 self._AssembleLocal(minor, self._children[0].dev_path,
1796 self._children[1].dev_path, self.size)
1797 if self._MatchesNet(self._GetDevInfo(self._GetShowData(minor))):
1800 _ThrowError("drbd%d: disk attach successful, but 'drbdsetup"
1801 " show' disagrees", minor)
1803 # this case must be considered only if we actually have local
1804 # storage, i.e. not in diskless mode, because all diskless
1805 # devices are equal from the point of view of local
1807 if (match_l and "local_dev" in info and
1808 not match_r and "local_addr" in info):
1809 # strange case - the device network part points to somewhere
1810 # else, even though its local storage is ours; as we own the
1811 # drbd space, we try to disconnect from the remote peer and
1812 # reconnect to our correct one
1814 self._ShutdownNet(minor)
1815 except errors.BlockDeviceError, err:
1816 _ThrowError("drbd%d: device has correct local storage, wrong"
1817 " remote peer and is unable to disconnect in order"
1818 " to attach to the correct peer: %s", minor, str(err))
1819 # note: _AssembleNet also handles the case when we don't want
1820 # local storage (i.e. one or more of the _[lr](host|port) is
1822 self._AssembleNet(minor, net_data, constants.DRBD_NET_PROTOCOL,
1823 hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1824 if self._MatchesNet(self._GetDevInfo(self._GetShowData(minor))):
1827 _ThrowError("drbd%d: network attach successful, but 'drbdsetup"
1828 " show' disagrees", minor)
1833 self._SetFromMinor(minor)
1835 _ThrowError("drbd%d: cannot activate, unknown or unhandled reason",
1838 def _FastAssemble(self):
1839 """Assemble the drbd device from zero.
1841 This is run when in Assemble we detect our minor is unused.
1844 minor = self._aminor
1845 if self._children and self._children[0] and self._children[1]:
1846 self._AssembleLocal(minor, self._children[0].dev_path,
1847 self._children[1].dev_path, self.size)
1848 if self._lhost and self._lport and self._rhost and self._rport:
1849 self._AssembleNet(minor,
1850 (self._lhost, self._lport, self._rhost, self._rport),
1851 constants.DRBD_NET_PROTOCOL,
1852 hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1853 self._SetFromMinor(minor)
1856 def _ShutdownLocal(cls, minor):
1857 """Detach from the local device.
1859 I/Os will continue to be served from the remote device. If we
1860 don't have a remote device, this operation will fail.
1863 result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "detach"])
1865 _ThrowError("drbd%d: can't detach local disk: %s", minor, result.output)
1868 def _ShutdownNet(cls, minor):
1869 """Disconnect from the remote peer.
1871 This fails if we don't have a local device.
1874 result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "disconnect"])
1876 _ThrowError("drbd%d: can't shutdown network: %s", minor, result.output)
1879 def _ShutdownAll(cls, minor):
1880 """Deactivate the device.
1882 This will, of course, fail if the device is in use.
1885 result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "down"])
1887 _ThrowError("drbd%d: can't shutdown drbd device: %s",
1888 minor, result.output)
1891 """Shutdown the DRBD device.
1894 if self.minor is None and not self.Attach():
1895 logging.info("drbd%d: not attached during Shutdown()", self._aminor)
1899 self.dev_path = None
1900 self._ShutdownAll(minor)
1903 """Stub remove for DRBD devices.
1909 def Create(cls, unique_id, children, size, params):
1910 """Create a new DRBD8 device.
1912 Since DRBD devices are not created per se, just assembled, this
1913 function only initializes the metadata.
1916 if len(children) != 2:
1917 raise errors.ProgrammerError("Invalid setup for the drbd device")
1918 # check that the minor is unused
1919 aminor = unique_id[4]
1920 proc_info = cls._MassageProcData(cls._GetProcData())
1921 if aminor in proc_info:
1922 status = DRBD8Status(proc_info[aminor])
1923 in_use = status.is_in_use
1927 _ThrowError("drbd%d: minor is already in use at Create() time", aminor)
1930 if not meta.Attach():
1931 _ThrowError("drbd%d: can't attach to meta device '%s'",
1933 cls._CheckMetaSize(meta.dev_path)
1934 cls._InitMeta(aminor, meta.dev_path)
1935 return cls(unique_id, children, size, params)
1937 def Grow(self, amount, dryrun):
1938 """Resize the DRBD device and its backing storage.
1941 if self.minor is None:
1942 _ThrowError("drbd%d: Grow called while not attached", self._aminor)
1943 if len(self._children) != 2 or None in self._children:
1944 _ThrowError("drbd%d: cannot grow diskless device", self.minor)
1945 self._children[0].Grow(amount, dryrun)
1947 # DRBD does not support dry-run mode, so we'll return here
1949 result = utils.RunCmd(["drbdsetup", self.dev_path, "resize", "-s",
1950 "%dm" % (self.size + amount)])
1952 _ThrowError("drbd%d: resize failed: %s", self.minor, result.output)
1955 class FileStorage(BlockDev):
1958 This class represents the a file storage backend device.
1960 The unique_id for the file device is a (file_driver, file_path) tuple.
1963 def __init__(self, unique_id, children, size, params):
1964 """Initalizes a file device backend.
1968 raise errors.BlockDeviceError("Invalid setup for file device")
1969 super(FileStorage, self).__init__(unique_id, children, size, params)
1970 if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
1971 raise ValueError("Invalid configuration data %s" % str(unique_id))
1972 self.driver = unique_id[0]
1973 self.dev_path = unique_id[1]
1977 """Assemble the device.
1979 Checks whether the file device exists, raises BlockDeviceError otherwise.
1982 if not os.path.exists(self.dev_path):
1983 _ThrowError("File device '%s' does not exist" % self.dev_path)
1986 """Shutdown the device.
1988 This is a no-op for the file type, as we don't deactivate
1989 the file on shutdown.
1994 def Open(self, force=False):
1995 """Make the device ready for I/O.
1997 This is a no-op for the file type.
2003 """Notifies that the device will no longer be used for I/O.
2005 This is a no-op for the file type.
2011 """Remove the file backing the block device.
2014 @return: True if the removal was successful
2018 os.remove(self.dev_path)
2019 except OSError, err:
2020 if err.errno != errno.ENOENT:
2021 _ThrowError("Can't remove file '%s': %s", self.dev_path, err)
2023 def Rename(self, new_id):
2024 """Renames the file.
2027 # TODO: implement rename for file-based storage
2028 _ThrowError("Rename is not supported for file-based storage")
2030 def Grow(self, amount, dryrun):
2033 @param amount: the amount (in mebibytes) to grow with
2036 # Check that the file exists
2038 current_size = self.GetActualSize()
2039 new_size = current_size + amount * 1024 * 1024
2040 assert new_size > current_size, "Cannot Grow with a negative amount"
2041 # We can't really simulate the growth
2045 f = open(self.dev_path, "a+")
2046 f.truncate(new_size)
2048 except EnvironmentError, err:
2049 _ThrowError("Error in file growth: %", str(err))
2052 """Attach to an existing file.
2054 Check if this file already exists.
2057 @return: True if file exists
2060 self.attached = os.path.exists(self.dev_path)
2061 return self.attached
2063 def GetActualSize(self):
2064 """Return the actual disk size.
2066 @note: the device needs to be active when this is called
2069 assert self.attached, "BlockDevice not attached in GetActualSize()"
2071 st = os.stat(self.dev_path)
2073 except OSError, err:
2074 _ThrowError("Can't stat %s: %s", self.dev_path, err)
2077 def Create(cls, unique_id, children, size, params):
2078 """Create a new file.
2080 @param size: the size of file in MiB
2082 @rtype: L{bdev.FileStorage}
2083 @return: an instance of FileStorage
2086 if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
2087 raise ValueError("Invalid configuration data %s" % str(unique_id))
2088 dev_path = unique_id[1]
2090 fd = os.open(dev_path, os.O_RDWR | os.O_CREAT | os.O_EXCL)
2091 f = os.fdopen(fd, "w")
2092 f.truncate(size * 1024 * 1024)
2094 except EnvironmentError, err:
2095 if err.errno == errno.EEXIST:
2096 _ThrowError("File already existing: %s", dev_path)
2097 _ThrowError("Error in file creation: %", str(err))
2099 return FileStorage(unique_id, children, size, params)
2102 class PersistentBlockDevice(BlockDev):
2103 """A block device with persistent node
2105 May be either directly attached, or exposed through DM (e.g. dm-multipath).
2106 udev helpers are probably required to give persistent, human-friendly
2109 For the time being, pathnames are required to lie under /dev.
2112 def __init__(self, unique_id, children, size, params):
2113 """Attaches to a static block device.
2115 The unique_id is a path under /dev.
2118 super(PersistentBlockDevice, self).__init__(unique_id, children, size,
2120 if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
2121 raise ValueError("Invalid configuration data %s" % str(unique_id))
2122 self.dev_path = unique_id[1]
2123 if not os.path.realpath(self.dev_path).startswith("/dev/"):
2124 raise ValueError("Full path '%s' lies outside /dev" %
2125 os.path.realpath(self.dev_path))
2126 # TODO: this is just a safety guard checking that we only deal with devices
2127 # we know how to handle. In the future this will be integrated with
2128 # external storage backends and possible values will probably be collected
2129 # from the cluster configuration.
2130 if unique_id[0] != constants.BLOCKDEV_DRIVER_MANUAL:
2131 raise ValueError("Got persistent block device of invalid type: %s" %
2134 self.major = self.minor = None
2138 def Create(cls, unique_id, children, size, params):
2139 """Create a new device
2141 This is a noop, we only return a PersistentBlockDevice instance
2144 return PersistentBlockDevice(unique_id, children, 0, params)
2154 def Rename(self, new_id):
2155 """Rename this device.
2158 _ThrowError("Rename is not supported for PersistentBlockDev storage")
2161 """Attach to an existing block device.
2165 self.attached = False
2167 st = os.stat(self.dev_path)
2168 except OSError, err:
2169 logging.error("Error stat()'ing %s: %s", self.dev_path, str(err))
2172 if not stat.S_ISBLK(st.st_mode):
2173 logging.error("%s is not a block device", self.dev_path)
2176 self.major = os.major(st.st_rdev)
2177 self.minor = os.minor(st.st_rdev)
2178 self.attached = True
2183 """Assemble the device.
2189 """Shutdown the device.
2194 def Open(self, force=False):
2195 """Make the device ready for I/O.
2201 """Notifies that the device will no longer be used for I/O.
2206 def Grow(self, amount, dryrun):
2207 """Grow the logical volume.
2210 _ThrowError("Grow is not supported for PersistentBlockDev storage")
2214 constants.LD_LV: LogicalVolume,
2215 constants.LD_DRBD8: DRBD8,
2216 constants.LD_BLOCKDEV: PersistentBlockDevice,
2219 if constants.ENABLE_FILE_STORAGE or constants.ENABLE_SHARED_FILE_STORAGE:
2220 DEV_MAP[constants.LD_FILE] = FileStorage
2223 def _VerifyDiskType(dev_type):
2224 if dev_type not in DEV_MAP:
2225 raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
2228 def FindDevice(disk, children):
2229 """Search for an existing, assembled device.
2231 This will succeed only if the device exists and is assembled, but it
2232 does not do any actions in order to activate the device.
2234 @type disk: L{objects.Disk}
2235 @param disk: the disk object to find
2236 @type children: list of L{bdev.BlockDev}
2237 @param children: the list of block devices that are children of the device
2238 represented by the disk parameter
2241 _VerifyDiskType(disk.dev_type)
2242 dev_params = objects.FillDict(constants.DISK_LD_DEFAULTS[disk.dev_type],
2244 device = DEV_MAP[disk.dev_type](disk.physical_id, children, disk.size,
2246 if not device.attached:
2251 def Assemble(disk, children):
2252 """Try to attach or assemble an existing device.
2254 This will attach to assemble the device, as needed, to bring it
2255 fully up. It must be safe to run on already-assembled devices.
2257 @type disk: L{objects.Disk}
2258 @param disk: the disk object to assemble
2259 @type children: list of L{bdev.BlockDev}
2260 @param children: the list of block devices that are children of the device
2261 represented by the disk parameter
2264 _VerifyDiskType(disk.dev_type)
2265 dev_params = objects.FillDict(constants.DISK_LD_DEFAULTS[disk.dev_type],
2267 device = DEV_MAP[disk.dev_type](disk.physical_id, children, disk.size,
2273 def Create(disk, children):
2276 @type disk: L{objects.Disk}
2277 @param disk: the disk object to create
2278 @type children: list of L{bdev.BlockDev}
2279 @param children: the list of block devices that are children of the device
2280 represented by the disk parameter
2283 _VerifyDiskType(disk.dev_type)
2284 dev_params = objects.FillDict(constants.DISK_LD_DEFAULTS[disk.dev_type],
2286 device = DEV_MAP[disk.dev_type].Create(disk.physical_id, children, disk.size,