Add uuid attributes to configuration entities
[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       lresult = lmatch.match(line)
807       if lresult is not None:
808         if old_minor is not None:
809           results[old_minor] = old_line
810         old_minor = int(lresult.group(1))
811         old_line = line
812       else:
813         if old_minor is not None:
814           old_line += " " + line.strip()
815     # add last line
816     if old_minor is not None:
817       results[old_minor] = old_line
818     return results
819
820   @classmethod
821   def _GetVersion(cls):
822     """Return the DRBD version.
823
824     This will return a dict with keys:
825       - k_major
826       - k_minor
827       - k_point
828       - api
829       - proto
830       - proto2 (only on drbd > 8.2.X)
831
832     """
833     proc_data = cls._GetProcData()
834     first_line = proc_data[0].strip()
835     version = cls._VERSION_RE.match(first_line)
836     if not version:
837       raise errors.BlockDeviceError("Can't parse DRBD version from '%s'" %
838                                     first_line)
839
840     values = version.groups()
841     retval = {'k_major': int(values[0]),
842               'k_minor': int(values[1]),
843               'k_point': int(values[2]),
844               'api': int(values[3]),
845               'proto': int(values[4]),
846              }
847     if values[5] is not None:
848       retval['proto2'] = values[5]
849
850     return retval
851
852   @staticmethod
853   def _DevPath(minor):
854     """Return the path to a drbd device for a given minor.
855
856     """
857     return "/dev/drbd%d" % minor
858
859   @classmethod
860   def GetUsedDevs(cls):
861     """Compute the list of used DRBD devices.
862
863     """
864     data = cls._GetProcData()
865
866     used_devs = {}
867     valid_line = re.compile("^ *([0-9]+): cs:([^ ]+).*$")
868     for line in data:
869       match = valid_line.match(line)
870       if not match:
871         continue
872       minor = int(match.group(1))
873       state = match.group(2)
874       if state == cls._ST_UNCONFIGURED:
875         continue
876       used_devs[minor] = state, line
877
878     return used_devs
879
880   def _SetFromMinor(self, minor):
881     """Set our parameters based on the given minor.
882
883     This sets our minor variable and our dev_path.
884
885     """
886     if minor is None:
887       self.minor = self.dev_path = None
888       self.attached = False
889     else:
890       self.minor = minor
891       self.dev_path = self._DevPath(minor)
892       self.attached = True
893
894   @staticmethod
895   def _CheckMetaSize(meta_device):
896     """Check if the given meta device looks like a valid one.
897
898     This currently only check the size, which must be around
899     128MiB.
900
901     """
902     result = utils.RunCmd(["blockdev", "--getsize", meta_device])
903     if result.failed:
904       _ThrowError("Failed to get device size: %s - %s",
905                   result.fail_reason, result.output)
906     try:
907       sectors = int(result.stdout)
908     except ValueError:
909       _ThrowError("Invalid output from blockdev: '%s'", result.stdout)
910     bytes = sectors * 512
911     if bytes < 128 * 1024 * 1024: # less than 128MiB
912       _ThrowError("Meta device too small (%.2fMib)", (bytes / 1024 / 1024))
913     # the maximum *valid* size of the meta device when living on top
914     # of LVM is hard to compute: it depends on the number of stripes
915     # and the PE size; e.g. a 2-stripe, 64MB PE will result in a 128MB
916     # (normal size), but an eight-stripe 128MB PE will result in a 1GB
917     # size meta device; as such, we restrict it to 1GB (a little bit
918     # too generous, but making assumptions about PE size is hard)
919     if bytes > 1024 * 1024 * 1024:
920       _ThrowError("Meta device too big (%.2fMiB)", (bytes / 1024 / 1024))
921
922   def Rename(self, new_id):
923     """Rename a device.
924
925     This is not supported for drbd devices.
926
927     """
928     raise errors.ProgrammerError("Can't rename a drbd device")
929
930
931 class DRBD8(BaseDRBD):
932   """DRBD v8.x block device.
933
934   This implements the local host part of the DRBD device, i.e. it
935   doesn't do anything to the supposed peer. If you need a fully
936   connected DRBD pair, you need to use this class on both hosts.
937
938   The unique_id for the drbd device is the (local_ip, local_port,
939   remote_ip, remote_port) tuple, and it must have two children: the
940   data device and the meta_device. The meta device is checked for
941   valid size and is zeroed on create.
942
943   """
944   _MAX_MINORS = 255
945   _PARSE_SHOW = None
946
947   # timeout constants
948   _NET_RECONFIG_TIMEOUT = 60
949
950   def __init__(self, unique_id, children, size):
951     if children and children.count(None) > 0:
952       children = []
953     super(DRBD8, self).__init__(unique_id, children, size)
954     self.major = self._DRBD_MAJOR
955     version = self._GetVersion()
956     if version['k_major'] != 8 :
957       _ThrowError("Mismatch in DRBD kernel version and requested ganeti"
958                   " usage: kernel is %s.%s, ganeti wants 8.x",
959                   version['k_major'], version['k_minor'])
960
961     if len(children) not in (0, 2):
962       raise ValueError("Invalid configuration data %s" % str(children))
963     if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 6:
964       raise ValueError("Invalid configuration data %s" % str(unique_id))
965     (self._lhost, self._lport,
966      self._rhost, self._rport,
967      self._aminor, self._secret) = unique_id
968     if (self._lhost is not None and self._lhost == self._rhost and
969         self._lport == self._rport):
970       raise ValueError("Invalid configuration data, same local/remote %s" %
971                        (unique_id,))
972     self.Attach()
973
974   @classmethod
975   def _InitMeta(cls, minor, dev_path):
976     """Initialize a meta device.
977
978     This will not work if the given minor is in use.
979
980     """
981     result = utils.RunCmd(["drbdmeta", "--force", cls._DevPath(minor),
982                            "v08", dev_path, "0", "create-md"])
983     if result.failed:
984       _ThrowError("Can't initialize meta device: %s", result.output)
985
986   @classmethod
987   def _FindUnusedMinor(cls):
988     """Find an unused DRBD device.
989
990     This is specific to 8.x as the minors are allocated dynamically,
991     so non-existing numbers up to a max minor count are actually free.
992
993     """
994     data = cls._GetProcData()
995
996     unused_line = re.compile("^ *([0-9]+): cs:Unconfigured$")
997     used_line = re.compile("^ *([0-9]+): cs:")
998     highest = None
999     for line in data:
1000       match = unused_line.match(line)
1001       if match:
1002         return int(match.group(1))
1003       match = used_line.match(line)
1004       if match:
1005         minor = int(match.group(1))
1006         highest = max(highest, minor)
1007     if highest is None: # there are no minors in use at all
1008       return 0
1009     if highest >= cls._MAX_MINORS:
1010       logging.error("Error: no free drbd minors!")
1011       raise errors.BlockDeviceError("Can't find a free DRBD minor")
1012     return highest + 1
1013
1014   @classmethod
1015   def _GetShowParser(cls):
1016     """Return a parser for `drbd show` output.
1017
1018     This will either create or return an already-create parser for the
1019     output of the command `drbd show`.
1020
1021     """
1022     if cls._PARSE_SHOW is not None:
1023       return cls._PARSE_SHOW
1024
1025     # pyparsing setup
1026     lbrace = pyp.Literal("{").suppress()
1027     rbrace = pyp.Literal("}").suppress()
1028     semi = pyp.Literal(";").suppress()
1029     # this also converts the value to an int
1030     number = pyp.Word(pyp.nums).setParseAction(lambda s, l, t: int(t[0]))
1031
1032     comment = pyp.Literal ("#") + pyp.Optional(pyp.restOfLine)
1033     defa = pyp.Literal("_is_default").suppress()
1034     dbl_quote = pyp.Literal('"').suppress()
1035
1036     keyword = pyp.Word(pyp.alphanums + '-')
1037
1038     # value types
1039     value = pyp.Word(pyp.alphanums + '_-/.:')
1040     quoted = dbl_quote + pyp.CharsNotIn('"') + dbl_quote
1041     addr_type = (pyp.Optional(pyp.Literal("ipv4")).suppress() +
1042                  pyp.Optional(pyp.Literal("ipv6")).suppress())
1043     addr_port = (addr_type + pyp.Word(pyp.nums + '.') +
1044                  pyp.Literal(':').suppress() + number)
1045     # meta device, extended syntax
1046     meta_value = ((value ^ quoted) + pyp.Literal('[').suppress() +
1047                   number + pyp.Word(']').suppress())
1048     # device name, extended syntax
1049     device_value = pyp.Literal("minor").suppress() + number
1050
1051     # a statement
1052     stmt = (~rbrace + keyword + ~lbrace +
1053             pyp.Optional(addr_port ^ value ^ quoted ^ meta_value ^
1054                          device_value) +
1055             pyp.Optional(defa) + semi +
1056             pyp.Optional(pyp.restOfLine).suppress())
1057
1058     # an entire section
1059     section_name = pyp.Word(pyp.alphas + '_')
1060     section = section_name + lbrace + pyp.ZeroOrMore(pyp.Group(stmt)) + rbrace
1061
1062     bnf = pyp.ZeroOrMore(pyp.Group(section ^ stmt))
1063     bnf.ignore(comment)
1064
1065     cls._PARSE_SHOW = bnf
1066
1067     return bnf
1068
1069   @classmethod
1070   def _GetShowData(cls, minor):
1071     """Return the `drbdsetup show` data for a minor.
1072
1073     """
1074     result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "show"])
1075     if result.failed:
1076       logging.error("Can't display the drbd config: %s - %s",
1077                     result.fail_reason, result.output)
1078       return None
1079     return result.stdout
1080
1081   @classmethod
1082   def _GetDevInfo(cls, out):
1083     """Parse details about a given DRBD minor.
1084
1085     This return, if available, the local backing device (as a path)
1086     and the local and remote (ip, port) information from a string
1087     containing the output of the `drbdsetup show` command as returned
1088     by _GetShowData.
1089
1090     """
1091     data = {}
1092     if not out:
1093       return data
1094
1095     bnf = cls._GetShowParser()
1096     # run pyparse
1097
1098     try:
1099       results = bnf.parseString(out)
1100     except pyp.ParseException, err:
1101       _ThrowError("Can't parse drbdsetup show output: %s", str(err))
1102
1103     # and massage the results into our desired format
1104     for section in results:
1105       sname = section[0]
1106       if sname == "_this_host":
1107         for lst in section[1:]:
1108           if lst[0] == "disk":
1109             data["local_dev"] = lst[1]
1110           elif lst[0] == "meta-disk":
1111             data["meta_dev"] = lst[1]
1112             data["meta_index"] = lst[2]
1113           elif lst[0] == "address":
1114             data["local_addr"] = tuple(lst[1:])
1115       elif sname == "_remote_host":
1116         for lst in section[1:]:
1117           if lst[0] == "address":
1118             data["remote_addr"] = tuple(lst[1:])
1119     return data
1120
1121   def _MatchesLocal(self, info):
1122     """Test if our local config matches with an existing device.
1123
1124     The parameter should be as returned from `_GetDevInfo()`. This
1125     method tests if our local backing device is the same as the one in
1126     the info parameter, in effect testing if we look like the given
1127     device.
1128
1129     """
1130     if self._children:
1131       backend, meta = self._children
1132     else:
1133       backend = meta = None
1134
1135     if backend is not None:
1136       retval = ("local_dev" in info and info["local_dev"] == backend.dev_path)
1137     else:
1138       retval = ("local_dev" not in info)
1139
1140     if meta is not None:
1141       retval = retval and ("meta_dev" in info and
1142                            info["meta_dev"] == meta.dev_path)
1143       retval = retval and ("meta_index" in info and
1144                            info["meta_index"] == 0)
1145     else:
1146       retval = retval and ("meta_dev" not in info and
1147                            "meta_index" not in info)
1148     return retval
1149
1150   def _MatchesNet(self, info):
1151     """Test if our network config matches with an existing device.
1152
1153     The parameter should be as returned from `_GetDevInfo()`. This
1154     method tests if our network configuration is the same as the one
1155     in the info parameter, in effect testing if we look like the given
1156     device.
1157
1158     """
1159     if (((self._lhost is None and not ("local_addr" in info)) and
1160          (self._rhost is None and not ("remote_addr" in info)))):
1161       return True
1162
1163     if self._lhost is None:
1164       return False
1165
1166     if not ("local_addr" in info and
1167             "remote_addr" in info):
1168       return False
1169
1170     retval = (info["local_addr"] == (self._lhost, self._lport))
1171     retval = (retval and
1172               info["remote_addr"] == (self._rhost, self._rport))
1173     return retval
1174
1175   @classmethod
1176   def _AssembleLocal(cls, minor, backend, meta, size):
1177     """Configure the local part of a DRBD device.
1178
1179     """
1180     args = ["drbdsetup", cls._DevPath(minor), "disk",
1181             backend, meta, "0",
1182             "-e", "detach",
1183             "--create-device"]
1184     if size:
1185       args.extend(["-d", "%sm" % size])
1186     result = utils.RunCmd(args)
1187     if result.failed:
1188       _ThrowError("drbd%d: can't attach local disk: %s", minor, result.output)
1189
1190   @classmethod
1191   def _AssembleNet(cls, minor, net_info, protocol,
1192                    dual_pri=False, hmac=None, secret=None):
1193     """Configure the network part of the device.
1194
1195     """
1196     lhost, lport, rhost, rport = net_info
1197     if None in net_info:
1198       # we don't want network connection and actually want to make
1199       # sure its shutdown
1200       cls._ShutdownNet(minor)
1201       return
1202
1203     # Workaround for a race condition. When DRBD is doing its dance to
1204     # establish a connection with its peer, it also sends the
1205     # synchronization speed over the wire. In some cases setting the
1206     # sync speed only after setting up both sides can race with DRBD
1207     # connecting, hence we set it here before telling DRBD anything
1208     # about its peer.
1209     cls._SetMinorSyncSpeed(minor, constants.SYNC_SPEED)
1210
1211     args = ["drbdsetup", cls._DevPath(minor), "net",
1212             "%s:%s" % (lhost, lport), "%s:%s" % (rhost, rport), protocol,
1213             "-A", "discard-zero-changes",
1214             "-B", "consensus",
1215             "--create-device",
1216             ]
1217     if dual_pri:
1218       args.append("-m")
1219     if hmac and secret:
1220       args.extend(["-a", hmac, "-x", secret])
1221     result = utils.RunCmd(args)
1222     if result.failed:
1223       _ThrowError("drbd%d: can't setup network: %s - %s",
1224                   minor, result.fail_reason, result.output)
1225
1226     timeout = time.time() + 10
1227     ok = False
1228     while time.time() < timeout:
1229       info = cls._GetDevInfo(cls._GetShowData(minor))
1230       if not "local_addr" in info or not "remote_addr" in info:
1231         time.sleep(1)
1232         continue
1233       if (info["local_addr"] != (lhost, lport) or
1234           info["remote_addr"] != (rhost, rport)):
1235         time.sleep(1)
1236         continue
1237       ok = True
1238       break
1239     if not ok:
1240       _ThrowError("drbd%d: timeout while configuring network", minor)
1241
1242   def AddChildren(self, devices):
1243     """Add a disk to the DRBD device.
1244
1245     """
1246     if self.minor is None:
1247       _ThrowError("drbd%d: can't attach to dbrd8 during AddChildren",
1248                   self._aminor)
1249     if len(devices) != 2:
1250       _ThrowError("drbd%d: need two devices for AddChildren", self.minor)
1251     info = self._GetDevInfo(self._GetShowData(self.minor))
1252     if "local_dev" in info:
1253       _ThrowError("drbd%d: already attached to a local disk", self.minor)
1254     backend, meta = devices
1255     if backend.dev_path is None or meta.dev_path is None:
1256       _ThrowError("drbd%d: children not ready during AddChildren", self.minor)
1257     backend.Open()
1258     meta.Open()
1259     self._CheckMetaSize(meta.dev_path)
1260     self._InitMeta(self._FindUnusedMinor(), meta.dev_path)
1261
1262     self._AssembleLocal(self.minor, backend.dev_path, meta.dev_path, self.size)
1263     self._children = devices
1264
1265   def RemoveChildren(self, devices):
1266     """Detach the drbd device from local storage.
1267
1268     """
1269     if self.minor is None:
1270       _ThrowError("drbd%d: can't attach to drbd8 during RemoveChildren",
1271                   self._aminor)
1272     # early return if we don't actually have backing storage
1273     info = self._GetDevInfo(self._GetShowData(self.minor))
1274     if "local_dev" not in info:
1275       return
1276     if len(self._children) != 2:
1277       _ThrowError("drbd%d: we don't have two children: %s", self.minor,
1278                   self._children)
1279     if self._children.count(None) == 2: # we don't actually have children :)
1280       logging.warning("drbd%d: requested detach while detached", self.minor)
1281       return
1282     if len(devices) != 2:
1283       _ThrowError("drbd%d: we need two children in RemoveChildren", self.minor)
1284     for child, dev in zip(self._children, devices):
1285       if dev != child.dev_path:
1286         _ThrowError("drbd%d: mismatch in local storage (%s != %s) in"
1287                     " RemoveChildren", self.minor, dev, child.dev_path)
1288
1289     self._ShutdownLocal(self.minor)
1290     self._children = []
1291
1292   @classmethod
1293   def _SetMinorSyncSpeed(cls, minor, kbytes):
1294     """Set the speed of the DRBD syncer.
1295
1296     This is the low-level implementation.
1297
1298     @type minor: int
1299     @param minor: the drbd minor whose settings we change
1300     @type kbytes: int
1301     @param kbytes: the speed in kbytes/second
1302     @rtype: boolean
1303     @return: the success of the operation
1304
1305     """
1306     result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "syncer",
1307                            "-r", "%d" % kbytes, "--create-device"])
1308     if result.failed:
1309       logging.error("Can't change syncer rate: %s - %s",
1310                     result.fail_reason, result.output)
1311     return not result.failed
1312
1313   def SetSyncSpeed(self, kbytes):
1314     """Set the speed of the DRBD syncer.
1315
1316     @type kbytes: int
1317     @param kbytes: the speed in kbytes/second
1318     @rtype: boolean
1319     @return: the success of the operation
1320
1321     """
1322     if self.minor is None:
1323       logging.info("Not attached during SetSyncSpeed")
1324       return False
1325     children_result = super(DRBD8, self).SetSyncSpeed(kbytes)
1326     return self._SetMinorSyncSpeed(self.minor, kbytes) and children_result
1327
1328   def GetProcStatus(self):
1329     """Return device data from /proc.
1330
1331     """
1332     if self.minor is None:
1333       _ThrowError("drbd%d: GetStats() called while not attached", self._aminor)
1334     proc_info = self._MassageProcData(self._GetProcData())
1335     if self.minor not in proc_info:
1336       _ThrowError("drbd%d: can't find myself in /proc", self.minor)
1337     return DRBD8Status(proc_info[self.minor])
1338
1339   def GetSyncStatus(self):
1340     """Returns the sync status of the device.
1341
1342
1343     If sync_percent is None, it means all is ok
1344     If estimated_time is None, it means we can't estimate
1345     the time needed, otherwise it's the time left in seconds.
1346
1347
1348     We set the is_degraded parameter to True on two conditions:
1349     network not connected or local disk missing.
1350
1351     We compute the ldisk parameter based on whether we have a local
1352     disk or not.
1353
1354     @rtype: objects.BlockDevStatus
1355
1356     """
1357     if self.minor is None and not self.Attach():
1358       _ThrowError("drbd%d: can't Attach() in GetSyncStatus", self._aminor)
1359
1360     stats = self.GetProcStatus()
1361     is_degraded = not stats.is_connected or not stats.is_disk_uptodate
1362
1363     if stats.is_disk_uptodate:
1364       ldisk_status = constants.LDS_OKAY
1365     elif stats.is_diskless:
1366       ldisk_status = constants.LDS_FAULTY
1367     else:
1368       ldisk_status = constants.LDS_UNKNOWN
1369
1370     return objects.BlockDevStatus(dev_path=self.dev_path,
1371                                   major=self.major,
1372                                   minor=self.minor,
1373                                   sync_percent=stats.sync_percent,
1374                                   estimated_time=stats.est_time,
1375                                   is_degraded=is_degraded,
1376                                   ldisk_status=ldisk_status)
1377
1378   def Open(self, force=False):
1379     """Make the local state primary.
1380
1381     If the 'force' parameter is given, the '-o' option is passed to
1382     drbdsetup. Since this is a potentially dangerous operation, the
1383     force flag should be only given after creation, when it actually
1384     is mandatory.
1385
1386     """
1387     if self.minor is None and not self.Attach():
1388       logging.error("DRBD cannot attach to a device during open")
1389       return False
1390     cmd = ["drbdsetup", self.dev_path, "primary"]
1391     if force:
1392       cmd.append("-o")
1393     result = utils.RunCmd(cmd)
1394     if result.failed:
1395       _ThrowError("drbd%d: can't make drbd device primary: %s", self.minor,
1396                   result.output)
1397
1398   def Close(self):
1399     """Make the local state secondary.
1400
1401     This will, of course, fail if the device is in use.
1402
1403     """
1404     if self.minor is None and not self.Attach():
1405       _ThrowError("drbd%d: can't Attach() in Close()", self._aminor)
1406     result = utils.RunCmd(["drbdsetup", self.dev_path, "secondary"])
1407     if result.failed:
1408       _ThrowError("drbd%d: can't switch drbd device to secondary: %s",
1409                   self.minor, result.output)
1410
1411   def DisconnectNet(self):
1412     """Removes network configuration.
1413
1414     This method shutdowns the network side of the device.
1415
1416     The method will wait up to a hardcoded timeout for the device to
1417     go into standalone after the 'disconnect' command before
1418     re-configuring it, as sometimes it takes a while for the
1419     disconnect to actually propagate and thus we might issue a 'net'
1420     command while the device is still connected. If the device will
1421     still be attached to the network and we time out, we raise an
1422     exception.
1423
1424     """
1425     if self.minor is None:
1426       _ThrowError("drbd%d: disk not attached in re-attach net", self._aminor)
1427
1428     if None in (self._lhost, self._lport, self._rhost, self._rport):
1429       _ThrowError("drbd%d: DRBD disk missing network info in"
1430                   " DisconnectNet()", self.minor)
1431
1432     ever_disconnected = _IgnoreError(self._ShutdownNet, self.minor)
1433     timeout_limit = time.time() + self._NET_RECONFIG_TIMEOUT
1434     sleep_time = 0.100 # we start the retry time at 100 milliseconds
1435     while time.time() < timeout_limit:
1436       status = self.GetProcStatus()
1437       if status.is_standalone:
1438         break
1439       # retry the disconnect, it seems possible that due to a
1440       # well-time disconnect on the peer, my disconnect command might
1441       # be ignored and forgotten
1442       ever_disconnected = _IgnoreError(self._ShutdownNet, self.minor) or \
1443                           ever_disconnected
1444       time.sleep(sleep_time)
1445       sleep_time = min(2, sleep_time * 1.5)
1446
1447     if not status.is_standalone:
1448       if ever_disconnected:
1449         msg = ("drbd%d: device did not react to the"
1450                " 'disconnect' command in a timely manner")
1451       else:
1452         msg = "drbd%d: can't shutdown network, even after multiple retries"
1453       _ThrowError(msg, self.minor)
1454
1455     reconfig_time = time.time() - timeout_limit + self._NET_RECONFIG_TIMEOUT
1456     if reconfig_time > 15: # hardcoded alert limit
1457       logging.info("drbd%d: DisconnectNet: detach took %.3f seconds",
1458                    self.minor, reconfig_time)
1459
1460   def AttachNet(self, multimaster):
1461     """Reconnects the network.
1462
1463     This method connects the network side of the device with a
1464     specified multi-master flag. The device needs to be 'Standalone'
1465     but have valid network configuration data.
1466
1467     Args:
1468       - multimaster: init the network in dual-primary mode
1469
1470     """
1471     if self.minor is None:
1472       _ThrowError("drbd%d: device not attached in AttachNet", self._aminor)
1473
1474     if None in (self._lhost, self._lport, self._rhost, self._rport):
1475       _ThrowError("drbd%d: missing network info in AttachNet()", self.minor)
1476
1477     status = self.GetProcStatus()
1478
1479     if not status.is_standalone:
1480       _ThrowError("drbd%d: device is not standalone in AttachNet", self.minor)
1481
1482     self._AssembleNet(self.minor,
1483                       (self._lhost, self._lport, self._rhost, self._rport),
1484                       constants.DRBD_NET_PROTOCOL, dual_pri=multimaster,
1485                       hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1486
1487   def Attach(self):
1488     """Check if our minor is configured.
1489
1490     This doesn't do any device configurations - it only checks if the
1491     minor is in a state different from Unconfigured.
1492
1493     Note that this function will not change the state of the system in
1494     any way (except in case of side-effects caused by reading from
1495     /proc).
1496
1497     """
1498     used_devs = self.GetUsedDevs()
1499     if self._aminor in used_devs:
1500       minor = self._aminor
1501     else:
1502       minor = None
1503
1504     self._SetFromMinor(minor)
1505     return minor is not None
1506
1507   def Assemble(self):
1508     """Assemble the drbd.
1509
1510     Method:
1511       - if we have a configured device, we try to ensure that it matches
1512         our config
1513       - if not, we create it from zero
1514
1515     """
1516     super(DRBD8, self).Assemble()
1517
1518     self.Attach()
1519     if self.minor is None:
1520       # local device completely unconfigured
1521       self._FastAssemble()
1522     else:
1523       # we have to recheck the local and network status and try to fix
1524       # the device
1525       self._SlowAssemble()
1526
1527   def _SlowAssemble(self):
1528     """Assembles the DRBD device from a (partially) configured device.
1529
1530     In case of partially attached (local device matches but no network
1531     setup), we perform the network attach. If successful, we re-test
1532     the attach if can return success.
1533
1534     """
1535     net_data = (self._lhost, self._lport, self._rhost, self._rport)
1536     for minor in (self._aminor,):
1537       info = self._GetDevInfo(self._GetShowData(minor))
1538       match_l = self._MatchesLocal(info)
1539       match_r = self._MatchesNet(info)
1540
1541       if match_l and match_r:
1542         # everything matches
1543         break
1544
1545       if match_l and not match_r and "local_addr" not in info:
1546         # disk matches, but not attached to network, attach and recheck
1547         self._AssembleNet(minor, net_data, constants.DRBD_NET_PROTOCOL,
1548                           hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1549         if self._MatchesNet(self._GetDevInfo(self._GetShowData(minor))):
1550           break
1551         else:
1552           _ThrowError("drbd%d: network attach successful, but 'drbdsetup"
1553                       " show' disagrees", minor)
1554
1555       if match_r and "local_dev" not in info:
1556         # no local disk, but network attached and it matches
1557         self._AssembleLocal(minor, self._children[0].dev_path,
1558                             self._children[1].dev_path, self.size)
1559         if self._MatchesNet(self._GetDevInfo(self._GetShowData(minor))):
1560           break
1561         else:
1562           _ThrowError("drbd%d: disk attach successful, but 'drbdsetup"
1563                       " show' disagrees", minor)
1564
1565       # this case must be considered only if we actually have local
1566       # storage, i.e. not in diskless mode, because all diskless
1567       # devices are equal from the point of view of local
1568       # configuration
1569       if (match_l and "local_dev" in info and
1570           not match_r and "local_addr" in info):
1571         # strange case - the device network part points to somewhere
1572         # else, even though its local storage is ours; as we own the
1573         # drbd space, we try to disconnect from the remote peer and
1574         # reconnect to our correct one
1575         try:
1576           self._ShutdownNet(minor)
1577         except errors.BlockDeviceError, err:
1578           _ThrowError("drbd%d: device has correct local storage, wrong"
1579                       " remote peer and is unable to disconnect in order"
1580                       " to attach to the correct peer: %s", minor, str(err))
1581         # note: _AssembleNet also handles the case when we don't want
1582         # local storage (i.e. one or more of the _[lr](host|port) is
1583         # None)
1584         self._AssembleNet(minor, net_data, constants.DRBD_NET_PROTOCOL,
1585                           hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1586         if self._MatchesNet(self._GetDevInfo(self._GetShowData(minor))):
1587           break
1588         else:
1589           _ThrowError("drbd%d: network attach successful, but 'drbdsetup"
1590                       " show' disagrees", minor)
1591
1592     else:
1593       minor = None
1594
1595     self._SetFromMinor(minor)
1596     if minor is None:
1597       _ThrowError("drbd%d: cannot activate, unknown or unhandled reason",
1598                   self._aminor)
1599
1600   def _FastAssemble(self):
1601     """Assemble the drbd device from zero.
1602
1603     This is run when in Assemble we detect our minor is unused.
1604
1605     """
1606     minor = self._aminor
1607     if self._children and self._children[0] and self._children[1]:
1608       self._AssembleLocal(minor, self._children[0].dev_path,
1609                           self._children[1].dev_path, self.size)
1610     if self._lhost and self._lport and self._rhost and self._rport:
1611       self._AssembleNet(minor,
1612                         (self._lhost, self._lport, self._rhost, self._rport),
1613                         constants.DRBD_NET_PROTOCOL,
1614                         hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1615     self._SetFromMinor(minor)
1616
1617   @classmethod
1618   def _ShutdownLocal(cls, minor):
1619     """Detach from the local device.
1620
1621     I/Os will continue to be served from the remote device. If we
1622     don't have a remote device, this operation will fail.
1623
1624     """
1625     result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "detach"])
1626     if result.failed:
1627       _ThrowError("drbd%d: can't detach local disk: %s", minor, result.output)
1628
1629   @classmethod
1630   def _ShutdownNet(cls, minor):
1631     """Disconnect from the remote peer.
1632
1633     This fails if we don't have a local device.
1634
1635     """
1636     result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "disconnect"])
1637     if result.failed:
1638       _ThrowError("drbd%d: can't shutdown network: %s", minor, result.output)
1639
1640   @classmethod
1641   def _ShutdownAll(cls, minor):
1642     """Deactivate the device.
1643
1644     This will, of course, fail if the device is in use.
1645
1646     """
1647     result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "down"])
1648     if result.failed:
1649       _ThrowError("drbd%d: can't shutdown drbd device: %s",
1650                   minor, result.output)
1651
1652   def Shutdown(self):
1653     """Shutdown the DRBD device.
1654
1655     """
1656     if self.minor is None and not self.Attach():
1657       logging.info("drbd%d: not attached during Shutdown()", self._aminor)
1658       return
1659     minor = self.minor
1660     self.minor = None
1661     self.dev_path = None
1662     self._ShutdownAll(minor)
1663
1664   def Remove(self):
1665     """Stub remove for DRBD devices.
1666
1667     """
1668     self.Shutdown()
1669
1670   @classmethod
1671   def Create(cls, unique_id, children, size):
1672     """Create a new DRBD8 device.
1673
1674     Since DRBD devices are not created per se, just assembled, this
1675     function only initializes the metadata.
1676
1677     """
1678     if len(children) != 2:
1679       raise errors.ProgrammerError("Invalid setup for the drbd device")
1680     # check that the minor is unused
1681     aminor = unique_id[4]
1682     proc_info = cls._MassageProcData(cls._GetProcData())
1683     if aminor in proc_info:
1684       status = DRBD8Status(proc_info[aminor])
1685       in_use = status.is_in_use
1686     else:
1687       in_use = False
1688     if in_use:
1689       _ThrowError("drbd%d: minor is already in use at Create() time", aminor)
1690     meta = children[1]
1691     meta.Assemble()
1692     if not meta.Attach():
1693       _ThrowError("drbd%d: can't attach to meta device '%s'",
1694                   aminor, meta)
1695     cls._CheckMetaSize(meta.dev_path)
1696     cls._InitMeta(aminor, meta.dev_path)
1697     return cls(unique_id, children, size)
1698
1699   def Grow(self, amount):
1700     """Resize the DRBD device and its backing storage.
1701
1702     """
1703     if self.minor is None:
1704       _ThrowError("drbd%d: Grow called while not attached", self._aminor)
1705     if len(self._children) != 2 or None in self._children:
1706       _ThrowError("drbd%d: cannot grow diskless device", self.minor)
1707     self._children[0].Grow(amount)
1708     result = utils.RunCmd(["drbdsetup", self.dev_path, "resize", "-s",
1709                            "%dm" % (self.size + amount)])
1710     if result.failed:
1711       _ThrowError("drbd%d: resize failed: %s", self.minor, result.output)
1712
1713
1714 class FileStorage(BlockDev):
1715   """File device.
1716
1717   This class represents the a file storage backend device.
1718
1719   The unique_id for the file device is a (file_driver, file_path) tuple.
1720
1721   """
1722   def __init__(self, unique_id, children, size):
1723     """Initalizes a file device backend.
1724
1725     """
1726     if children:
1727       raise errors.BlockDeviceError("Invalid setup for file device")
1728     super(FileStorage, self).__init__(unique_id, children, size)
1729     if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
1730       raise ValueError("Invalid configuration data %s" % str(unique_id))
1731     self.driver = unique_id[0]
1732     self.dev_path = unique_id[1]
1733     self.Attach()
1734
1735   def Assemble(self):
1736     """Assemble the device.
1737
1738     Checks whether the file device exists, raises BlockDeviceError otherwise.
1739
1740     """
1741     if not os.path.exists(self.dev_path):
1742       _ThrowError("File device '%s' does not exist" % self.dev_path)
1743
1744   def Shutdown(self):
1745     """Shutdown the device.
1746
1747     This is a no-op for the file type, as we don't deactivate
1748     the file on shutdown.
1749
1750     """
1751     pass
1752
1753   def Open(self, force=False):
1754     """Make the device ready for I/O.
1755
1756     This is a no-op for the file type.
1757
1758     """
1759     pass
1760
1761   def Close(self):
1762     """Notifies that the device will no longer be used for I/O.
1763
1764     This is a no-op for the file type.
1765
1766     """
1767     pass
1768
1769   def Remove(self):
1770     """Remove the file backing the block device.
1771
1772     @rtype: boolean
1773     @return: True if the removal was successful
1774
1775     """
1776     try:
1777       os.remove(self.dev_path)
1778     except OSError, err:
1779       if err.errno != errno.ENOENT:
1780         _ThrowError("Can't remove file '%s': %s", self.dev_path, err)
1781
1782   def Attach(self):
1783     """Attach to an existing file.
1784
1785     Check if this file already exists.
1786
1787     @rtype: boolean
1788     @return: True if file exists
1789
1790     """
1791     self.attached = os.path.exists(self.dev_path)
1792     return self.attached
1793
1794   def GetActualSize(self):
1795     """Return the actual disk size.
1796
1797     @note: the device needs to be active when this is called
1798
1799     """
1800     assert self.attached, "BlockDevice not attached in GetActualSize()"
1801     try:
1802       st = os.stat(self.dev_path)
1803       return st.st_size
1804     except OSError, err:
1805       _ThrowError("Can't stat %s: %s", self.dev_path, err)
1806
1807   @classmethod
1808   def Create(cls, unique_id, children, size):
1809     """Create a new file.
1810
1811     @param size: the size of file in MiB
1812
1813     @rtype: L{bdev.FileStorage}
1814     @return: an instance of FileStorage
1815
1816     """
1817     if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
1818       raise ValueError("Invalid configuration data %s" % str(unique_id))
1819     dev_path = unique_id[1]
1820     if os.path.exists(dev_path):
1821       _ThrowError("File already existing: %s", dev_path)
1822     try:
1823       f = open(dev_path, 'w')
1824       f.truncate(size * 1024 * 1024)
1825       f.close()
1826     except IOError, err:
1827       _ThrowError("Error in file creation: %", str(err))
1828
1829     return FileStorage(unique_id, children, size)
1830
1831
1832 DEV_MAP = {
1833   constants.LD_LV: LogicalVolume,
1834   constants.LD_DRBD8: DRBD8,
1835   constants.LD_FILE: FileStorage,
1836   }
1837
1838
1839 def FindDevice(dev_type, unique_id, children, size):
1840   """Search for an existing, assembled device.
1841
1842   This will succeed only if the device exists and is assembled, but it
1843   does not do any actions in order to activate the device.
1844
1845   """
1846   if dev_type not in DEV_MAP:
1847     raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
1848   device = DEV_MAP[dev_type](unique_id, children, size)
1849   if not device.attached:
1850     return None
1851   return device
1852
1853
1854 def Assemble(dev_type, unique_id, children, size):
1855   """Try to attach or assemble an existing device.
1856
1857   This will attach to assemble the device, as needed, to bring it
1858   fully up. It must be safe to run on already-assembled devices.
1859
1860   """
1861   if dev_type not in DEV_MAP:
1862     raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
1863   device = DEV_MAP[dev_type](unique_id, children, size)
1864   device.Assemble()
1865   return device
1866
1867
1868 def Create(dev_type, unique_id, children, size):
1869   """Create a device.
1870
1871   """
1872   if dev_type not in DEV_MAP:
1873     raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
1874   device = DEV_MAP[dev_type].Create(unique_id, children, size)
1875   return device