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