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