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