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