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