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