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