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