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