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