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