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