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