Add separate module for backported language functionality
[ganeti-local] / lib / bdev.py
1 #
2 #
3
4 # Copyright (C) 2006, 2007 Google Inc.
5 #
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.
10 #
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.
15 #
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
19 # 02110-1301, USA.
20
21
22 """Block device abstraction"""
23
24 import re
25 import time
26 import errno
27 import pyparsing as pyp
28 import os
29 import logging
30
31 from ganeti import utils
32 from ganeti import errors
33 from ganeti import constants
34 from ganeti import objects
35 from ganeti import compat
36
37
38 # Size of reads in _CanReadDevice
39 _DEVICE_READ_SIZE = 128 * 1024
40
41
42 def _IgnoreError(fn, *args, **kwargs):
43   """Executes the given function, ignoring BlockDeviceErrors.
44
45   This is used in order to simplify the execution of cleanup or
46   rollback functions.
47
48   @rtype: boolean
49   @return: True when fn didn't raise an exception, False otherwise
50
51   """
52   try:
53     fn(*args, **kwargs)
54     return True
55   except errors.BlockDeviceError, err:
56     logging.warning("Caught BlockDeviceError but ignoring: %s", str(err))
57     return False
58
59
60 def _ThrowError(msg, *args):
61   """Log an error to the node daemon and the raise an exception.
62
63   @type msg: string
64   @param msg: the text of the exception
65   @raise errors.BlockDeviceError
66
67   """
68   if args:
69     msg = msg % args
70   logging.error(msg)
71   raise errors.BlockDeviceError(msg)
72
73
74 def _CanReadDevice(path):
75   """Check if we can read from the given device.
76
77   This tries to read the first 128k of the device.
78
79   """
80   try:
81     utils.ReadFile(path, size=_DEVICE_READ_SIZE)
82     return True
83   except EnvironmentError:
84     logging.warning("Can't read from device %s", path, exc_info=True)
85     return False
86
87
88 class BlockDev(object):
89   """Block device abstract class.
90
91   A block device can be in the following states:
92     - not existing on the system, and by `Create()` it goes into:
93     - existing but not setup/not active, and by `Assemble()` goes into:
94     - active read-write and by `Open()` it goes into
95     - online (=used, or ready for use)
96
97   A device can also be online but read-only, however we are not using
98   the readonly state (LV has it, if needed in the future) and we are
99   usually looking at this like at a stack, so it's easier to
100   conceptualise the transition from not-existing to online and back
101   like a linear one.
102
103   The many different states of the device are due to the fact that we
104   need to cover many device types:
105     - logical volumes are created, lvchange -a y $lv, and used
106     - drbd devices are attached to a local disk/remote peer and made primary
107
108   A block device is identified by three items:
109     - the /dev path of the device (dynamic)
110     - a unique ID of the device (static)
111     - it's major/minor pair (dynamic)
112
113   Not all devices implement both the first two as distinct items. LVM
114   logical volumes have their unique ID (the pair volume group, logical
115   volume name) in a 1-to-1 relation to the dev path. For DRBD devices,
116   the /dev path is again dynamic and the unique id is the pair (host1,
117   dev1), (host2, dev2).
118
119   You can get to a device in two ways:
120     - creating the (real) device, which returns you
121       an attached instance (lvcreate)
122     - attaching of a python instance to an existing (real) device
123
124   The second point, the attachement to a device, is different
125   depending on whether the device is assembled or not. At init() time,
126   we search for a device with the same unique_id as us. If found,
127   good. It also means that the device is already assembled. If not,
128   after assembly we'll have our correct major/minor.
129
130   """
131   def __init__(self, unique_id, children, size):
132     self._children = children
133     self.dev_path = None
134     self.unique_id = unique_id
135     self.major = None
136     self.minor = None
137     self.attached = False
138     self.size = size
139
140   def Assemble(self):
141     """Assemble the device from its components.
142
143     Implementations of this method by child classes must ensure that:
144       - after the device has been assembled, it knows its major/minor
145         numbers; this allows other devices (usually parents) to probe
146         correctly for their children
147       - calling this method on an existing, in-use device is safe
148       - if the device is already configured (and in an OK state),
149         this method is idempotent
150
151     """
152     pass
153
154   def Attach(self):
155     """Find a device which matches our config and attach to it.
156
157     """
158     raise NotImplementedError
159
160   def Close(self):
161     """Notifies that the device will no longer be used for I/O.
162
163     """
164     raise NotImplementedError
165
166   @classmethod
167   def Create(cls, unique_id, children, size):
168     """Create the device.
169
170     If the device cannot be created, it will return None
171     instead. Error messages go to the logging system.
172
173     Note that for some devices, the unique_id is used, and for other,
174     the children. The idea is that these two, taken together, are
175     enough for both creation and assembly (later).
176
177     """
178     raise NotImplementedError
179
180   def Remove(self):
181     """Remove this device.
182
183     This makes sense only for some of the device types: LV and file
184     storage. Also note that if the device can't attach, the removal
185     can't be completed.
186
187     """
188     raise NotImplementedError
189
190   def Rename(self, new_id):
191     """Rename this device.
192
193     This may or may not make sense for a given device type.
194
195     """
196     raise NotImplementedError
197
198   def Open(self, force=False):
199     """Make the device ready for use.
200
201     This makes the device ready for I/O. For now, just the DRBD
202     devices need this.
203
204     The force parameter signifies that if the device has any kind of
205     --force thing, it should be used, we know what we are doing.
206
207     """
208     raise NotImplementedError
209
210   def Shutdown(self):
211     """Shut down the device, freeing its children.
212
213     This undoes the `Assemble()` work, except for the child
214     assembling; as such, the children on the device are still
215     assembled after this call.
216
217     """
218     raise NotImplementedError
219
220   def SetSyncSpeed(self, speed):
221     """Adjust the sync speed of the mirror.
222
223     In case this is not a mirroring device, this is no-op.
224
225     """
226     result = True
227     if self._children:
228       for child in self._children:
229         result = result and child.SetSyncSpeed(speed)
230     return result
231
232   def GetSyncStatus(self):
233     """Returns the sync status of the device.
234
235     If this device is a mirroring device, this function returns the
236     status of the mirror.
237
238     If sync_percent is None, it means the device is not syncing.
239
240     If estimated_time is None, it means we can't estimate
241     the time needed, otherwise it's the time left in seconds.
242
243     If is_degraded is True, it means the device is missing
244     redundancy. This is usually a sign that something went wrong in
245     the device setup, if sync_percent is None.
246
247     The ldisk parameter represents the degradation of the local
248     data. This is only valid for some devices, the rest will always
249     return False (not degraded).
250
251     @rtype: objects.BlockDevStatus
252
253     """
254     return objects.BlockDevStatus(dev_path=self.dev_path,
255                                   major=self.major,
256                                   minor=self.minor,
257                                   sync_percent=None,
258                                   estimated_time=None,
259                                   is_degraded=False,
260                                   ldisk_status=constants.LDS_OKAY)
261
262   def CombinedSyncStatus(self):
263     """Calculate the mirror status recursively for our children.
264
265     The return value is the same as for `GetSyncStatus()` except the
266     minimum percent and maximum time are calculated across our
267     children.
268
269     @rtype: objects.BlockDevStatus
270
271     """
272     status = self.GetSyncStatus()
273
274     min_percent = status.sync_percent
275     max_time = status.estimated_time
276     is_degraded = status.is_degraded
277     ldisk_status = status.ldisk_status
278
279     if self._children:
280       for child in self._children:
281         child_status = child.GetSyncStatus()
282
283         if min_percent is None:
284           min_percent = child_status.sync_percent
285         elif child_status.sync_percent is not None:
286           min_percent = min(min_percent, child_status.sync_percent)
287
288         if max_time is None:
289           max_time = child_status.estimated_time
290         elif child_status.estimated_time is not None:
291           max_time = max(max_time, child_status.estimated_time)
292
293         is_degraded = is_degraded or child_status.is_degraded
294
295         if ldisk_status is None:
296           ldisk_status = child_status.ldisk_status
297         elif child_status.ldisk_status is not None:
298           ldisk_status = max(ldisk_status, child_status.ldisk_status)
299
300     return objects.BlockDevStatus(dev_path=self.dev_path,
301                                   major=self.major,
302                                   minor=self.minor,
303                                   sync_percent=min_percent,
304                                   estimated_time=max_time,
305                                   is_degraded=is_degraded,
306                                   ldisk_status=ldisk_status)
307
308
309   def SetInfo(self, text):
310     """Update metadata with info text.
311
312     Only supported for some device types.
313
314     """
315     for child in self._children:
316       child.SetInfo(text)
317
318   def Grow(self, amount):
319     """Grow the block device.
320
321     @param amount: the amount (in mebibytes) to grow with
322
323     """
324     raise NotImplementedError
325
326   def GetActualSize(self):
327     """Return the actual disk size.
328
329     @note: the device needs to be active when this is called
330
331     """
332     assert self.attached, "BlockDevice not attached in GetActualSize()"
333     result = utils.RunCmd(["blockdev", "--getsize64", self.dev_path])
334     if result.failed:
335       _ThrowError("blockdev failed (%s): %s",
336                   result.fail_reason, result.output)
337     try:
338       sz = int(result.output.strip())
339     except (ValueError, TypeError), err:
340       _ThrowError("Failed to parse blockdev output: %s", str(err))
341     return sz
342
343   def __repr__(self):
344     return ("<%s: unique_id: %s, children: %s, %s:%s, %s>" %
345             (self.__class__, self.unique_id, self._children,
346              self.major, self.minor, self.dev_path))
347
348
349 class LogicalVolume(BlockDev):
350   """Logical Volume block device.
351
352   """
353   _VALID_NAME_RE = re.compile("^[a-zA-Z0-9+_.-]*$")
354   _INVALID_NAMES = frozenset([".", "..", "snapshot", "pvmove"])
355   _INVALID_SUBSTRINGS = frozenset(["_mlog", "_mimage"])
356
357   def __init__(self, unique_id, children, size):
358     """Attaches to a LV device.
359
360     The unique_id is a tuple (vg_name, lv_name)
361
362     """
363     super(LogicalVolume, self).__init__(unique_id, children, size)
364     if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
365       raise ValueError("Invalid configuration data %s" % str(unique_id))
366     self._vg_name, self._lv_name = unique_id
367     self._ValidateName(self._vg_name)
368     self._ValidateName(self._lv_name)
369     self.dev_path = utils.PathJoin("/dev", self._vg_name, self._lv_name)
370     self._degraded = True
371     self.major = self.minor = self.pe_size = self.stripe_count = None
372     self.Attach()
373
374   @classmethod
375   def Create(cls, unique_id, children, size):
376     """Create a new logical volume.
377
378     """
379     if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
380       raise errors.ProgrammerError("Invalid configuration data %s" %
381                                    str(unique_id))
382     vg_name, lv_name = unique_id
383     cls._ValidateName(vg_name)
384     cls._ValidateName(lv_name)
385     pvs_info = cls.GetPVInfo([vg_name])
386     if not pvs_info:
387       _ThrowError("Can't compute PV info for vg %s", vg_name)
388     pvs_info.sort()
389     pvs_info.reverse()
390
391     pvlist = [ pv[1] for pv in pvs_info ]
392     if compat.any(pvlist, lambda v: ":" in v):
393       _ThrowError("Some of your PVs have the invalid character ':' in their"
394                   " name, this is not supported - please filter them out"
395                   " in lvm.conf using either 'filter' or 'preferred_names'")
396     free_size = sum([ pv[0] for pv in pvs_info ])
397     current_pvs = len(pvlist)
398     stripes = min(current_pvs, constants.LVM_STRIPECOUNT)
399
400     # The size constraint should have been checked from the master before
401     # calling the create function.
402     if free_size < size:
403       _ThrowError("Not enough free space: required %s,"
404                   " available %s", size, free_size)
405     cmd = ["lvcreate", "-L%dm" % size, "-n%s" % lv_name]
406     # If the free space is not well distributed, we won't be able to
407     # create an optimally-striped volume; in that case, we want to try
408     # with N, N-1, ..., 2, and finally 1 (non-stripped) number of
409     # stripes
410     for stripes_arg in range(stripes, 0, -1):
411       result = utils.RunCmd(cmd + ["-i%d" % stripes_arg] + [vg_name] + pvlist)
412       if not result.failed:
413         break
414     if result.failed:
415       _ThrowError("LV create failed (%s): %s",
416                   result.fail_reason, result.output)
417     return LogicalVolume(unique_id, children, size)
418
419   @staticmethod
420   def GetPVInfo(vg_names, filter_allocatable=True):
421     """Get the free space info for PVs in a volume group.
422
423     @param vg_names: list of volume group names, if empty all will be returned
424     @param filter_allocatable: whether to skip over unallocatable PVs
425
426     @rtype: list
427     @return: list of tuples (free_space, name) with free_space in mebibytes
428
429     """
430     sep = "|"
431     command = ["pvs", "--noheadings", "--nosuffix", "--units=m",
432                "-opv_name,vg_name,pv_free,pv_attr", "--unbuffered",
433                "--separator=%s" % sep ]
434     result = utils.RunCmd(command)
435     if result.failed:
436       logging.error("Can't get the PV information: %s - %s",
437                     result.fail_reason, result.output)
438       return None
439     data = []
440     for line in result.stdout.splitlines():
441       fields = line.strip().split(sep)
442       if len(fields) != 4:
443         logging.error("Can't parse pvs output: line '%s'", line)
444         return None
445       # (possibly) skip over pvs which are not allocatable
446       if filter_allocatable and fields[3][0] != 'a':
447         continue
448       # (possibly) skip over pvs which are not in the right volume group(s)
449       if vg_names and fields[1] not in vg_names:
450         continue
451       data.append((float(fields[2]), fields[0], fields[1]))
452
453     return data
454
455   @classmethod
456   def _ValidateName(cls, name):
457     """Validates that a given name is valid as VG or LV name.
458
459     The list of valid characters and restricted names is taken out of
460     the lvm(8) manpage, with the simplification that we enforce both
461     VG and LV restrictions on the names.
462
463     """
464     if (not cls._VALID_NAME_RE.match(name) or
465         name in cls._INVALID_NAMES or
466         compat.any(cls._INVALID_SUBSTRINGS, lambda x: x in name)):
467       _ThrowError("Invalid LVM name '%s'", name)
468
469   def Remove(self):
470     """Remove this logical volume.
471
472     """
473     if not self.minor and not self.Attach():
474       # the LV does not exist
475       return
476     result = utils.RunCmd(["lvremove", "-f", "%s/%s" %
477                            (self._vg_name, self._lv_name)])
478     if result.failed:
479       _ThrowError("Can't lvremove: %s - %s", result.fail_reason, result.output)
480
481   def Rename(self, new_id):
482     """Rename this logical volume.
483
484     """
485     if not isinstance(new_id, (tuple, list)) or len(new_id) != 2:
486       raise errors.ProgrammerError("Invalid new logical id '%s'" % new_id)
487     new_vg, new_name = new_id
488     if new_vg != self._vg_name:
489       raise errors.ProgrammerError("Can't move a logical volume across"
490                                    " volume groups (from %s to to %s)" %
491                                    (self._vg_name, new_vg))
492     result = utils.RunCmd(["lvrename", new_vg, self._lv_name, new_name])
493     if result.failed:
494       _ThrowError("Failed to rename the logical volume: %s", result.output)
495     self._lv_name = new_name
496     self.dev_path = utils.PathJoin("/dev", self._vg_name, self._lv_name)
497
498   def Attach(self):
499     """Attach to an existing LV.
500
501     This method will try to see if an existing and active LV exists
502     which matches our name. If so, its major/minor will be
503     recorded.
504
505     """
506     self.attached = False
507     result = utils.RunCmd(["lvs", "--noheadings", "--separator=,",
508                            "--units=m", "--nosuffix",
509                            "-olv_attr,lv_kernel_major,lv_kernel_minor,"
510                            "vg_extent_size,stripes", self.dev_path])
511     if result.failed:
512       logging.error("Can't find LV %s: %s, %s",
513                     self.dev_path, result.fail_reason, result.output)
514       return False
515     # the output can (and will) have multiple lines for multi-segment
516     # LVs, as the 'stripes' parameter is a segment one, so we take
517     # only the last entry, which is the one we're interested in; note
518     # that with LVM2 anyway the 'stripes' value must be constant
519     # across segments, so this is a no-op actually
520     out = result.stdout.splitlines()
521     if not out: # totally empty result? splitlines() returns at least
522                 # one line for any non-empty string
523       logging.error("Can't parse LVS output, no lines? Got '%s'", str(out))
524       return False
525     out = out[-1].strip().rstrip(',')
526     out = out.split(",")
527     if len(out) != 5:
528       logging.error("Can't parse LVS output, len(%s) != 5", str(out))
529       return False
530
531     status, major, minor, pe_size, stripes = out
532     if len(status) != 6:
533       logging.error("lvs lv_attr is not 6 characters (%s)", status)
534       return False
535
536     try:
537       major = int(major)
538       minor = int(minor)
539     except (TypeError, ValueError), err:
540       logging.error("lvs major/minor cannot be parsed: %s", str(err))
541
542     try:
543       pe_size = int(float(pe_size))
544     except (TypeError, ValueError), err:
545       logging.error("Can't parse vg extent size: %s", err)
546       return False
547
548     try:
549       stripes = int(stripes)
550     except (TypeError, ValueError), err:
551       logging.error("Can't parse the number of stripes: %s", err)
552       return False
553
554     self.major = major
555     self.minor = minor
556     self.pe_size = pe_size
557     self.stripe_count = stripes
558     self._degraded = status[0] == 'v' # virtual volume, i.e. doesn't backing
559                                       # storage
560     self.attached = True
561     return True
562
563   def Assemble(self):
564     """Assemble the device.
565
566     We always run `lvchange -ay` on the LV to ensure it's active before
567     use, as there were cases when xenvg was not active after boot
568     (also possibly after disk issues).
569
570     """
571     result = utils.RunCmd(["lvchange", "-ay", self.dev_path])
572     if result.failed:
573       _ThrowError("Can't activate lv %s: %s", self.dev_path, result.output)
574
575   def Shutdown(self):
576     """Shutdown the device.
577
578     This is a no-op for the LV device type, as we don't deactivate the
579     volumes on shutdown.
580
581     """
582     pass
583
584   def GetSyncStatus(self):
585     """Returns the sync status of the device.
586
587     If this device is a mirroring device, this function returns the
588     status of the mirror.
589
590     For logical volumes, sync_percent and estimated_time are always
591     None (no recovery in progress, as we don't handle the mirrored LV
592     case). The is_degraded parameter is the inverse of the ldisk
593     parameter.
594
595     For the ldisk parameter, we check if the logical volume has the
596     'virtual' type, which means it's not backed by existing storage
597     anymore (read from it return I/O error). This happens after a
598     physical disk failure and subsequent 'vgreduce --removemissing' on
599     the volume group.
600
601     The status was already read in Attach, so we just return it.
602
603     @rtype: objects.BlockDevStatus
604
605     """
606     if self._degraded:
607       ldisk_status = constants.LDS_FAULTY
608     else:
609       ldisk_status = constants.LDS_OKAY
610
611     return objects.BlockDevStatus(dev_path=self.dev_path,
612                                   major=self.major,
613                                   minor=self.minor,
614                                   sync_percent=None,
615                                   estimated_time=None,
616                                   is_degraded=self._degraded,
617                                   ldisk_status=ldisk_status)
618
619   def Open(self, force=False):
620     """Make the device ready for I/O.
621
622     This is a no-op for the LV device type.
623
624     """
625     pass
626
627   def Close(self):
628     """Notifies that the device will no longer be used for I/O.
629
630     This is a no-op for the LV device type.
631
632     """
633     pass
634
635   def Snapshot(self, size):
636     """Create a snapshot copy of an lvm block device.
637
638     """
639     snap_name = self._lv_name + ".snap"
640
641     # remove existing snapshot if found
642     snap = LogicalVolume((self._vg_name, snap_name), None, size)
643     _IgnoreError(snap.Remove)
644
645     pvs_info = self.GetPVInfo([self._vg_name])
646     if not pvs_info:
647       _ThrowError("Can't compute PV info for vg %s", self._vg_name)
648     pvs_info.sort()
649     pvs_info.reverse()
650     free_size, _, _ = pvs_info[0]
651     if free_size < size:
652       _ThrowError("Not enough free space: required %s,"
653                   " available %s", size, free_size)
654
655     result = utils.RunCmd(["lvcreate", "-L%dm" % size, "-s",
656                            "-n%s" % snap_name, self.dev_path])
657     if result.failed:
658       _ThrowError("command: %s error: %s - %s",
659                   result.cmd, result.fail_reason, result.output)
660
661     return snap_name
662
663   def SetInfo(self, text):
664     """Update metadata with info text.
665
666     """
667     BlockDev.SetInfo(self, text)
668
669     # Replace invalid characters
670     text = re.sub('^[^A-Za-z0-9_+.]', '_', text)
671     text = re.sub('[^-A-Za-z0-9_+.]', '_', text)
672
673     # Only up to 128 characters are allowed
674     text = text[:128]
675
676     result = utils.RunCmd(["lvchange", "--addtag", text,
677                            self.dev_path])
678     if result.failed:
679       _ThrowError("Command: %s error: %s - %s", result.cmd, result.fail_reason,
680                   result.output)
681
682   def Grow(self, amount):
683     """Grow the logical volume.
684
685     """
686     if self.pe_size is None or self.stripe_count is None:
687       if not self.Attach():
688         _ThrowError("Can't attach to LV during Grow()")
689     full_stripe_size = self.pe_size * self.stripe_count
690     rest = amount % full_stripe_size
691     if rest != 0:
692       amount += full_stripe_size - rest
693     # we try multiple algorithms since the 'best' ones might not have
694     # space available in the right place, but later ones might (since
695     # they have less constraints); also note that only recent LVM
696     # supports 'cling'
697     for alloc_policy in "contiguous", "cling", "normal":
698       result = utils.RunCmd(["lvextend", "--alloc", alloc_policy,
699                              "-L", "+%dm" % amount, self.dev_path])
700       if not result.failed:
701         return
702     _ThrowError("Can't grow LV %s: %s", self.dev_path, result.output)
703
704
705 class DRBD8Status(object):
706   """A DRBD status representation class.
707
708   Note that this doesn't support unconfigured devices (cs:Unconfigured).
709
710   """
711   UNCONF_RE = re.compile(r"\s*[0-9]+:\s*cs:Unconfigured$")
712   LINE_RE = re.compile(r"\s*[0-9]+:\s*cs:(\S+)\s+(?:st|ro):([^/]+)/(\S+)"
713                        "\s+ds:([^/]+)/(\S+)\s+.*$")
714   SYNC_RE = re.compile(r"^.*\ssync'ed:\s*([0-9.]+)%.*"
715                        "\sfinish: ([0-9]+):([0-9]+):([0-9]+)\s.*$")
716
717   CS_UNCONFIGURED = "Unconfigured"
718   CS_STANDALONE = "StandAlone"
719   CS_WFCONNECTION = "WFConnection"
720   CS_WFREPORTPARAMS = "WFReportParams"
721   CS_CONNECTED = "Connected"
722   CS_STARTINGSYNCS = "StartingSyncS"
723   CS_STARTINGSYNCT = "StartingSyncT"
724   CS_WFBITMAPS = "WFBitMapS"
725   CS_WFBITMAPT = "WFBitMapT"
726   CS_WFSYNCUUID = "WFSyncUUID"
727   CS_SYNCSOURCE = "SyncSource"
728   CS_SYNCTARGET = "SyncTarget"
729   CS_PAUSEDSYNCS = "PausedSyncS"
730   CS_PAUSEDSYNCT = "PausedSyncT"
731   CSET_SYNC = frozenset([
732     CS_WFREPORTPARAMS,
733     CS_STARTINGSYNCS,
734     CS_STARTINGSYNCT,
735     CS_WFBITMAPS,
736     CS_WFBITMAPT,
737     CS_WFSYNCUUID,
738     CS_SYNCSOURCE,
739     CS_SYNCTARGET,
740     CS_PAUSEDSYNCS,
741     CS_PAUSEDSYNCT,
742     ])
743
744   DS_DISKLESS = "Diskless"
745   DS_ATTACHING = "Attaching" # transient state
746   DS_FAILED = "Failed" # transient state, next: diskless
747   DS_NEGOTIATING = "Negotiating" # transient state
748   DS_INCONSISTENT = "Inconsistent" # while syncing or after creation
749   DS_OUTDATED = "Outdated"
750   DS_DUNKNOWN = "DUnknown" # shown for peer disk when not connected
751   DS_CONSISTENT = "Consistent"
752   DS_UPTODATE = "UpToDate" # normal state
753
754   RO_PRIMARY = "Primary"
755   RO_SECONDARY = "Secondary"
756   RO_UNKNOWN = "Unknown"
757
758   def __init__(self, procline):
759     u = self.UNCONF_RE.match(procline)
760     if u:
761       self.cstatus = self.CS_UNCONFIGURED
762       self.lrole = self.rrole = self.ldisk = self.rdisk = None
763     else:
764       m = self.LINE_RE.match(procline)
765       if not m:
766         raise errors.BlockDeviceError("Can't parse input data '%s'" % procline)
767       self.cstatus = m.group(1)
768       self.lrole = m.group(2)
769       self.rrole = m.group(3)
770       self.ldisk = m.group(4)
771       self.rdisk = m.group(5)
772
773     # end reading of data from the LINE_RE or UNCONF_RE
774
775     self.is_standalone = self.cstatus == self.CS_STANDALONE
776     self.is_wfconn = self.cstatus == self.CS_WFCONNECTION
777     self.is_connected = self.cstatus == self.CS_CONNECTED
778     self.is_primary = self.lrole == self.RO_PRIMARY
779     self.is_secondary = self.lrole == self.RO_SECONDARY
780     self.peer_primary = self.rrole == self.RO_PRIMARY
781     self.peer_secondary = self.rrole == self.RO_SECONDARY
782     self.both_primary = self.is_primary and self.peer_primary
783     self.both_secondary = self.is_secondary and self.peer_secondary
784
785     self.is_diskless = self.ldisk == self.DS_DISKLESS
786     self.is_disk_uptodate = self.ldisk == self.DS_UPTODATE
787
788     self.is_in_resync = self.cstatus in self.CSET_SYNC
789     self.is_in_use = self.cstatus != self.CS_UNCONFIGURED
790
791     m = self.SYNC_RE.match(procline)
792     if m:
793       self.sync_percent = float(m.group(1))
794       hours = int(m.group(2))
795       minutes = int(m.group(3))
796       seconds = int(m.group(4))
797       self.est_time = hours * 3600 + minutes * 60 + seconds
798     else:
799       # we have (in this if branch) no percent information, but if
800       # we're resyncing we need to 'fake' a sync percent information,
801       # as this is how cmdlib determines if it makes sense to wait for
802       # resyncing or not
803       if self.is_in_resync:
804         self.sync_percent = 0
805       else:
806         self.sync_percent = None
807       self.est_time = None
808
809
810 class BaseDRBD(BlockDev): # pylint: disable-msg=W0223
811   """Base DRBD class.
812
813   This class contains a few bits of common functionality between the
814   0.7 and 8.x versions of DRBD.
815
816   """
817   _VERSION_RE = re.compile(r"^version: (\d+)\.(\d+)\.(\d+)"
818                            r" \(api:(\d+)/proto:(\d+)(?:-(\d+))?\)")
819   _VALID_LINE_RE = re.compile("^ *([0-9]+): cs:([^ ]+).*$")
820   _UNUSED_LINE_RE = re.compile("^ *([0-9]+): cs:Unconfigured$")
821
822   _DRBD_MAJOR = 147
823   _ST_UNCONFIGURED = "Unconfigured"
824   _ST_WFCONNECTION = "WFConnection"
825   _ST_CONNECTED = "Connected"
826
827   _STATUS_FILE = "/proc/drbd"
828
829   @staticmethod
830   def _GetProcData(filename=_STATUS_FILE):
831     """Return data from /proc/drbd.
832
833     """
834     try:
835       data = utils.ReadFile(filename).splitlines()
836     except EnvironmentError, err:
837       if err.errno == errno.ENOENT:
838         _ThrowError("The file %s cannot be opened, check if the module"
839                     " is loaded (%s)", filename, str(err))
840       else:
841         _ThrowError("Can't read the DRBD proc file %s: %s", filename, str(err))
842     if not data:
843       _ThrowError("Can't read any data from %s", filename)
844     return data
845
846   @classmethod
847   def _MassageProcData(cls, data):
848     """Transform the output of _GetProdData into a nicer form.
849
850     @return: a dictionary of minor: joined lines from /proc/drbd
851         for that minor
852
853     """
854     results = {}
855     old_minor = old_line = None
856     for line in data:
857       if not line: # completely empty lines, as can be returned by drbd8.0+
858         continue
859       lresult = cls._VALID_LINE_RE.match(line)
860       if lresult is not None:
861         if old_minor is not None:
862           results[old_minor] = old_line
863         old_minor = int(lresult.group(1))
864         old_line = line
865       else:
866         if old_minor is not None:
867           old_line += " " + line.strip()
868     # add last line
869     if old_minor is not None:
870       results[old_minor] = old_line
871     return results
872
873   @classmethod
874   def _GetVersion(cls):
875     """Return the DRBD version.
876
877     This will return a dict with keys:
878       - k_major
879       - k_minor
880       - k_point
881       - api
882       - proto
883       - proto2 (only on drbd > 8.2.X)
884
885     """
886     proc_data = cls._GetProcData()
887     first_line = proc_data[0].strip()
888     version = cls._VERSION_RE.match(first_line)
889     if not version:
890       raise errors.BlockDeviceError("Can't parse DRBD version from '%s'" %
891                                     first_line)
892
893     values = version.groups()
894     retval = {'k_major': int(values[0]),
895               'k_minor': int(values[1]),
896               'k_point': int(values[2]),
897               'api': int(values[3]),
898               'proto': int(values[4]),
899              }
900     if values[5] is not None:
901       retval['proto2'] = values[5]
902
903     return retval
904
905   @staticmethod
906   def _DevPath(minor):
907     """Return the path to a drbd device for a given minor.
908
909     """
910     return "/dev/drbd%d" % minor
911
912   @classmethod
913   def GetUsedDevs(cls):
914     """Compute the list of used DRBD devices.
915
916     """
917     data = cls._GetProcData()
918
919     used_devs = {}
920     for line in data:
921       match = cls._VALID_LINE_RE.match(line)
922       if not match:
923         continue
924       minor = int(match.group(1))
925       state = match.group(2)
926       if state == cls._ST_UNCONFIGURED:
927         continue
928       used_devs[minor] = state, line
929
930     return used_devs
931
932   def _SetFromMinor(self, minor):
933     """Set our parameters based on the given minor.
934
935     This sets our minor variable and our dev_path.
936
937     """
938     if minor is None:
939       self.minor = self.dev_path = None
940       self.attached = False
941     else:
942       self.minor = minor
943       self.dev_path = self._DevPath(minor)
944       self.attached = True
945
946   @staticmethod
947   def _CheckMetaSize(meta_device):
948     """Check if the given meta device looks like a valid one.
949
950     This currently only check the size, which must be around
951     128MiB.
952
953     """
954     result = utils.RunCmd(["blockdev", "--getsize", meta_device])
955     if result.failed:
956       _ThrowError("Failed to get device size: %s - %s",
957                   result.fail_reason, result.output)
958     try:
959       sectors = int(result.stdout)
960     except (TypeError, ValueError):
961       _ThrowError("Invalid output from blockdev: '%s'", result.stdout)
962     bytes = sectors * 512
963     if bytes < 128 * 1024 * 1024: # less than 128MiB
964       _ThrowError("Meta device too small (%.2fMib)", (bytes / 1024 / 1024))
965     # the maximum *valid* size of the meta device when living on top
966     # of LVM is hard to compute: it depends on the number of stripes
967     # and the PE size; e.g. a 2-stripe, 64MB PE will result in a 128MB
968     # (normal size), but an eight-stripe 128MB PE will result in a 1GB
969     # size meta device; as such, we restrict it to 1GB (a little bit
970     # too generous, but making assumptions about PE size is hard)
971     if bytes > 1024 * 1024 * 1024:
972       _ThrowError("Meta device too big (%.2fMiB)", (bytes / 1024 / 1024))
973
974   def Rename(self, new_id):
975     """Rename a device.
976
977     This is not supported for drbd devices.
978
979     """
980     raise errors.ProgrammerError("Can't rename a drbd device")
981
982
983 class DRBD8(BaseDRBD):
984   """DRBD v8.x block device.
985
986   This implements the local host part of the DRBD device, i.e. it
987   doesn't do anything to the supposed peer. If you need a fully
988   connected DRBD pair, you need to use this class on both hosts.
989
990   The unique_id for the drbd device is the (local_ip, local_port,
991   remote_ip, remote_port) tuple, and it must have two children: the
992   data device and the meta_device. The meta device is checked for
993   valid size and is zeroed on create.
994
995   """
996   _MAX_MINORS = 255
997   _PARSE_SHOW = None
998
999   # timeout constants
1000   _NET_RECONFIG_TIMEOUT = 60
1001
1002   def __init__(self, unique_id, children, size):
1003     if children and children.count(None) > 0:
1004       children = []
1005     if len(children) not in (0, 2):
1006       raise ValueError("Invalid configuration data %s" % str(children))
1007     if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 6:
1008       raise ValueError("Invalid configuration data %s" % str(unique_id))
1009     (self._lhost, self._lport,
1010      self._rhost, self._rport,
1011      self._aminor, self._secret) = unique_id
1012     if children:
1013       if not _CanReadDevice(children[1].dev_path):
1014         logging.info("drbd%s: Ignoring unreadable meta device", self._aminor)
1015         children = []
1016     super(DRBD8, self).__init__(unique_id, children, size)
1017     self.major = self._DRBD_MAJOR
1018     version = self._GetVersion()
1019     if version['k_major'] != 8 :
1020       _ThrowError("Mismatch in DRBD kernel version and requested ganeti"
1021                   " usage: kernel is %s.%s, ganeti wants 8.x",
1022                   version['k_major'], version['k_minor'])
1023
1024     if (self._lhost is not None and self._lhost == self._rhost and
1025         self._lport == self._rport):
1026       raise ValueError("Invalid configuration data, same local/remote %s" %
1027                        (unique_id,))
1028     self.Attach()
1029
1030   @classmethod
1031   def _InitMeta(cls, minor, dev_path):
1032     """Initialize a meta device.
1033
1034     This will not work if the given minor is in use.
1035
1036     """
1037     result = utils.RunCmd(["drbdmeta", "--force", cls._DevPath(minor),
1038                            "v08", dev_path, "0", "create-md"])
1039     if result.failed:
1040       _ThrowError("Can't initialize meta device: %s", result.output)
1041
1042   @classmethod
1043   def _FindUnusedMinor(cls):
1044     """Find an unused DRBD device.
1045
1046     This is specific to 8.x as the minors are allocated dynamically,
1047     so non-existing numbers up to a max minor count are actually free.
1048
1049     """
1050     data = cls._GetProcData()
1051
1052     highest = None
1053     for line in data:
1054       match = cls._UNUSED_LINE_RE.match(line)
1055       if match:
1056         return int(match.group(1))
1057       match = cls._VALID_LINE_RE.match(line)
1058       if match:
1059         minor = int(match.group(1))
1060         highest = max(highest, minor)
1061     if highest is None: # there are no minors in use at all
1062       return 0
1063     if highest >= cls._MAX_MINORS:
1064       logging.error("Error: no free drbd minors!")
1065       raise errors.BlockDeviceError("Can't find a free DRBD minor")
1066     return highest + 1
1067
1068   @classmethod
1069   def _GetShowParser(cls):
1070     """Return a parser for `drbd show` output.
1071
1072     This will either create or return an already-create parser for the
1073     output of the command `drbd show`.
1074
1075     """
1076     if cls._PARSE_SHOW is not None:
1077       return cls._PARSE_SHOW
1078
1079     # pyparsing setup
1080     lbrace = pyp.Literal("{").suppress()
1081     rbrace = pyp.Literal("}").suppress()
1082     semi = pyp.Literal(";").suppress()
1083     # this also converts the value to an int
1084     number = pyp.Word(pyp.nums).setParseAction(lambda s, l, t: int(t[0]))
1085
1086     comment = pyp.Literal ("#") + pyp.Optional(pyp.restOfLine)
1087     defa = pyp.Literal("_is_default").suppress()
1088     dbl_quote = pyp.Literal('"').suppress()
1089
1090     keyword = pyp.Word(pyp.alphanums + '-')
1091
1092     # value types
1093     value = pyp.Word(pyp.alphanums + '_-/.:')
1094     quoted = dbl_quote + pyp.CharsNotIn('"') + dbl_quote
1095     addr_type = (pyp.Optional(pyp.Literal("ipv4")).suppress() +
1096                  pyp.Optional(pyp.Literal("ipv6")).suppress())
1097     addr_port = (addr_type + pyp.Word(pyp.nums + '.') +
1098                  pyp.Literal(':').suppress() + number)
1099     # meta device, extended syntax
1100     meta_value = ((value ^ quoted) + pyp.Literal('[').suppress() +
1101                   number + pyp.Word(']').suppress())
1102     # device name, extended syntax
1103     device_value = pyp.Literal("minor").suppress() + number
1104
1105     # a statement
1106     stmt = (~rbrace + keyword + ~lbrace +
1107             pyp.Optional(addr_port ^ value ^ quoted ^ meta_value ^
1108                          device_value) +
1109             pyp.Optional(defa) + semi +
1110             pyp.Optional(pyp.restOfLine).suppress())
1111
1112     # an entire section
1113     section_name = pyp.Word(pyp.alphas + '_')
1114     section = section_name + lbrace + pyp.ZeroOrMore(pyp.Group(stmt)) + rbrace
1115
1116     bnf = pyp.ZeroOrMore(pyp.Group(section ^ stmt))
1117     bnf.ignore(comment)
1118
1119     cls._PARSE_SHOW = bnf
1120
1121     return bnf
1122
1123   @classmethod
1124   def _GetShowData(cls, minor):
1125     """Return the `drbdsetup show` data for a minor.
1126
1127     """
1128     result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "show"])
1129     if result.failed:
1130       logging.error("Can't display the drbd config: %s - %s",
1131                     result.fail_reason, result.output)
1132       return None
1133     return result.stdout
1134
1135   @classmethod
1136   def _GetDevInfo(cls, out):
1137     """Parse details about a given DRBD minor.
1138
1139     This return, if available, the local backing device (as a path)
1140     and the local and remote (ip, port) information from a string
1141     containing the output of the `drbdsetup show` command as returned
1142     by _GetShowData.
1143
1144     """
1145     data = {}
1146     if not out:
1147       return data
1148
1149     bnf = cls._GetShowParser()
1150     # run pyparse
1151
1152     try:
1153       results = bnf.parseString(out)
1154     except pyp.ParseException, err:
1155       _ThrowError("Can't parse drbdsetup show output: %s", str(err))
1156
1157     # and massage the results into our desired format
1158     for section in results:
1159       sname = section[0]
1160       if sname == "_this_host":
1161         for lst in section[1:]:
1162           if lst[0] == "disk":
1163             data["local_dev"] = lst[1]
1164           elif lst[0] == "meta-disk":
1165             data["meta_dev"] = lst[1]
1166             data["meta_index"] = lst[2]
1167           elif lst[0] == "address":
1168             data["local_addr"] = tuple(lst[1:])
1169       elif sname == "_remote_host":
1170         for lst in section[1:]:
1171           if lst[0] == "address":
1172             data["remote_addr"] = tuple(lst[1:])
1173     return data
1174
1175   def _MatchesLocal(self, info):
1176     """Test if our local config matches with an existing device.
1177
1178     The parameter should be as returned from `_GetDevInfo()`. This
1179     method tests if our local backing device is the same as the one in
1180     the info parameter, in effect testing if we look like the given
1181     device.
1182
1183     """
1184     if self._children:
1185       backend, meta = self._children
1186     else:
1187       backend = meta = None
1188
1189     if backend is not None:
1190       retval = ("local_dev" in info and info["local_dev"] == backend.dev_path)
1191     else:
1192       retval = ("local_dev" not in info)
1193
1194     if meta is not None:
1195       retval = retval and ("meta_dev" in info and
1196                            info["meta_dev"] == meta.dev_path)
1197       retval = retval and ("meta_index" in info and
1198                            info["meta_index"] == 0)
1199     else:
1200       retval = retval and ("meta_dev" not in info and
1201                            "meta_index" not in info)
1202     return retval
1203
1204   def _MatchesNet(self, info):
1205     """Test if our network config matches with an existing device.
1206
1207     The parameter should be as returned from `_GetDevInfo()`. This
1208     method tests if our network configuration is the same as the one
1209     in the info parameter, in effect testing if we look like the given
1210     device.
1211
1212     """
1213     if (((self._lhost is None and not ("local_addr" in info)) and
1214          (self._rhost is None and not ("remote_addr" in info)))):
1215       return True
1216
1217     if self._lhost is None:
1218       return False
1219
1220     if not ("local_addr" in info and
1221             "remote_addr" in info):
1222       return False
1223
1224     retval = (info["local_addr"] == (self._lhost, self._lport))
1225     retval = (retval and
1226               info["remote_addr"] == (self._rhost, self._rport))
1227     return retval
1228
1229   @classmethod
1230   def _AssembleLocal(cls, minor, backend, meta, size):
1231     """Configure the local part of a DRBD device.
1232
1233     """
1234     args = ["drbdsetup", cls._DevPath(minor), "disk",
1235             backend, meta, "0",
1236             "-e", "detach",
1237             "--create-device"]
1238     if size:
1239       args.extend(["-d", "%sm" % size])
1240     if not constants.DRBD_BARRIERS: # disable barriers, if configured so
1241       version = cls._GetVersion()
1242       # various DRBD versions support different disk barrier options;
1243       # what we aim here is to revert back to the 'drain' method of
1244       # disk flushes and to disable metadata barriers, in effect going
1245       # back to pre-8.0.7 behaviour
1246       vmaj = version['k_major']
1247       vmin = version['k_minor']
1248       vrel = version['k_point']
1249       assert vmaj == 8
1250       if vmin == 0: # 8.0.x
1251         if vrel >= 12:
1252           args.extend(['-i', '-m'])
1253       elif vmin == 2: # 8.2.x
1254         if vrel >= 7:
1255           args.extend(['-i', '-m'])
1256       elif vmaj >= 3: # 8.3.x or newer
1257         args.extend(['-i', '-a', 'm'])
1258     result = utils.RunCmd(args)
1259     if result.failed:
1260       _ThrowError("drbd%d: can't attach local disk: %s", minor, result.output)
1261
1262   @classmethod
1263   def _AssembleNet(cls, minor, net_info, protocol,
1264                    dual_pri=False, hmac=None, secret=None):
1265     """Configure the network part of the device.
1266
1267     """
1268     lhost, lport, rhost, rport = net_info
1269     if None in net_info:
1270       # we don't want network connection and actually want to make
1271       # sure its shutdown
1272       cls._ShutdownNet(minor)
1273       return
1274
1275     # Workaround for a race condition. When DRBD is doing its dance to
1276     # establish a connection with its peer, it also sends the
1277     # synchronization speed over the wire. In some cases setting the
1278     # sync speed only after setting up both sides can race with DRBD
1279     # connecting, hence we set it here before telling DRBD anything
1280     # about its peer.
1281     cls._SetMinorSyncSpeed(minor, constants.SYNC_SPEED)
1282
1283     args = ["drbdsetup", cls._DevPath(minor), "net",
1284             "%s:%s" % (lhost, lport), "%s:%s" % (rhost, rport), protocol,
1285             "-A", "discard-zero-changes",
1286             "-B", "consensus",
1287             "--create-device",
1288             ]
1289     if dual_pri:
1290       args.append("-m")
1291     if hmac and secret:
1292       args.extend(["-a", hmac, "-x", secret])
1293     result = utils.RunCmd(args)
1294     if result.failed:
1295       _ThrowError("drbd%d: can't setup network: %s - %s",
1296                   minor, result.fail_reason, result.output)
1297
1298     def _CheckNetworkConfig():
1299       info = cls._GetDevInfo(cls._GetShowData(minor))
1300       if not "local_addr" in info or not "remote_addr" in info:
1301         raise utils.RetryAgain()
1302
1303       if (info["local_addr"] != (lhost, lport) or
1304           info["remote_addr"] != (rhost, rport)):
1305         raise utils.RetryAgain()
1306
1307     try:
1308       utils.Retry(_CheckNetworkConfig, 1.0, 10.0)
1309     except utils.RetryTimeout:
1310       _ThrowError("drbd%d: timeout while configuring network", minor)
1311
1312   def AddChildren(self, devices):
1313     """Add a disk to the DRBD device.
1314
1315     """
1316     if self.minor is None:
1317       _ThrowError("drbd%d: can't attach to dbrd8 during AddChildren",
1318                   self._aminor)
1319     if len(devices) != 2:
1320       _ThrowError("drbd%d: need two devices for AddChildren", self.minor)
1321     info = self._GetDevInfo(self._GetShowData(self.minor))
1322     if "local_dev" in info:
1323       _ThrowError("drbd%d: already attached to a local disk", self.minor)
1324     backend, meta = devices
1325     if backend.dev_path is None or meta.dev_path is None:
1326       _ThrowError("drbd%d: children not ready during AddChildren", self.minor)
1327     backend.Open()
1328     meta.Open()
1329     self._CheckMetaSize(meta.dev_path)
1330     self._InitMeta(self._FindUnusedMinor(), meta.dev_path)
1331
1332     self._AssembleLocal(self.minor, backend.dev_path, meta.dev_path, self.size)
1333     self._children = devices
1334
1335   def RemoveChildren(self, devices):
1336     """Detach the drbd device from local storage.
1337
1338     """
1339     if self.minor is None:
1340       _ThrowError("drbd%d: can't attach to drbd8 during RemoveChildren",
1341                   self._aminor)
1342     # early return if we don't actually have backing storage
1343     info = self._GetDevInfo(self._GetShowData(self.minor))
1344     if "local_dev" not in info:
1345       return
1346     if len(self._children) != 2:
1347       _ThrowError("drbd%d: we don't have two children: %s", self.minor,
1348                   self._children)
1349     if self._children.count(None) == 2: # we don't actually have children :)
1350       logging.warning("drbd%d: requested detach while detached", self.minor)
1351       return
1352     if len(devices) != 2:
1353       _ThrowError("drbd%d: we need two children in RemoveChildren", self.minor)
1354     for child, dev in zip(self._children, devices):
1355       if dev != child.dev_path:
1356         _ThrowError("drbd%d: mismatch in local storage (%s != %s) in"
1357                     " RemoveChildren", self.minor, dev, child.dev_path)
1358
1359     self._ShutdownLocal(self.minor)
1360     self._children = []
1361
1362   @classmethod
1363   def _SetMinorSyncSpeed(cls, minor, kbytes):
1364     """Set the speed of the DRBD syncer.
1365
1366     This is the low-level implementation.
1367
1368     @type minor: int
1369     @param minor: the drbd minor whose settings we change
1370     @type kbytes: int
1371     @param kbytes: the speed in kbytes/second
1372     @rtype: boolean
1373     @return: the success of the operation
1374
1375     """
1376     result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "syncer",
1377                            "-r", "%d" % kbytes, "--create-device"])
1378     if result.failed:
1379       logging.error("Can't change syncer rate: %s - %s",
1380                     result.fail_reason, result.output)
1381     return not result.failed
1382
1383   def SetSyncSpeed(self, kbytes):
1384     """Set the speed of the DRBD syncer.
1385
1386     @type kbytes: int
1387     @param kbytes: the speed in kbytes/second
1388     @rtype: boolean
1389     @return: the success of the operation
1390
1391     """
1392     if self.minor is None:
1393       logging.info("Not attached during SetSyncSpeed")
1394       return False
1395     children_result = super(DRBD8, self).SetSyncSpeed(kbytes)
1396     return self._SetMinorSyncSpeed(self.minor, kbytes) and children_result
1397
1398   def GetProcStatus(self):
1399     """Return device data from /proc.
1400
1401     """
1402     if self.minor is None:
1403       _ThrowError("drbd%d: GetStats() called while not attached", self._aminor)
1404     proc_info = self._MassageProcData(self._GetProcData())
1405     if self.minor not in proc_info:
1406       _ThrowError("drbd%d: can't find myself in /proc", self.minor)
1407     return DRBD8Status(proc_info[self.minor])
1408
1409   def GetSyncStatus(self):
1410     """Returns the sync status of the device.
1411
1412
1413     If sync_percent is None, it means all is ok
1414     If estimated_time is None, it means we can't estimate
1415     the time needed, otherwise it's the time left in seconds.
1416
1417
1418     We set the is_degraded parameter to True on two conditions:
1419     network not connected or local disk missing.
1420
1421     We compute the ldisk parameter based on whether we have a local
1422     disk or not.
1423
1424     @rtype: objects.BlockDevStatus
1425
1426     """
1427     if self.minor is None and not self.Attach():
1428       _ThrowError("drbd%d: can't Attach() in GetSyncStatus", self._aminor)
1429
1430     stats = self.GetProcStatus()
1431     is_degraded = not stats.is_connected or not stats.is_disk_uptodate
1432
1433     if stats.is_disk_uptodate:
1434       ldisk_status = constants.LDS_OKAY
1435     elif stats.is_diskless:
1436       ldisk_status = constants.LDS_FAULTY
1437     else:
1438       ldisk_status = constants.LDS_UNKNOWN
1439
1440     return objects.BlockDevStatus(dev_path=self.dev_path,
1441                                   major=self.major,
1442                                   minor=self.minor,
1443                                   sync_percent=stats.sync_percent,
1444                                   estimated_time=stats.est_time,
1445                                   is_degraded=is_degraded,
1446                                   ldisk_status=ldisk_status)
1447
1448   def Open(self, force=False):
1449     """Make the local state primary.
1450
1451     If the 'force' parameter is given, the '-o' option is passed to
1452     drbdsetup. Since this is a potentially dangerous operation, the
1453     force flag should be only given after creation, when it actually
1454     is mandatory.
1455
1456     """
1457     if self.minor is None and not self.Attach():
1458       logging.error("DRBD cannot attach to a device during open")
1459       return False
1460     cmd = ["drbdsetup", self.dev_path, "primary"]
1461     if force:
1462       cmd.append("-o")
1463     result = utils.RunCmd(cmd)
1464     if result.failed:
1465       _ThrowError("drbd%d: can't make drbd device primary: %s", self.minor,
1466                   result.output)
1467
1468   def Close(self):
1469     """Make the local state secondary.
1470
1471     This will, of course, fail if the device is in use.
1472
1473     """
1474     if self.minor is None and not self.Attach():
1475       _ThrowError("drbd%d: can't Attach() in Close()", self._aminor)
1476     result = utils.RunCmd(["drbdsetup", self.dev_path, "secondary"])
1477     if result.failed:
1478       _ThrowError("drbd%d: can't switch drbd device to secondary: %s",
1479                   self.minor, result.output)
1480
1481   def DisconnectNet(self):
1482     """Removes network configuration.
1483
1484     This method shutdowns the network side of the device.
1485
1486     The method will wait up to a hardcoded timeout for the device to
1487     go into standalone after the 'disconnect' command before
1488     re-configuring it, as sometimes it takes a while for the
1489     disconnect to actually propagate and thus we might issue a 'net'
1490     command while the device is still connected. If the device will
1491     still be attached to the network and we time out, we raise an
1492     exception.
1493
1494     """
1495     if self.minor is None:
1496       _ThrowError("drbd%d: disk not attached in re-attach net", self._aminor)
1497
1498     if None in (self._lhost, self._lport, self._rhost, self._rport):
1499       _ThrowError("drbd%d: DRBD disk missing network info in"
1500                   " DisconnectNet()", self.minor)
1501
1502     class _DisconnectStatus:
1503       def __init__(self, ever_disconnected):
1504         self.ever_disconnected = ever_disconnected
1505
1506     dstatus = _DisconnectStatus(_IgnoreError(self._ShutdownNet, self.minor))
1507
1508     def _WaitForDisconnect():
1509       if self.GetProcStatus().is_standalone:
1510         return
1511
1512       # retry the disconnect, it seems possible that due to a well-time
1513       # disconnect on the peer, my disconnect command might be ignored and
1514       # forgotten
1515       dstatus.ever_disconnected = \
1516         _IgnoreError(self._ShutdownNet, self.minor) or dstatus.ever_disconnected
1517
1518       raise utils.RetryAgain()
1519
1520     # Keep start time
1521     start_time = time.time()
1522
1523     try:
1524       # Start delay at 100 milliseconds and grow up to 2 seconds
1525       utils.Retry(_WaitForDisconnect, (0.1, 1.5, 2.0),
1526                   self._NET_RECONFIG_TIMEOUT)
1527     except utils.RetryTimeout:
1528       if dstatus.ever_disconnected:
1529         msg = ("drbd%d: device did not react to the"
1530                " 'disconnect' command in a timely manner")
1531       else:
1532         msg = "drbd%d: can't shutdown network, even after multiple retries"
1533
1534       _ThrowError(msg, self.minor)
1535
1536     reconfig_time = time.time() - start_time
1537     if reconfig_time > (self._NET_RECONFIG_TIMEOUT * 0.25):
1538       logging.info("drbd%d: DisconnectNet: detach took %.3f seconds",
1539                    self.minor, reconfig_time)
1540
1541   def AttachNet(self, multimaster):
1542     """Reconnects the network.
1543
1544     This method connects the network side of the device with a
1545     specified multi-master flag. The device needs to be 'Standalone'
1546     but have valid network configuration data.
1547
1548     Args:
1549       - multimaster: init the network in dual-primary mode
1550
1551     """
1552     if self.minor is None:
1553       _ThrowError("drbd%d: device not attached in AttachNet", self._aminor)
1554
1555     if None in (self._lhost, self._lport, self._rhost, self._rport):
1556       _ThrowError("drbd%d: missing network info in AttachNet()", self.minor)
1557
1558     status = self.GetProcStatus()
1559
1560     if not status.is_standalone:
1561       _ThrowError("drbd%d: device is not standalone in AttachNet", self.minor)
1562
1563     self._AssembleNet(self.minor,
1564                       (self._lhost, self._lport, self._rhost, self._rport),
1565                       constants.DRBD_NET_PROTOCOL, dual_pri=multimaster,
1566                       hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1567
1568   def Attach(self):
1569     """Check if our minor is configured.
1570
1571     This doesn't do any device configurations - it only checks if the
1572     minor is in a state different from Unconfigured.
1573
1574     Note that this function will not change the state of the system in
1575     any way (except in case of side-effects caused by reading from
1576     /proc).
1577
1578     """
1579     used_devs = self.GetUsedDevs()
1580     if self._aminor in used_devs:
1581       minor = self._aminor
1582     else:
1583       minor = None
1584
1585     self._SetFromMinor(minor)
1586     return minor is not None
1587
1588   def Assemble(self):
1589     """Assemble the drbd.
1590
1591     Method:
1592       - if we have a configured device, we try to ensure that it matches
1593         our config
1594       - if not, we create it from zero
1595
1596     """
1597     super(DRBD8, self).Assemble()
1598
1599     self.Attach()
1600     if self.minor is None:
1601       # local device completely unconfigured
1602       self._FastAssemble()
1603     else:
1604       # we have to recheck the local and network status and try to fix
1605       # the device
1606       self._SlowAssemble()
1607
1608   def _SlowAssemble(self):
1609     """Assembles the DRBD device from a (partially) configured device.
1610
1611     In case of partially attached (local device matches but no network
1612     setup), we perform the network attach. If successful, we re-test
1613     the attach if can return success.
1614
1615     """
1616     # TODO: Rewrite to not use a for loop just because there is 'break'
1617     # pylint: disable-msg=W0631
1618     net_data = (self._lhost, self._lport, self._rhost, self._rport)
1619     for minor in (self._aminor,):
1620       info = self._GetDevInfo(self._GetShowData(minor))
1621       match_l = self._MatchesLocal(info)
1622       match_r = self._MatchesNet(info)
1623
1624       if match_l and match_r:
1625         # everything matches
1626         break
1627
1628       if match_l and not match_r and "local_addr" not in info:
1629         # disk matches, but not attached to network, attach and recheck
1630         self._AssembleNet(minor, net_data, constants.DRBD_NET_PROTOCOL,
1631                           hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1632         if self._MatchesNet(self._GetDevInfo(self._GetShowData(minor))):
1633           break
1634         else:
1635           _ThrowError("drbd%d: network attach successful, but 'drbdsetup"
1636                       " show' disagrees", minor)
1637
1638       if match_r and "local_dev" not in info:
1639         # no local disk, but network attached and it matches
1640         self._AssembleLocal(minor, self._children[0].dev_path,
1641                             self._children[1].dev_path, self.size)
1642         if self._MatchesNet(self._GetDevInfo(self._GetShowData(minor))):
1643           break
1644         else:
1645           _ThrowError("drbd%d: disk attach successful, but 'drbdsetup"
1646                       " show' disagrees", minor)
1647
1648       # this case must be considered only if we actually have local
1649       # storage, i.e. not in diskless mode, because all diskless
1650       # devices are equal from the point of view of local
1651       # configuration
1652       if (match_l and "local_dev" in info and
1653           not match_r and "local_addr" in info):
1654         # strange case - the device network part points to somewhere
1655         # else, even though its local storage is ours; as we own the
1656         # drbd space, we try to disconnect from the remote peer and
1657         # reconnect to our correct one
1658         try:
1659           self._ShutdownNet(minor)
1660         except errors.BlockDeviceError, err:
1661           _ThrowError("drbd%d: device has correct local storage, wrong"
1662                       " remote peer and is unable to disconnect in order"
1663                       " to attach to the correct peer: %s", minor, str(err))
1664         # note: _AssembleNet also handles the case when we don't want
1665         # local storage (i.e. one or more of the _[lr](host|port) is
1666         # None)
1667         self._AssembleNet(minor, net_data, constants.DRBD_NET_PROTOCOL,
1668                           hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1669         if self._MatchesNet(self._GetDevInfo(self._GetShowData(minor))):
1670           break
1671         else:
1672           _ThrowError("drbd%d: network attach successful, but 'drbdsetup"
1673                       " show' disagrees", minor)
1674
1675     else:
1676       minor = None
1677
1678     self._SetFromMinor(minor)
1679     if minor is None:
1680       _ThrowError("drbd%d: cannot activate, unknown or unhandled reason",
1681                   self._aminor)
1682
1683   def _FastAssemble(self):
1684     """Assemble the drbd device from zero.
1685
1686     This is run when in Assemble we detect our minor is unused.
1687
1688     """
1689     minor = self._aminor
1690     if self._children and self._children[0] and self._children[1]:
1691       self._AssembleLocal(minor, self._children[0].dev_path,
1692                           self._children[1].dev_path, self.size)
1693     if self._lhost and self._lport and self._rhost and self._rport:
1694       self._AssembleNet(minor,
1695                         (self._lhost, self._lport, self._rhost, self._rport),
1696                         constants.DRBD_NET_PROTOCOL,
1697                         hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1698     self._SetFromMinor(minor)
1699
1700   @classmethod
1701   def _ShutdownLocal(cls, minor):
1702     """Detach from the local device.
1703
1704     I/Os will continue to be served from the remote device. If we
1705     don't have a remote device, this operation will fail.
1706
1707     """
1708     result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "detach"])
1709     if result.failed:
1710       _ThrowError("drbd%d: can't detach local disk: %s", minor, result.output)
1711
1712   @classmethod
1713   def _ShutdownNet(cls, minor):
1714     """Disconnect from the remote peer.
1715
1716     This fails if we don't have a local device.
1717
1718     """
1719     result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "disconnect"])
1720     if result.failed:
1721       _ThrowError("drbd%d: can't shutdown network: %s", minor, result.output)
1722
1723   @classmethod
1724   def _ShutdownAll(cls, minor):
1725     """Deactivate the device.
1726
1727     This will, of course, fail if the device is in use.
1728
1729     """
1730     result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "down"])
1731     if result.failed:
1732       _ThrowError("drbd%d: can't shutdown drbd device: %s",
1733                   minor, result.output)
1734
1735   def Shutdown(self):
1736     """Shutdown the DRBD device.
1737
1738     """
1739     if self.minor is None and not self.Attach():
1740       logging.info("drbd%d: not attached during Shutdown()", self._aminor)
1741       return
1742     minor = self.minor
1743     self.minor = None
1744     self.dev_path = None
1745     self._ShutdownAll(minor)
1746
1747   def Remove(self):
1748     """Stub remove for DRBD devices.
1749
1750     """
1751     self.Shutdown()
1752
1753   @classmethod
1754   def Create(cls, unique_id, children, size):
1755     """Create a new DRBD8 device.
1756
1757     Since DRBD devices are not created per se, just assembled, this
1758     function only initializes the metadata.
1759
1760     """
1761     if len(children) != 2:
1762       raise errors.ProgrammerError("Invalid setup for the drbd device")
1763     # check that the minor is unused
1764     aminor = unique_id[4]
1765     proc_info = cls._MassageProcData(cls._GetProcData())
1766     if aminor in proc_info:
1767       status = DRBD8Status(proc_info[aminor])
1768       in_use = status.is_in_use
1769     else:
1770       in_use = False
1771     if in_use:
1772       _ThrowError("drbd%d: minor is already in use at Create() time", aminor)
1773     meta = children[1]
1774     meta.Assemble()
1775     if not meta.Attach():
1776       _ThrowError("drbd%d: can't attach to meta device '%s'",
1777                   aminor, meta)
1778     cls._CheckMetaSize(meta.dev_path)
1779     cls._InitMeta(aminor, meta.dev_path)
1780     return cls(unique_id, children, size)
1781
1782   def Grow(self, amount):
1783     """Resize the DRBD device and its backing storage.
1784
1785     """
1786     if self.minor is None:
1787       _ThrowError("drbd%d: Grow called while not attached", self._aminor)
1788     if len(self._children) != 2 or None in self._children:
1789       _ThrowError("drbd%d: cannot grow diskless device", self.minor)
1790     self._children[0].Grow(amount)
1791     result = utils.RunCmd(["drbdsetup", self.dev_path, "resize", "-s",
1792                            "%dm" % (self.size + amount)])
1793     if result.failed:
1794       _ThrowError("drbd%d: resize failed: %s", self.minor, result.output)
1795
1796
1797 class FileStorage(BlockDev):
1798   """File device.
1799
1800   This class represents the a file storage backend device.
1801
1802   The unique_id for the file device is a (file_driver, file_path) tuple.
1803
1804   """
1805   def __init__(self, unique_id, children, size):
1806     """Initalizes a file device backend.
1807
1808     """
1809     if children:
1810       raise errors.BlockDeviceError("Invalid setup for file device")
1811     super(FileStorage, self).__init__(unique_id, children, size)
1812     if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
1813       raise ValueError("Invalid configuration data %s" % str(unique_id))
1814     self.driver = unique_id[0]
1815     self.dev_path = unique_id[1]
1816     self.Attach()
1817
1818   def Assemble(self):
1819     """Assemble the device.
1820
1821     Checks whether the file device exists, raises BlockDeviceError otherwise.
1822
1823     """
1824     if not os.path.exists(self.dev_path):
1825       _ThrowError("File device '%s' does not exist" % self.dev_path)
1826
1827   def Shutdown(self):
1828     """Shutdown the device.
1829
1830     This is a no-op for the file type, as we don't deactivate
1831     the file on shutdown.
1832
1833     """
1834     pass
1835
1836   def Open(self, force=False):
1837     """Make the device ready for I/O.
1838
1839     This is a no-op for the file type.
1840
1841     """
1842     pass
1843
1844   def Close(self):
1845     """Notifies that the device will no longer be used for I/O.
1846
1847     This is a no-op for the file type.
1848
1849     """
1850     pass
1851
1852   def Remove(self):
1853     """Remove the file backing the block device.
1854
1855     @rtype: boolean
1856     @return: True if the removal was successful
1857
1858     """
1859     try:
1860       os.remove(self.dev_path)
1861     except OSError, err:
1862       if err.errno != errno.ENOENT:
1863         _ThrowError("Can't remove file '%s': %s", self.dev_path, err)
1864
1865   def Rename(self, new_id):
1866     """Renames the file.
1867
1868     """
1869     # TODO: implement rename for file-based storage
1870     _ThrowError("Rename is not supported for file-based storage")
1871
1872   def Grow(self, amount):
1873     """Grow the file
1874
1875     @param amount: the amount (in mebibytes) to grow with
1876
1877     """
1878     # Check that the file exists
1879     self.Assemble()
1880     current_size = self.GetActualSize()
1881     new_size = current_size + amount * 1024 * 1024
1882     assert new_size > current_size, "Cannot Grow with a negative amount"
1883     try:
1884       f = open(self.dev_path, "a+")
1885       f.truncate(new_size)
1886       f.close()
1887     except EnvironmentError, err:
1888       _ThrowError("Error in file growth: %", str(err))
1889
1890   def Attach(self):
1891     """Attach to an existing file.
1892
1893     Check if this file already exists.
1894
1895     @rtype: boolean
1896     @return: True if file exists
1897
1898     """
1899     self.attached = os.path.exists(self.dev_path)
1900     return self.attached
1901
1902   def GetActualSize(self):
1903     """Return the actual disk size.
1904
1905     @note: the device needs to be active when this is called
1906
1907     """
1908     assert self.attached, "BlockDevice not attached in GetActualSize()"
1909     try:
1910       st = os.stat(self.dev_path)
1911       return st.st_size
1912     except OSError, err:
1913       _ThrowError("Can't stat %s: %s", self.dev_path, err)
1914
1915   @classmethod
1916   def Create(cls, unique_id, children, size):
1917     """Create a new file.
1918
1919     @param size: the size of file in MiB
1920
1921     @rtype: L{bdev.FileStorage}
1922     @return: an instance of FileStorage
1923
1924     """
1925     if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
1926       raise ValueError("Invalid configuration data %s" % str(unique_id))
1927     dev_path = unique_id[1]
1928     try:
1929       fd = os.open(dev_path, os.O_RDWR | os.O_CREAT | os.O_EXCL)
1930       f = os.fdopen(fd, "w")
1931       f.truncate(size * 1024 * 1024)
1932       f.close()
1933     except EnvironmentError, err:
1934       if err.errno == errno.EEXIST:
1935         _ThrowError("File already existing: %s", dev_path)
1936       _ThrowError("Error in file creation: %", str(err))
1937
1938     return FileStorage(unique_id, children, size)
1939
1940
1941 DEV_MAP = {
1942   constants.LD_LV: LogicalVolume,
1943   constants.LD_DRBD8: DRBD8,
1944   }
1945
1946 if constants.ENABLE_FILE_STORAGE:
1947   DEV_MAP[constants.LD_FILE] = FileStorage
1948
1949
1950 def FindDevice(dev_type, unique_id, children, size):
1951   """Search for an existing, assembled device.
1952
1953   This will succeed only if the device exists and is assembled, but it
1954   does not do any actions in order to activate the device.
1955
1956   """
1957   if dev_type not in DEV_MAP:
1958     raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
1959   device = DEV_MAP[dev_type](unique_id, children, size)
1960   if not device.attached:
1961     return None
1962   return device
1963
1964
1965 def Assemble(dev_type, unique_id, children, size):
1966   """Try to attach or assemble an existing device.
1967
1968   This will attach to assemble the device, as needed, to bring it
1969   fully up. It must be safe to run on already-assembled devices.
1970
1971   """
1972   if dev_type not in DEV_MAP:
1973     raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
1974   device = DEV_MAP[dev_type](unique_id, children, size)
1975   device.Assemble()
1976   return device
1977
1978
1979 def Create(dev_type, unique_id, children, size):
1980   """Create a device.
1981
1982   """
1983   if dev_type not in DEV_MAP:
1984     raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
1985   device = DEV_MAP[dev_type].Create(unique_id, children, size)
1986   return device