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