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