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