Small improvements for cluster verify
[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     # Zero the metadata first, in order to make sure drbdmeta doesn't
1135     # try to auto-detect existing filesystems or similar (see
1136     # http://code.google.com/p/ganeti/issues/detail?id=182); we only
1137     # care about the first 128MB of data in the device, even though it
1138     # can be bigger
1139     result = utils.RunCmd([constants.DD_CMD,
1140                            "if=/dev/zero", "of=%s" % dev_path,
1141                            "bs=1048576", "count=128", "oflag=direct"])
1142     if result.failed:
1143       _ThrowError("Can't wipe the meta device: %s", result.output)
1144
1145     result = utils.RunCmd(["drbdmeta", "--force", cls._DevPath(minor),
1146                            "v08", dev_path, "0", "create-md"])
1147     if result.failed:
1148       _ThrowError("Can't initialize meta device: %s", result.output)
1149
1150   @classmethod
1151   def _FindUnusedMinor(cls):
1152     """Find an unused DRBD device.
1153
1154     This is specific to 8.x as the minors are allocated dynamically,
1155     so non-existing numbers up to a max minor count are actually free.
1156
1157     """
1158     data = cls._GetProcData()
1159
1160     highest = None
1161     for line in data:
1162       match = cls._UNUSED_LINE_RE.match(line)
1163       if match:
1164         return int(match.group(1))
1165       match = cls._VALID_LINE_RE.match(line)
1166       if match:
1167         minor = int(match.group(1))
1168         highest = max(highest, minor)
1169     if highest is None: # there are no minors in use at all
1170       return 0
1171     if highest >= cls._MAX_MINORS:
1172       logging.error("Error: no free drbd minors!")
1173       raise errors.BlockDeviceError("Can't find a free DRBD minor")
1174     return highest + 1
1175
1176   @classmethod
1177   def _GetShowParser(cls):
1178     """Return a parser for `drbd show` output.
1179
1180     This will either create or return an already-create parser for the
1181     output of the command `drbd show`.
1182
1183     """
1184     if cls._PARSE_SHOW is not None:
1185       return cls._PARSE_SHOW
1186
1187     # pyparsing setup
1188     lbrace = pyp.Literal("{").suppress()
1189     rbrace = pyp.Literal("}").suppress()
1190     lbracket = pyp.Literal("[").suppress()
1191     rbracket = pyp.Literal("]").suppress()
1192     semi = pyp.Literal(";").suppress()
1193     colon = pyp.Literal(":").suppress()
1194     # this also converts the value to an int
1195     number = pyp.Word(pyp.nums).setParseAction(lambda s, l, t: int(t[0]))
1196
1197     comment = pyp.Literal ("#") + pyp.Optional(pyp.restOfLine)
1198     defa = pyp.Literal("_is_default").suppress()
1199     dbl_quote = pyp.Literal('"').suppress()
1200
1201     keyword = pyp.Word(pyp.alphanums + '-')
1202
1203     # value types
1204     value = pyp.Word(pyp.alphanums + '_-/.:')
1205     quoted = dbl_quote + pyp.CharsNotIn('"') + dbl_quote
1206     ipv4_addr = (pyp.Optional(pyp.Literal("ipv4")).suppress() +
1207                  pyp.Word(pyp.nums + ".") + colon + number)
1208     ipv6_addr = (pyp.Optional(pyp.Literal("ipv6")).suppress() +
1209                  pyp.Optional(lbracket) + pyp.Word(pyp.hexnums + ":") +
1210                  pyp.Optional(rbracket) + colon + number)
1211     # meta device, extended syntax
1212     meta_value = ((value ^ quoted) + lbracket + number + rbracket)
1213     # device name, extended syntax
1214     device_value = pyp.Literal("minor").suppress() + number
1215
1216     # a statement
1217     stmt = (~rbrace + keyword + ~lbrace +
1218             pyp.Optional(ipv4_addr ^ ipv6_addr ^ value ^ quoted ^ meta_value ^
1219                          device_value) +
1220             pyp.Optional(defa) + semi +
1221             pyp.Optional(pyp.restOfLine).suppress())
1222
1223     # an entire section
1224     section_name = pyp.Word(pyp.alphas + "_")
1225     section = section_name + lbrace + pyp.ZeroOrMore(pyp.Group(stmt)) + rbrace
1226
1227     bnf = pyp.ZeroOrMore(pyp.Group(section ^ stmt))
1228     bnf.ignore(comment)
1229
1230     cls._PARSE_SHOW = bnf
1231
1232     return bnf
1233
1234   @classmethod
1235   def _GetShowData(cls, minor):
1236     """Return the `drbdsetup show` data for a minor.
1237
1238     """
1239     result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "show"])
1240     if result.failed:
1241       logging.error("Can't display the drbd config: %s - %s",
1242                     result.fail_reason, result.output)
1243       return None
1244     return result.stdout
1245
1246   @classmethod
1247   def _GetDevInfo(cls, out):
1248     """Parse details about a given DRBD minor.
1249
1250     This return, if available, the local backing device (as a path)
1251     and the local and remote (ip, port) information from a string
1252     containing the output of the `drbdsetup show` command as returned
1253     by _GetShowData.
1254
1255     """
1256     data = {}
1257     if not out:
1258       return data
1259
1260     bnf = cls._GetShowParser()
1261     # run pyparse
1262
1263     try:
1264       results = bnf.parseString(out)
1265     except pyp.ParseException, err:
1266       _ThrowError("Can't parse drbdsetup show output: %s", str(err))
1267
1268     # and massage the results into our desired format
1269     for section in results:
1270       sname = section[0]
1271       if sname == "_this_host":
1272         for lst in section[1:]:
1273           if lst[0] == "disk":
1274             data["local_dev"] = lst[1]
1275           elif lst[0] == "meta-disk":
1276             data["meta_dev"] = lst[1]
1277             data["meta_index"] = lst[2]
1278           elif lst[0] == "address":
1279             data["local_addr"] = tuple(lst[1:])
1280       elif sname == "_remote_host":
1281         for lst in section[1:]:
1282           if lst[0] == "address":
1283             data["remote_addr"] = tuple(lst[1:])
1284     return data
1285
1286   def _MatchesLocal(self, info):
1287     """Test if our local config matches with an existing device.
1288
1289     The parameter should be as returned from `_GetDevInfo()`. This
1290     method tests if our local backing device is the same as the one in
1291     the info parameter, in effect testing if we look like the given
1292     device.
1293
1294     """
1295     if self._children:
1296       backend, meta = self._children
1297     else:
1298       backend = meta = None
1299
1300     if backend is not None:
1301       retval = ("local_dev" in info and info["local_dev"] == backend.dev_path)
1302     else:
1303       retval = ("local_dev" not in info)
1304
1305     if meta is not None:
1306       retval = retval and ("meta_dev" in info and
1307                            info["meta_dev"] == meta.dev_path)
1308       retval = retval and ("meta_index" in info and
1309                            info["meta_index"] == 0)
1310     else:
1311       retval = retval and ("meta_dev" not in info and
1312                            "meta_index" not in info)
1313     return retval
1314
1315   def _MatchesNet(self, info):
1316     """Test if our network config matches with an existing device.
1317
1318     The parameter should be as returned from `_GetDevInfo()`. This
1319     method tests if our network configuration is the same as the one
1320     in the info parameter, in effect testing if we look like the given
1321     device.
1322
1323     """
1324     if (((self._lhost is None and not ("local_addr" in info)) and
1325          (self._rhost is None and not ("remote_addr" in info)))):
1326       return True
1327
1328     if self._lhost is None:
1329       return False
1330
1331     if not ("local_addr" in info and
1332             "remote_addr" in info):
1333       return False
1334
1335     retval = (info["local_addr"] == (self._lhost, self._lport))
1336     retval = (retval and
1337               info["remote_addr"] == (self._rhost, self._rport))
1338     return retval
1339
1340   @classmethod
1341   def _AssembleLocal(cls, minor, backend, meta, size):
1342     """Configure the local part of a DRBD device.
1343
1344     """
1345     args = ["drbdsetup", cls._DevPath(minor), "disk",
1346             backend, meta, "0",
1347             "-e", "detach",
1348             "--create-device"]
1349     if size:
1350       args.extend(["-d", "%sm" % size])
1351     if not constants.DRBD_BARRIERS: # disable barriers, if configured so
1352       version = cls._GetVersion(cls._GetProcData())
1353       # various DRBD versions support different disk barrier options;
1354       # what we aim here is to revert back to the 'drain' method of
1355       # disk flushes and to disable metadata barriers, in effect going
1356       # back to pre-8.0.7 behaviour
1357       vmaj = version["k_major"]
1358       vmin = version["k_minor"]
1359       vrel = version["k_point"]
1360       assert vmaj == 8
1361       if vmin == 0: # 8.0.x
1362         if vrel >= 12:
1363           args.extend(["-i", "-m"])
1364       elif vmin == 2: # 8.2.x
1365         if vrel >= 7:
1366           args.extend(["-i", "-m"])
1367       elif vmaj >= 3: # 8.3.x or newer
1368         args.extend(["-i", "-a", "m"])
1369     result = utils.RunCmd(args)
1370     if result.failed:
1371       _ThrowError("drbd%d: can't attach local disk: %s", minor, result.output)
1372
1373   @classmethod
1374   def _AssembleNet(cls, minor, net_info, protocol,
1375                    dual_pri=False, hmac=None, secret=None):
1376     """Configure the network part of the device.
1377
1378     """
1379     lhost, lport, rhost, rport = net_info
1380     if None in net_info:
1381       # we don't want network connection and actually want to make
1382       # sure its shutdown
1383       cls._ShutdownNet(minor)
1384       return
1385
1386     # Workaround for a race condition. When DRBD is doing its dance to
1387     # establish a connection with its peer, it also sends the
1388     # synchronization speed over the wire. In some cases setting the
1389     # sync speed only after setting up both sides can race with DRBD
1390     # connecting, hence we set it here before telling DRBD anything
1391     # about its peer.
1392     cls._SetMinorSyncSpeed(minor, constants.SYNC_SPEED)
1393
1394     if netutils.IP6Address.IsValid(lhost):
1395       if not netutils.IP6Address.IsValid(rhost):
1396         _ThrowError("drbd%d: can't connect ip %s to ip %s" %
1397                     (minor, lhost, rhost))
1398       family = "ipv6"
1399     elif netutils.IP4Address.IsValid(lhost):
1400       if not netutils.IP4Address.IsValid(rhost):
1401         _ThrowError("drbd%d: can't connect ip %s to ip %s" %
1402                     (minor, lhost, rhost))
1403       family = "ipv4"
1404     else:
1405       _ThrowError("drbd%d: Invalid ip %s" % (minor, lhost))
1406
1407     args = ["drbdsetup", cls._DevPath(minor), "net",
1408             "%s:%s:%s" % (family, lhost, lport),
1409             "%s:%s:%s" % (family, rhost, rport), protocol,
1410             "-A", "discard-zero-changes",
1411             "-B", "consensus",
1412             "--create-device",
1413             ]
1414     if dual_pri:
1415       args.append("-m")
1416     if hmac and secret:
1417       args.extend(["-a", hmac, "-x", secret])
1418     result = utils.RunCmd(args)
1419     if result.failed:
1420       _ThrowError("drbd%d: can't setup network: %s - %s",
1421                   minor, result.fail_reason, result.output)
1422
1423     def _CheckNetworkConfig():
1424       info = cls._GetDevInfo(cls._GetShowData(minor))
1425       if not "local_addr" in info or not "remote_addr" in info:
1426         raise utils.RetryAgain()
1427
1428       if (info["local_addr"] != (lhost, lport) or
1429           info["remote_addr"] != (rhost, rport)):
1430         raise utils.RetryAgain()
1431
1432     try:
1433       utils.Retry(_CheckNetworkConfig, 1.0, 10.0)
1434     except utils.RetryTimeout:
1435       _ThrowError("drbd%d: timeout while configuring network", minor)
1436
1437   def AddChildren(self, devices):
1438     """Add a disk to the DRBD device.
1439
1440     """
1441     if self.minor is None:
1442       _ThrowError("drbd%d: can't attach to dbrd8 during AddChildren",
1443                   self._aminor)
1444     if len(devices) != 2:
1445       _ThrowError("drbd%d: need two devices for AddChildren", self.minor)
1446     info = self._GetDevInfo(self._GetShowData(self.minor))
1447     if "local_dev" in info:
1448       _ThrowError("drbd%d: already attached to a local disk", self.minor)
1449     backend, meta = devices
1450     if backend.dev_path is None or meta.dev_path is None:
1451       _ThrowError("drbd%d: children not ready during AddChildren", self.minor)
1452     backend.Open()
1453     meta.Open()
1454     self._CheckMetaSize(meta.dev_path)
1455     self._InitMeta(self._FindUnusedMinor(), meta.dev_path)
1456
1457     self._AssembleLocal(self.minor, backend.dev_path, meta.dev_path, self.size)
1458     self._children = devices
1459
1460   def RemoveChildren(self, devices):
1461     """Detach the drbd device from local storage.
1462
1463     """
1464     if self.minor is None:
1465       _ThrowError("drbd%d: can't attach to drbd8 during RemoveChildren",
1466                   self._aminor)
1467     # early return if we don't actually have backing storage
1468     info = self._GetDevInfo(self._GetShowData(self.minor))
1469     if "local_dev" not in info:
1470       return
1471     if len(self._children) != 2:
1472       _ThrowError("drbd%d: we don't have two children: %s", self.minor,
1473                   self._children)
1474     if self._children.count(None) == 2: # we don't actually have children :)
1475       logging.warning("drbd%d: requested detach while detached", self.minor)
1476       return
1477     if len(devices) != 2:
1478       _ThrowError("drbd%d: we need two children in RemoveChildren", self.minor)
1479     for child, dev in zip(self._children, devices):
1480       if dev != child.dev_path:
1481         _ThrowError("drbd%d: mismatch in local storage (%s != %s) in"
1482                     " RemoveChildren", self.minor, dev, child.dev_path)
1483
1484     self._ShutdownLocal(self.minor)
1485     self._children = []
1486
1487   @classmethod
1488   def _SetMinorSyncSpeed(cls, minor, kbytes):
1489     """Set the speed of the DRBD syncer.
1490
1491     This is the low-level implementation.
1492
1493     @type minor: int
1494     @param minor: the drbd minor whose settings we change
1495     @type kbytes: int
1496     @param kbytes: the speed in kbytes/second
1497     @rtype: boolean
1498     @return: the success of the operation
1499
1500     """
1501     result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "syncer",
1502                            "-r", "%d" % kbytes, "--create-device"])
1503     if result.failed:
1504       logging.error("Can't change syncer rate: %s - %s",
1505                     result.fail_reason, result.output)
1506     return not result.failed
1507
1508   def SetSyncSpeed(self, kbytes):
1509     """Set the speed of the DRBD syncer.
1510
1511     @type kbytes: int
1512     @param kbytes: the speed in kbytes/second
1513     @rtype: boolean
1514     @return: the success of the operation
1515
1516     """
1517     if self.minor is None:
1518       logging.info("Not attached during SetSyncSpeed")
1519       return False
1520     children_result = super(DRBD8, self).SetSyncSpeed(kbytes)
1521     return self._SetMinorSyncSpeed(self.minor, kbytes) and children_result
1522
1523   def PauseResumeSync(self, pause):
1524     """Pauses or resumes the sync of a DRBD device.
1525
1526     @param pause: Wether to pause or resume
1527     @return: the success of the operation
1528
1529     """
1530     if self.minor is None:
1531       logging.info("Not attached during PauseSync")
1532       return False
1533
1534     children_result = super(DRBD8, self).PauseResumeSync(pause)
1535
1536     if pause:
1537       cmd = "pause-sync"
1538     else:
1539       cmd = "resume-sync"
1540
1541     result = utils.RunCmd(["drbdsetup", self.dev_path, cmd])
1542     if result.failed:
1543       logging.error("Can't %s: %s - %s", cmd,
1544                     result.fail_reason, result.output)
1545     return not result.failed and children_result
1546
1547   def GetProcStatus(self):
1548     """Return device data from /proc.
1549
1550     """
1551     if self.minor is None:
1552       _ThrowError("drbd%d: GetStats() called while not attached", self._aminor)
1553     proc_info = self._MassageProcData(self._GetProcData())
1554     if self.minor not in proc_info:
1555       _ThrowError("drbd%d: can't find myself in /proc", self.minor)
1556     return DRBD8Status(proc_info[self.minor])
1557
1558   def GetSyncStatus(self):
1559     """Returns the sync status of the device.
1560
1561
1562     If sync_percent is None, it means all is ok
1563     If estimated_time is None, it means we can't estimate
1564     the time needed, otherwise it's the time left in seconds.
1565
1566
1567     We set the is_degraded parameter to True on two conditions:
1568     network not connected or local disk missing.
1569
1570     We compute the ldisk parameter based on whether we have a local
1571     disk or not.
1572
1573     @rtype: objects.BlockDevStatus
1574
1575     """
1576     if self.minor is None and not self.Attach():
1577       _ThrowError("drbd%d: can't Attach() in GetSyncStatus", self._aminor)
1578
1579     stats = self.GetProcStatus()
1580     is_degraded = not stats.is_connected or not stats.is_disk_uptodate
1581
1582     if stats.is_disk_uptodate:
1583       ldisk_status = constants.LDS_OKAY
1584     elif stats.is_diskless:
1585       ldisk_status = constants.LDS_FAULTY
1586     else:
1587       ldisk_status = constants.LDS_UNKNOWN
1588
1589     return objects.BlockDevStatus(dev_path=self.dev_path,
1590                                   major=self.major,
1591                                   minor=self.minor,
1592                                   sync_percent=stats.sync_percent,
1593                                   estimated_time=stats.est_time,
1594                                   is_degraded=is_degraded,
1595                                   ldisk_status=ldisk_status)
1596
1597   def Open(self, force=False):
1598     """Make the local state primary.
1599
1600     If the 'force' parameter is given, the '-o' option is passed to
1601     drbdsetup. Since this is a potentially dangerous operation, the
1602     force flag should be only given after creation, when it actually
1603     is mandatory.
1604
1605     """
1606     if self.minor is None and not self.Attach():
1607       logging.error("DRBD cannot attach to a device during open")
1608       return False
1609     cmd = ["drbdsetup", self.dev_path, "primary"]
1610     if force:
1611       cmd.append("-o")
1612     result = utils.RunCmd(cmd)
1613     if result.failed:
1614       _ThrowError("drbd%d: can't make drbd device primary: %s", self.minor,
1615                   result.output)
1616
1617   def Close(self):
1618     """Make the local state secondary.
1619
1620     This will, of course, fail if the device is in use.
1621
1622     """
1623     if self.minor is None and not self.Attach():
1624       _ThrowError("drbd%d: can't Attach() in Close()", self._aminor)
1625     result = utils.RunCmd(["drbdsetup", self.dev_path, "secondary"])
1626     if result.failed:
1627       _ThrowError("drbd%d: can't switch drbd device to secondary: %s",
1628                   self.minor, result.output)
1629
1630   def DisconnectNet(self):
1631     """Removes network configuration.
1632
1633     This method shutdowns the network side of the device.
1634
1635     The method will wait up to a hardcoded timeout for the device to
1636     go into standalone after the 'disconnect' command before
1637     re-configuring it, as sometimes it takes a while for the
1638     disconnect to actually propagate and thus we might issue a 'net'
1639     command while the device is still connected. If the device will
1640     still be attached to the network and we time out, we raise an
1641     exception.
1642
1643     """
1644     if self.minor is None:
1645       _ThrowError("drbd%d: disk not attached in re-attach net", self._aminor)
1646
1647     if None in (self._lhost, self._lport, self._rhost, self._rport):
1648       _ThrowError("drbd%d: DRBD disk missing network info in"
1649                   " DisconnectNet()", self.minor)
1650
1651     class _DisconnectStatus:
1652       def __init__(self, ever_disconnected):
1653         self.ever_disconnected = ever_disconnected
1654
1655     dstatus = _DisconnectStatus(_IgnoreError(self._ShutdownNet, self.minor))
1656
1657     def _WaitForDisconnect():
1658       if self.GetProcStatus().is_standalone:
1659         return
1660
1661       # retry the disconnect, it seems possible that due to a well-time
1662       # disconnect on the peer, my disconnect command might be ignored and
1663       # forgotten
1664       dstatus.ever_disconnected = \
1665         _IgnoreError(self._ShutdownNet, self.minor) or dstatus.ever_disconnected
1666
1667       raise utils.RetryAgain()
1668
1669     # Keep start time
1670     start_time = time.time()
1671
1672     try:
1673       # Start delay at 100 milliseconds and grow up to 2 seconds
1674       utils.Retry(_WaitForDisconnect, (0.1, 1.5, 2.0),
1675                   self._NET_RECONFIG_TIMEOUT)
1676     except utils.RetryTimeout:
1677       if dstatus.ever_disconnected:
1678         msg = ("drbd%d: device did not react to the"
1679                " 'disconnect' command in a timely manner")
1680       else:
1681         msg = "drbd%d: can't shutdown network, even after multiple retries"
1682
1683       _ThrowError(msg, self.minor)
1684
1685     reconfig_time = time.time() - start_time
1686     if reconfig_time > (self._NET_RECONFIG_TIMEOUT * 0.25):
1687       logging.info("drbd%d: DisconnectNet: detach took %.3f seconds",
1688                    self.minor, reconfig_time)
1689
1690   def AttachNet(self, multimaster):
1691     """Reconnects the network.
1692
1693     This method connects the network side of the device with a
1694     specified multi-master flag. The device needs to be 'Standalone'
1695     but have valid network configuration data.
1696
1697     Args:
1698       - multimaster: init the network in dual-primary mode
1699
1700     """
1701     if self.minor is None:
1702       _ThrowError("drbd%d: device not attached in AttachNet", self._aminor)
1703
1704     if None in (self._lhost, self._lport, self._rhost, self._rport):
1705       _ThrowError("drbd%d: missing network info in AttachNet()", self.minor)
1706
1707     status = self.GetProcStatus()
1708
1709     if not status.is_standalone:
1710       _ThrowError("drbd%d: device is not standalone in AttachNet", self.minor)
1711
1712     self._AssembleNet(self.minor,
1713                       (self._lhost, self._lport, self._rhost, self._rport),
1714                       constants.DRBD_NET_PROTOCOL, dual_pri=multimaster,
1715                       hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1716
1717   def Attach(self):
1718     """Check if our minor is configured.
1719
1720     This doesn't do any device configurations - it only checks if the
1721     minor is in a state different from Unconfigured.
1722
1723     Note that this function will not change the state of the system in
1724     any way (except in case of side-effects caused by reading from
1725     /proc).
1726
1727     """
1728     used_devs = self.GetUsedDevs()
1729     if self._aminor in used_devs:
1730       minor = self._aminor
1731     else:
1732       minor = None
1733
1734     self._SetFromMinor(minor)
1735     return minor is not None
1736
1737   def Assemble(self):
1738     """Assemble the drbd.
1739
1740     Method:
1741       - if we have a configured device, we try to ensure that it matches
1742         our config
1743       - if not, we create it from zero
1744
1745     """
1746     super(DRBD8, self).Assemble()
1747
1748     self.Attach()
1749     if self.minor is None:
1750       # local device completely unconfigured
1751       self._FastAssemble()
1752     else:
1753       # we have to recheck the local and network status and try to fix
1754       # the device
1755       self._SlowAssemble()
1756
1757   def _SlowAssemble(self):
1758     """Assembles the DRBD device from a (partially) configured device.
1759
1760     In case of partially attached (local device matches but no network
1761     setup), we perform the network attach. If successful, we re-test
1762     the attach if can return success.
1763
1764     """
1765     # TODO: Rewrite to not use a for loop just because there is 'break'
1766     # pylint: disable-msg=W0631
1767     net_data = (self._lhost, self._lport, self._rhost, self._rport)
1768     for minor in (self._aminor,):
1769       info = self._GetDevInfo(self._GetShowData(minor))
1770       match_l = self._MatchesLocal(info)
1771       match_r = self._MatchesNet(info)
1772
1773       if match_l and match_r:
1774         # everything matches
1775         break
1776
1777       if match_l and not match_r and "local_addr" not in info:
1778         # disk matches, but not attached to network, attach and recheck
1779         self._AssembleNet(minor, net_data, constants.DRBD_NET_PROTOCOL,
1780                           hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1781         if self._MatchesNet(self._GetDevInfo(self._GetShowData(minor))):
1782           break
1783         else:
1784           _ThrowError("drbd%d: network attach successful, but 'drbdsetup"
1785                       " show' disagrees", minor)
1786
1787       if match_r and "local_dev" not in info:
1788         # no local disk, but network attached and it matches
1789         self._AssembleLocal(minor, self._children[0].dev_path,
1790                             self._children[1].dev_path, self.size)
1791         if self._MatchesNet(self._GetDevInfo(self._GetShowData(minor))):
1792           break
1793         else:
1794           _ThrowError("drbd%d: disk attach successful, but 'drbdsetup"
1795                       " show' disagrees", minor)
1796
1797       # this case must be considered only if we actually have local
1798       # storage, i.e. not in diskless mode, because all diskless
1799       # devices are equal from the point of view of local
1800       # configuration
1801       if (match_l and "local_dev" in info and
1802           not match_r and "local_addr" in info):
1803         # strange case - the device network part points to somewhere
1804         # else, even though its local storage is ours; as we own the
1805         # drbd space, we try to disconnect from the remote peer and
1806         # reconnect to our correct one
1807         try:
1808           self._ShutdownNet(minor)
1809         except errors.BlockDeviceError, err:
1810           _ThrowError("drbd%d: device has correct local storage, wrong"
1811                       " remote peer and is unable to disconnect in order"
1812                       " to attach to the correct peer: %s", minor, str(err))
1813         # note: _AssembleNet also handles the case when we don't want
1814         # local storage (i.e. one or more of the _[lr](host|port) is
1815         # None)
1816         self._AssembleNet(minor, net_data, constants.DRBD_NET_PROTOCOL,
1817                           hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1818         if self._MatchesNet(self._GetDevInfo(self._GetShowData(minor))):
1819           break
1820         else:
1821           _ThrowError("drbd%d: network attach successful, but 'drbdsetup"
1822                       " show' disagrees", minor)
1823
1824     else:
1825       minor = None
1826
1827     self._SetFromMinor(minor)
1828     if minor is None:
1829       _ThrowError("drbd%d: cannot activate, unknown or unhandled reason",
1830                   self._aminor)
1831
1832   def _FastAssemble(self):
1833     """Assemble the drbd device from zero.
1834
1835     This is run when in Assemble we detect our minor is unused.
1836
1837     """
1838     minor = self._aminor
1839     if self._children and self._children[0] and self._children[1]:
1840       self._AssembleLocal(minor, self._children[0].dev_path,
1841                           self._children[1].dev_path, self.size)
1842     if self._lhost and self._lport and self._rhost and self._rport:
1843       self._AssembleNet(minor,
1844                         (self._lhost, self._lport, self._rhost, self._rport),
1845                         constants.DRBD_NET_PROTOCOL,
1846                         hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1847     self._SetFromMinor(minor)
1848
1849   @classmethod
1850   def _ShutdownLocal(cls, minor):
1851     """Detach from the local device.
1852
1853     I/Os will continue to be served from the remote device. If we
1854     don't have a remote device, this operation will fail.
1855
1856     """
1857     result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "detach"])
1858     if result.failed:
1859       _ThrowError("drbd%d: can't detach local disk: %s", minor, result.output)
1860
1861   @classmethod
1862   def _ShutdownNet(cls, minor):
1863     """Disconnect from the remote peer.
1864
1865     This fails if we don't have a local device.
1866
1867     """
1868     result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "disconnect"])
1869     if result.failed:
1870       _ThrowError("drbd%d: can't shutdown network: %s", minor, result.output)
1871
1872   @classmethod
1873   def _ShutdownAll(cls, minor):
1874     """Deactivate the device.
1875
1876     This will, of course, fail if the device is in use.
1877
1878     """
1879     result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "down"])
1880     if result.failed:
1881       _ThrowError("drbd%d: can't shutdown drbd device: %s",
1882                   minor, result.output)
1883
1884   def Shutdown(self):
1885     """Shutdown the DRBD device.
1886
1887     """
1888     if self.minor is None and not self.Attach():
1889       logging.info("drbd%d: not attached during Shutdown()", self._aminor)
1890       return
1891     minor = self.minor
1892     self.minor = None
1893     self.dev_path = None
1894     self._ShutdownAll(minor)
1895
1896   def Remove(self):
1897     """Stub remove for DRBD devices.
1898
1899     """
1900     self.Shutdown()
1901
1902   @classmethod
1903   def Create(cls, unique_id, children, size):
1904     """Create a new DRBD8 device.
1905
1906     Since DRBD devices are not created per se, just assembled, this
1907     function only initializes the metadata.
1908
1909     """
1910     if len(children) != 2:
1911       raise errors.ProgrammerError("Invalid setup for the drbd device")
1912     # check that the minor is unused
1913     aminor = unique_id[4]
1914     proc_info = cls._MassageProcData(cls._GetProcData())
1915     if aminor in proc_info:
1916       status = DRBD8Status(proc_info[aminor])
1917       in_use = status.is_in_use
1918     else:
1919       in_use = False
1920     if in_use:
1921       _ThrowError("drbd%d: minor is already in use at Create() time", aminor)
1922     meta = children[1]
1923     meta.Assemble()
1924     if not meta.Attach():
1925       _ThrowError("drbd%d: can't attach to meta device '%s'",
1926                   aminor, meta)
1927     cls._CheckMetaSize(meta.dev_path)
1928     cls._InitMeta(aminor, meta.dev_path)
1929     return cls(unique_id, children, size)
1930
1931   def Grow(self, amount, dryrun):
1932     """Resize the DRBD device and its backing storage.
1933
1934     """
1935     if self.minor is None:
1936       _ThrowError("drbd%d: Grow called while not attached", self._aminor)
1937     if len(self._children) != 2 or None in self._children:
1938       _ThrowError("drbd%d: cannot grow diskless device", self.minor)
1939     self._children[0].Grow(amount, dryrun)
1940     if dryrun:
1941       # DRBD does not support dry-run mode, so we'll return here
1942       return
1943     result = utils.RunCmd(["drbdsetup", self.dev_path, "resize", "-s",
1944                            "%dm" % (self.size + amount)])
1945     if result.failed:
1946       _ThrowError("drbd%d: resize failed: %s", self.minor, result.output)
1947
1948
1949 class FileStorage(BlockDev):
1950   """File device.
1951
1952   This class represents the a file storage backend device.
1953
1954   The unique_id for the file device is a (file_driver, file_path) tuple.
1955
1956   """
1957   def __init__(self, unique_id, children, size):
1958     """Initalizes a file device backend.
1959
1960     """
1961     if children:
1962       raise errors.BlockDeviceError("Invalid setup for file device")
1963     super(FileStorage, self).__init__(unique_id, children, size)
1964     if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
1965       raise ValueError("Invalid configuration data %s" % str(unique_id))
1966     self.driver = unique_id[0]
1967     self.dev_path = unique_id[1]
1968     self.Attach()
1969
1970   def Assemble(self):
1971     """Assemble the device.
1972
1973     Checks whether the file device exists, raises BlockDeviceError otherwise.
1974
1975     """
1976     if not os.path.exists(self.dev_path):
1977       _ThrowError("File device '%s' does not exist" % self.dev_path)
1978
1979   def Shutdown(self):
1980     """Shutdown the device.
1981
1982     This is a no-op for the file type, as we don't deactivate
1983     the file on shutdown.
1984
1985     """
1986     pass
1987
1988   def Open(self, force=False):
1989     """Make the device ready for I/O.
1990
1991     This is a no-op for the file type.
1992
1993     """
1994     pass
1995
1996   def Close(self):
1997     """Notifies that the device will no longer be used for I/O.
1998
1999     This is a no-op for the file type.
2000
2001     """
2002     pass
2003
2004   def Remove(self):
2005     """Remove the file backing the block device.
2006
2007     @rtype: boolean
2008     @return: True if the removal was successful
2009
2010     """
2011     try:
2012       os.remove(self.dev_path)
2013     except OSError, err:
2014       if err.errno != errno.ENOENT:
2015         _ThrowError("Can't remove file '%s': %s", self.dev_path, err)
2016
2017   def Rename(self, new_id):
2018     """Renames the file.
2019
2020     """
2021     # TODO: implement rename for file-based storage
2022     _ThrowError("Rename is not supported for file-based storage")
2023
2024   def Grow(self, amount, dryrun):
2025     """Grow the file
2026
2027     @param amount: the amount (in mebibytes) to grow with
2028
2029     """
2030     # Check that the file exists
2031     self.Assemble()
2032     current_size = self.GetActualSize()
2033     new_size = current_size + amount * 1024 * 1024
2034     assert new_size > current_size, "Cannot Grow with a negative amount"
2035     # We can't really simulate the growth
2036     if dryrun:
2037       return
2038     try:
2039       f = open(self.dev_path, "a+")
2040       f.truncate(new_size)
2041       f.close()
2042     except EnvironmentError, err:
2043       _ThrowError("Error in file growth: %", str(err))
2044
2045   def Attach(self):
2046     """Attach to an existing file.
2047
2048     Check if this file already exists.
2049
2050     @rtype: boolean
2051     @return: True if file exists
2052
2053     """
2054     self.attached = os.path.exists(self.dev_path)
2055     return self.attached
2056
2057   def GetActualSize(self):
2058     """Return the actual disk size.
2059
2060     @note: the device needs to be active when this is called
2061
2062     """
2063     assert self.attached, "BlockDevice not attached in GetActualSize()"
2064     try:
2065       st = os.stat(self.dev_path)
2066       return st.st_size
2067     except OSError, err:
2068       _ThrowError("Can't stat %s: %s", self.dev_path, err)
2069
2070   @classmethod
2071   def Create(cls, unique_id, children, size):
2072     """Create a new file.
2073
2074     @param size: the size of file in MiB
2075
2076     @rtype: L{bdev.FileStorage}
2077     @return: an instance of FileStorage
2078
2079     """
2080     if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
2081       raise ValueError("Invalid configuration data %s" % str(unique_id))
2082     dev_path = unique_id[1]
2083     try:
2084       fd = os.open(dev_path, os.O_RDWR | os.O_CREAT | os.O_EXCL)
2085       f = os.fdopen(fd, "w")
2086       f.truncate(size * 1024 * 1024)
2087       f.close()
2088     except EnvironmentError, err:
2089       if err.errno == errno.EEXIST:
2090         _ThrowError("File already existing: %s", dev_path)
2091       _ThrowError("Error in file creation: %", str(err))
2092
2093     return FileStorage(unique_id, children, size)
2094
2095
2096 class PersistentBlockDevice(BlockDev):
2097   """A block device with persistent node
2098
2099   May be either directly attached, or exposed through DM (e.g. dm-multipath).
2100   udev helpers are probably required to give persistent, human-friendly
2101   names.
2102
2103   For the time being, pathnames are required to lie under /dev.
2104
2105   """
2106   def __init__(self, unique_id, children, size):
2107     """Attaches to a static block device.
2108
2109     The unique_id is a path under /dev.
2110
2111     """
2112     super(PersistentBlockDevice, self).__init__(unique_id, children, size)
2113     if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
2114       raise ValueError("Invalid configuration data %s" % str(unique_id))
2115     self.dev_path = unique_id[1]
2116     if not os.path.realpath(self.dev_path).startswith("/dev/"):
2117       raise ValueError("Full path '%s' lies outside /dev" %
2118                               os.path.realpath(self.dev_path))
2119     # TODO: this is just a safety guard checking that we only deal with devices
2120     # we know how to handle. In the future this will be integrated with
2121     # external storage backends and possible values will probably be collected
2122     # from the cluster configuration.
2123     if unique_id[0] != constants.BLOCKDEV_DRIVER_MANUAL:
2124       raise ValueError("Got persistent block device of invalid type: %s" %
2125                        unique_id[0])
2126
2127     self.major = self.minor = None
2128     self.Attach()
2129
2130   @classmethod
2131   def Create(cls, unique_id, children, size):
2132     """Create a new device
2133
2134     This is a noop, we only return a PersistentBlockDevice instance
2135
2136     """
2137     return PersistentBlockDevice(unique_id, children, 0)
2138
2139   def Remove(self):
2140     """Remove a device
2141
2142     This is a noop
2143
2144     """
2145     pass
2146
2147   def Rename(self, new_id):
2148     """Rename this device.
2149
2150     """
2151     _ThrowError("Rename is not supported for PersistentBlockDev storage")
2152
2153   def Attach(self):
2154     """Attach to an existing block device.
2155
2156
2157     """
2158     self.attached = False
2159     try:
2160       st = os.stat(self.dev_path)
2161     except OSError, err:
2162       logging.error("Error stat()'ing %s: %s", self.dev_path, str(err))
2163       return False
2164
2165     if not stat.S_ISBLK(st.st_mode):
2166       logging.error("%s is not a block device", self.dev_path)
2167       return False
2168
2169     self.major = os.major(st.st_rdev)
2170     self.minor = os.minor(st.st_rdev)
2171     self.attached = True
2172
2173     return True
2174
2175   def Assemble(self):
2176     """Assemble the device.
2177
2178     """
2179     pass
2180
2181   def Shutdown(self):
2182     """Shutdown the device.
2183
2184     """
2185     pass
2186
2187   def Open(self, force=False):
2188     """Make the device ready for I/O.
2189
2190     """
2191     pass
2192
2193   def Close(self):
2194     """Notifies that the device will no longer be used for I/O.
2195
2196     """
2197     pass
2198
2199   def Grow(self, amount, dryrun):
2200     """Grow the logical volume.
2201
2202     """
2203     _ThrowError("Grow is not supported for PersistentBlockDev storage")
2204
2205
2206 DEV_MAP = {
2207   constants.LD_LV: LogicalVolume,
2208   constants.LD_DRBD8: DRBD8,
2209   constants.LD_BLOCKDEV: PersistentBlockDevice,
2210   }
2211
2212 if constants.ENABLE_FILE_STORAGE or constants.ENABLE_SHARED_FILE_STORAGE:
2213   DEV_MAP[constants.LD_FILE] = FileStorage
2214
2215
2216 def FindDevice(dev_type, unique_id, children, size):
2217   """Search for an existing, assembled device.
2218
2219   This will succeed only if the device exists and is assembled, but it
2220   does not do any actions in order to activate the device.
2221
2222   """
2223   if dev_type not in DEV_MAP:
2224     raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
2225   device = DEV_MAP[dev_type](unique_id, children, size)
2226   if not device.attached:
2227     return None
2228   return device
2229
2230
2231 def Assemble(dev_type, unique_id, children, size):
2232   """Try to attach or assemble an existing device.
2233
2234   This will attach to assemble the device, as needed, to bring it
2235   fully up. It must be safe to run on already-assembled devices.
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](unique_id, children, size)
2241   device.Assemble()
2242   return device
2243
2244
2245 def Create(dev_type, unique_id, children, size):
2246   """Create a device.
2247
2248   """
2249   if dev_type not in DEV_MAP:
2250     raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
2251   device = DEV_MAP[dev_type].Create(unique_id, children, size)
2252   return device