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