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