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