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