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