Convert default root partition to msdos style
[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     stat = open(filename, "r")
643     try:
644       data = stat.read().splitlines()
645     finally:
646       stat.close()
647     if not data:
648       _ThrowError("Can't read any data from %s", filename)
649     return data
650
651   @staticmethod
652   def _MassageProcData(data):
653     """Transform the output of _GetProdData into a nicer form.
654
655     @return: a dictionary of minor: joined lines from /proc/drbd
656         for that minor
657
658     """
659     lmatch = re.compile("^ *([0-9]+):.*$")
660     results = {}
661     old_minor = old_line = None
662     for line in data:
663       lresult = lmatch.match(line)
664       if lresult is not None:
665         if old_minor is not None:
666           results[old_minor] = old_line
667         old_minor = int(lresult.group(1))
668         old_line = line
669       else:
670         if old_minor is not None:
671           old_line += " " + line.strip()
672     # add last line
673     if old_minor is not None:
674       results[old_minor] = old_line
675     return results
676
677   @classmethod
678   def _GetVersion(cls):
679     """Return the DRBD version.
680
681     This will return a dict with keys:
682       - k_major
683       - k_minor
684       - k_point
685       - api
686       - proto
687       - proto2 (only on drbd > 8.2.X)
688
689     """
690     proc_data = cls._GetProcData()
691     first_line = proc_data[0].strip()
692     version = cls._VERSION_RE.match(first_line)
693     if not version:
694       raise errors.BlockDeviceError("Can't parse DRBD version from '%s'" %
695                                     first_line)
696
697     values = version.groups()
698     retval = {'k_major': int(values[0]),
699               'k_minor': int(values[1]),
700               'k_point': int(values[2]),
701               'api': int(values[3]),
702               'proto': int(values[4]),
703              }
704     if values[5] is not None:
705       retval['proto2'] = values[5]
706
707     return retval
708
709   @staticmethod
710   def _DevPath(minor):
711     """Return the path to a drbd device for a given minor.
712
713     """
714     return "/dev/drbd%d" % minor
715
716   @classmethod
717   def GetUsedDevs(cls):
718     """Compute the list of used DRBD devices.
719
720     """
721     data = cls._GetProcData()
722
723     used_devs = {}
724     valid_line = re.compile("^ *([0-9]+): cs:([^ ]+).*$")
725     for line in data:
726       match = valid_line.match(line)
727       if not match:
728         continue
729       minor = int(match.group(1))
730       state = match.group(2)
731       if state == cls._ST_UNCONFIGURED:
732         continue
733       used_devs[minor] = state, line
734
735     return used_devs
736
737   def _SetFromMinor(self, minor):
738     """Set our parameters based on the given minor.
739
740     This sets our minor variable and our dev_path.
741
742     """
743     if minor is None:
744       self.minor = self.dev_path = None
745       self.attached = False
746     else:
747       self.minor = minor
748       self.dev_path = self._DevPath(minor)
749       self.attached = True
750
751   @staticmethod
752   def _CheckMetaSize(meta_device):
753     """Check if the given meta device looks like a valid one.
754
755     This currently only check the size, which must be around
756     128MiB.
757
758     """
759     result = utils.RunCmd(["blockdev", "--getsize", meta_device])
760     if result.failed:
761       _ThrowError("Failed to get device size: %s - %s",
762                   result.fail_reason, result.output)
763     try:
764       sectors = int(result.stdout)
765     except ValueError:
766       _ThrowError("Invalid output from blockdev: '%s'", result.stdout)
767     bytes = sectors * 512
768     if bytes < 128 * 1024 * 1024: # less than 128MiB
769       _ThrowError("Meta device too small (%.2fMib)", (bytes / 1024 / 1024))
770     if bytes > (128 + 32) * 1024 * 1024: # account for an extra (big) PE on LVM
771       _ThrowError("Meta device too big (%.2fMiB)", (bytes / 1024 / 1024))
772
773   def Rename(self, new_id):
774     """Rename a device.
775
776     This is not supported for drbd devices.
777
778     """
779     raise errors.ProgrammerError("Can't rename a drbd device")
780
781
782 class DRBD8(BaseDRBD):
783   """DRBD v8.x block device.
784
785   This implements the local host part of the DRBD device, i.e. it
786   doesn't do anything to the supposed peer. If you need a fully
787   connected DRBD pair, you need to use this class on both hosts.
788
789   The unique_id for the drbd device is the (local_ip, local_port,
790   remote_ip, remote_port) tuple, and it must have two children: the
791   data device and the meta_device. The meta device is checked for
792   valid size and is zeroed on create.
793
794   """
795   _MAX_MINORS = 255
796   _PARSE_SHOW = None
797
798   # timeout constants
799   _NET_RECONFIG_TIMEOUT = 60
800
801   def __init__(self, unique_id, children):
802     if children and children.count(None) > 0:
803       children = []
804     super(DRBD8, self).__init__(unique_id, children)
805     self.major = self._DRBD_MAJOR
806     version = self._GetVersion()
807     if version['k_major'] != 8 :
808       _ThrowError("Mismatch in DRBD kernel version and requested ganeti"
809                   " usage: kernel is %s.%s, ganeti wants 8.x",
810                   version['k_major'], version['k_minor'])
811
812     if len(children) not in (0, 2):
813       raise ValueError("Invalid configuration data %s" % str(children))
814     if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 6:
815       raise ValueError("Invalid configuration data %s" % str(unique_id))
816     (self._lhost, self._lport,
817      self._rhost, self._rport,
818      self._aminor, self._secret) = unique_id
819     if (self._lhost is not None and self._lhost == self._rhost and
820         self._lport == self._rport):
821       raise ValueError("Invalid configuration data, same local/remote %s" %
822                        (unique_id,))
823     self.Attach()
824
825   @classmethod
826   def _InitMeta(cls, minor, dev_path):
827     """Initialize a meta device.
828
829     This will not work if the given minor is in use.
830
831     """
832     result = utils.RunCmd(["drbdmeta", "--force", cls._DevPath(minor),
833                            "v08", dev_path, "0", "create-md"])
834     if result.failed:
835       _ThrowError("Can't initialize meta device: %s", result.output)
836
837   @classmethod
838   def _FindUnusedMinor(cls):
839     """Find an unused DRBD device.
840
841     This is specific to 8.x as the minors are allocated dynamically,
842     so non-existing numbers up to a max minor count are actually free.
843
844     """
845     data = cls._GetProcData()
846
847     unused_line = re.compile("^ *([0-9]+): cs:Unconfigured$")
848     used_line = re.compile("^ *([0-9]+): cs:")
849     highest = None
850     for line in data:
851       match = unused_line.match(line)
852       if match:
853         return int(match.group(1))
854       match = used_line.match(line)
855       if match:
856         minor = int(match.group(1))
857         highest = max(highest, minor)
858     if highest is None: # there are no minors in use at all
859       return 0
860     if highest >= cls._MAX_MINORS:
861       logging.error("Error: no free drbd minors!")
862       raise errors.BlockDeviceError("Can't find a free DRBD minor")
863     return highest + 1
864
865   @classmethod
866   def _GetShowParser(cls):
867     """Return a parser for `drbd show` output.
868
869     This will either create or return an already-create parser for the
870     output of the command `drbd show`.
871
872     """
873     if cls._PARSE_SHOW is not None:
874       return cls._PARSE_SHOW
875
876     # pyparsing setup
877     lbrace = pyp.Literal("{").suppress()
878     rbrace = pyp.Literal("}").suppress()
879     semi = pyp.Literal(";").suppress()
880     # this also converts the value to an int
881     number = pyp.Word(pyp.nums).setParseAction(lambda s, l, t: int(t[0]))
882
883     comment = pyp.Literal ("#") + pyp.Optional(pyp.restOfLine)
884     defa = pyp.Literal("_is_default").suppress()
885     dbl_quote = pyp.Literal('"').suppress()
886
887     keyword = pyp.Word(pyp.alphanums + '-')
888
889     # value types
890     value = pyp.Word(pyp.alphanums + '_-/.:')
891     quoted = dbl_quote + pyp.CharsNotIn('"') + dbl_quote
892     addr_port = (pyp.Word(pyp.nums + '.') + pyp.Literal(':').suppress() +
893                  number)
894     # meta device, extended syntax
895     meta_value = ((value ^ quoted) + pyp.Literal('[').suppress() +
896                   number + pyp.Word(']').suppress())
897
898     # a statement
899     stmt = (~rbrace + keyword + ~lbrace +
900             pyp.Optional(addr_port ^ value ^ quoted ^ meta_value) +
901             pyp.Optional(defa) + semi +
902             pyp.Optional(pyp.restOfLine).suppress())
903
904     # an entire section
905     section_name = pyp.Word(pyp.alphas + '_')
906     section = section_name + lbrace + pyp.ZeroOrMore(pyp.Group(stmt)) + rbrace
907
908     bnf = pyp.ZeroOrMore(pyp.Group(section ^ stmt))
909     bnf.ignore(comment)
910
911     cls._PARSE_SHOW = bnf
912
913     return bnf
914
915   @classmethod
916   def _GetShowData(cls, minor):
917     """Return the `drbdsetup show` data for a minor.
918
919     """
920     result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "show"])
921     if result.failed:
922       logging.error("Can't display the drbd config: %s - %s",
923                     result.fail_reason, result.output)
924       return None
925     return result.stdout
926
927   @classmethod
928   def _GetDevInfo(cls, out):
929     """Parse details about a given DRBD minor.
930
931     This return, if available, the local backing device (as a path)
932     and the local and remote (ip, port) information from a string
933     containing the output of the `drbdsetup show` command as returned
934     by _GetShowData.
935
936     """
937     data = {}
938     if not out:
939       return data
940
941     bnf = cls._GetShowParser()
942     # run pyparse
943
944     try:
945       results = bnf.parseString(out)
946     except pyp.ParseException, err:
947       _ThrowError("Can't parse drbdsetup show output: %s", str(err))
948
949     # and massage the results into our desired format
950     for section in results:
951       sname = section[0]
952       if sname == "_this_host":
953         for lst in section[1:]:
954           if lst[0] == "disk":
955             data["local_dev"] = lst[1]
956           elif lst[0] == "meta-disk":
957             data["meta_dev"] = lst[1]
958             data["meta_index"] = lst[2]
959           elif lst[0] == "address":
960             data["local_addr"] = tuple(lst[1:])
961       elif sname == "_remote_host":
962         for lst in section[1:]:
963           if lst[0] == "address":
964             data["remote_addr"] = tuple(lst[1:])
965     return data
966
967   def _MatchesLocal(self, info):
968     """Test if our local config matches with an existing device.
969
970     The parameter should be as returned from `_GetDevInfo()`. This
971     method tests if our local backing device is the same as the one in
972     the info parameter, in effect testing if we look like the given
973     device.
974
975     """
976     if self._children:
977       backend, meta = self._children
978     else:
979       backend = meta = None
980
981     if backend is not None:
982       retval = ("local_dev" in info and info["local_dev"] == backend.dev_path)
983     else:
984       retval = ("local_dev" not in info)
985
986     if meta is not None:
987       retval = retval and ("meta_dev" in info and
988                            info["meta_dev"] == meta.dev_path)
989       retval = retval and ("meta_index" in info and
990                            info["meta_index"] == 0)
991     else:
992       retval = retval and ("meta_dev" not in info and
993                            "meta_index" not in info)
994     return retval
995
996   def _MatchesNet(self, info):
997     """Test if our network config matches with an existing device.
998
999     The parameter should be as returned from `_GetDevInfo()`. This
1000     method tests if our network configuration is the same as the one
1001     in the info parameter, in effect testing if we look like the given
1002     device.
1003
1004     """
1005     if (((self._lhost is None and not ("local_addr" in info)) and
1006          (self._rhost is None and not ("remote_addr" in info)))):
1007       return True
1008
1009     if self._lhost is None:
1010       return False
1011
1012     if not ("local_addr" in info and
1013             "remote_addr" in info):
1014       return False
1015
1016     retval = (info["local_addr"] == (self._lhost, self._lport))
1017     retval = (retval and
1018               info["remote_addr"] == (self._rhost, self._rport))
1019     return retval
1020
1021   @classmethod
1022   def _AssembleLocal(cls, minor, backend, meta):
1023     """Configure the local part of a DRBD device.
1024
1025     """
1026     args = ["drbdsetup", cls._DevPath(minor), "disk",
1027             backend, meta, "0", "-e", "detach", "--create-device"]
1028     result = utils.RunCmd(args)
1029     if result.failed:
1030       _ThrowError("drbd%d: can't attach local disk: %s", minor, result.output)
1031
1032   @classmethod
1033   def _AssembleNet(cls, minor, net_info, protocol,
1034                    dual_pri=False, hmac=None, secret=None):
1035     """Configure the network part of the device.
1036
1037     """
1038     lhost, lport, rhost, rport = net_info
1039     if None in net_info:
1040       # we don't want network connection and actually want to make
1041       # sure its shutdown
1042       cls._ShutdownNet(minor)
1043       return
1044
1045     # Workaround for a race condition. When DRBD is doing its dance to
1046     # establish a connection with its peer, it also sends the
1047     # synchronization speed over the wire. In some cases setting the
1048     # sync speed only after setting up both sides can race with DRBD
1049     # connecting, hence we set it here before telling DRBD anything
1050     # about its peer.
1051     cls._SetMinorSyncSpeed(minor, constants.SYNC_SPEED)
1052
1053     args = ["drbdsetup", cls._DevPath(minor), "net",
1054             "%s:%s" % (lhost, lport), "%s:%s" % (rhost, rport), protocol,
1055             "-A", "discard-zero-changes",
1056             "-B", "consensus",
1057             "--create-device",
1058             ]
1059     if dual_pri:
1060       args.append("-m")
1061     if hmac and secret:
1062       args.extend(["-a", hmac, "-x", secret])
1063     result = utils.RunCmd(args)
1064     if result.failed:
1065       _ThrowError("drbd%d: can't setup network: %s - %s",
1066                   minor, result.fail_reason, result.output)
1067
1068     timeout = time.time() + 10
1069     ok = False
1070     while time.time() < timeout:
1071       info = cls._GetDevInfo(cls._GetShowData(minor))
1072       if not "local_addr" in info or not "remote_addr" in info:
1073         time.sleep(1)
1074         continue
1075       if (info["local_addr"] != (lhost, lport) or
1076           info["remote_addr"] != (rhost, rport)):
1077         time.sleep(1)
1078         continue
1079       ok = True
1080       break
1081     if not ok:
1082       _ThrowError("drbd%d: timeout while configuring network", minor)
1083
1084   def AddChildren(self, devices):
1085     """Add a disk to the DRBD device.
1086
1087     """
1088     if self.minor is None:
1089       _ThrowError("drbd%d: can't attach to dbrd8 during AddChildren",
1090                   self._aminor)
1091     if len(devices) != 2:
1092       _ThrowError("drbd%d: need two devices for AddChildren", self.minor)
1093     info = self._GetDevInfo(self._GetShowData(self.minor))
1094     if "local_dev" in info:
1095       _ThrowError("drbd%d: already attached to a local disk", self.minor)
1096     backend, meta = devices
1097     if backend.dev_path is None or meta.dev_path is None:
1098       _ThrowError("drbd%d: children not ready during AddChildren", self.minor)
1099     backend.Open()
1100     meta.Open()
1101     self._CheckMetaSize(meta.dev_path)
1102     self._InitMeta(self._FindUnusedMinor(), meta.dev_path)
1103
1104     self._AssembleLocal(self.minor, backend.dev_path, meta.dev_path)
1105     self._children = devices
1106
1107   def RemoveChildren(self, devices):
1108     """Detach the drbd device from local storage.
1109
1110     """
1111     if self.minor is None:
1112       _ThrowError("drbd%d: can't attach to drbd8 during RemoveChildren",
1113                   self._aminor)
1114     # early return if we don't actually have backing storage
1115     info = self._GetDevInfo(self._GetShowData(self.minor))
1116     if "local_dev" not in info:
1117       return
1118     if len(self._children) != 2:
1119       _ThrowError("drbd%d: we don't have two children: %s", self.minor,
1120                   self._children)
1121     if self._children.count(None) == 2: # we don't actually have children :)
1122       logging.warning("drbd%d: requested detach while detached", self.minor)
1123       return
1124     if len(devices) != 2:
1125       _ThrowError("drbd%d: we need two children in RemoveChildren", self.minor)
1126     for child, dev in zip(self._children, devices):
1127       if dev != child.dev_path:
1128         _ThrowError("drbd%d: mismatch in local storage (%s != %s) in"
1129                     " RemoveChildren", self.minor, dev, child.dev_path)
1130
1131     self._ShutdownLocal(self.minor)
1132     self._children = []
1133
1134   @classmethod
1135   def _SetMinorSyncSpeed(cls, minor, kbytes):
1136     """Set the speed of the DRBD syncer.
1137
1138     This is the low-level implementation.
1139
1140     @type minor: int
1141     @param minor: the drbd minor whose settings we change
1142     @type kbytes: int
1143     @param kbytes: the speed in kbytes/second
1144     @rtype: boolean
1145     @return: the success of the operation
1146
1147     """
1148     result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "syncer",
1149                            "-r", "%d" % kbytes, "--create-device"])
1150     if result.failed:
1151       logging.error("Can't change syncer rate: %s - %s",
1152                     result.fail_reason, result.output)
1153     return not result.failed
1154
1155   def SetSyncSpeed(self, kbytes):
1156     """Set the speed of the DRBD syncer.
1157
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     if self.minor is None:
1165       logging.info("Not attached during SetSyncSpeed")
1166       return False
1167     children_result = super(DRBD8, self).SetSyncSpeed(kbytes)
1168     return self._SetMinorSyncSpeed(self.minor, kbytes) and children_result
1169
1170   def GetProcStatus(self):
1171     """Return device data from /proc.
1172
1173     """
1174     if self.minor is None:
1175       _ThrowError("drbd%d: GetStats() called while not attached", self._aminor)
1176     proc_info = self._MassageProcData(self._GetProcData())
1177     if self.minor not in proc_info:
1178       _ThrowError("drbd%d: can't find myself in /proc", self.minor)
1179     return DRBD8Status(proc_info[self.minor])
1180
1181   def GetSyncStatus(self):
1182     """Returns the sync status of the device.
1183
1184
1185     If sync_percent is None, it means all is ok
1186     If estimated_time is None, it means we can't esimate
1187     the time needed, otherwise it's the time left in seconds.
1188
1189
1190     We set the is_degraded parameter to True on two conditions:
1191     network not connected or local disk missing.
1192
1193     We compute the ldisk parameter based on wheter we have a local
1194     disk or not.
1195
1196     @rtype: tuple
1197     @return: (sync_percent, estimated_time, is_degraded, ldisk)
1198
1199     """
1200     if self.minor is None and not self.Attach():
1201       _ThrowError("drbd%d: can't Attach() in GetSyncStatus", self._aminor)
1202     stats = self.GetProcStatus()
1203     ldisk = not stats.is_disk_uptodate
1204     is_degraded = not stats.is_connected
1205     return stats.sync_percent, stats.est_time, is_degraded or ldisk, ldisk
1206
1207   def Open(self, force=False):
1208     """Make the local state primary.
1209
1210     If the 'force' parameter is given, the '-o' option is passed to
1211     drbdsetup. Since this is a potentially dangerous operation, the
1212     force flag should be only given after creation, when it actually
1213     is mandatory.
1214
1215     """
1216     if self.minor is None and not self.Attach():
1217       logging.error("DRBD cannot attach to a device during open")
1218       return False
1219     cmd = ["drbdsetup", self.dev_path, "primary"]
1220     if force:
1221       cmd.append("-o")
1222     result = utils.RunCmd(cmd)
1223     if result.failed:
1224       _ThrowError("drbd%d: can't make drbd device primary: %s", self.minor,
1225                   result.output)
1226
1227   def Close(self):
1228     """Make the local state secondary.
1229
1230     This will, of course, fail if the device is in use.
1231
1232     """
1233     if self.minor is None and not self.Attach():
1234       _ThrowError("drbd%d: can't Attach() in Close()", self._aminor)
1235     result = utils.RunCmd(["drbdsetup", self.dev_path, "secondary"])
1236     if result.failed:
1237       _ThrowError("drbd%d: can't switch drbd device to secondary: %s",
1238                   self.minor, result.output)
1239
1240   def DisconnectNet(self):
1241     """Removes network configuration.
1242
1243     This method shutdowns the network side of the device.
1244
1245     The method will wait up to a hardcoded timeout for the device to
1246     go into standalone after the 'disconnect' command before
1247     re-configuring it, as sometimes it takes a while for the
1248     disconnect to actually propagate and thus we might issue a 'net'
1249     command while the device is still connected. If the device will
1250     still be attached to the network and we time out, we raise an
1251     exception.
1252
1253     """
1254     if self.minor is None:
1255       _ThrowError("drbd%d: disk not attached in re-attach net", self._aminor)
1256
1257     if None in (self._lhost, self._lport, self._rhost, self._rport):
1258       _ThrowError("drbd%d: DRBD disk missing network info in"
1259                   " DisconnectNet()", self.minor)
1260
1261     ever_disconnected = _IgnoreError(self._ShutdownNet, self.minor)
1262     timeout_limit = time.time() + self._NET_RECONFIG_TIMEOUT
1263     sleep_time = 0.100 # we start the retry time at 100 miliseconds
1264     while time.time() < timeout_limit:
1265       status = self.GetProcStatus()
1266       if status.is_standalone:
1267         break
1268       # retry the disconnect, it seems possible that due to a
1269       # well-time disconnect on the peer, my disconnect command might
1270       # be ingored and forgotten
1271       ever_disconnected = _IgnoreError(self._ShutdownNet, self.minor) or \
1272                           ever_disconnected
1273       time.sleep(sleep_time)
1274       sleep_time = min(2, sleep_time * 1.5)
1275
1276     if not status.is_standalone:
1277       if ever_disconnected:
1278         msg = ("drbd%d: device did not react to the"
1279                " 'disconnect' command in a timely manner")
1280       else:
1281         msg = "drbd%d: can't shutdown network, even after multiple retries"
1282       _ThrowError(msg, self.minor)
1283
1284     reconfig_time = time.time() - timeout_limit + self._NET_RECONFIG_TIMEOUT
1285     if reconfig_time > 15: # hardcoded alert limit
1286       logging.info("drbd%d: DisconnectNet: detach took %.3f seconds",
1287                    self.minor, reconfig_time)
1288
1289   def AttachNet(self, multimaster):
1290     """Reconnects the network.
1291
1292     This method connects the network side of the device with a
1293     specified multi-master flag. The device needs to be 'Standalone'
1294     but have valid network configuration data.
1295
1296     Args:
1297       - multimaster: init the network in dual-primary mode
1298
1299     """
1300     if self.minor is None:
1301       _ThrowError("drbd%d: device not attached in AttachNet", self._aminor)
1302
1303     if None in (self._lhost, self._lport, self._rhost, self._rport):
1304       _ThrowError("drbd%d: missing network info in AttachNet()", self.minor)
1305
1306     status = self.GetProcStatus()
1307
1308     if not status.is_standalone:
1309       _ThrowError("drbd%d: device is not standalone in AttachNet", self.minor)
1310
1311     self._AssembleNet(self.minor,
1312                       (self._lhost, self._lport, self._rhost, self._rport),
1313                       constants.DRBD_NET_PROTOCOL, dual_pri=multimaster,
1314                       hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1315
1316   def Attach(self):
1317     """Check if our minor is configured.
1318
1319     This doesn't do any device configurations - it only checks if the
1320     minor is in a state different from Unconfigured.
1321
1322     Note that this function will not change the state of the system in
1323     any way (except in case of side-effects caused by reading from
1324     /proc).
1325
1326     """
1327     used_devs = self.GetUsedDevs()
1328     if self._aminor in used_devs:
1329       minor = self._aminor
1330     else:
1331       minor = None
1332
1333     self._SetFromMinor(minor)
1334     return minor is not None
1335
1336   def Assemble(self):
1337     """Assemble the drbd.
1338
1339     Method:
1340       - if we have a configured device, we try to ensure that it matches
1341         our config
1342       - if not, we create it from zero
1343
1344     """
1345     super(DRBD8, self).Assemble()
1346
1347     self.Attach()
1348     if self.minor is None:
1349       # local device completely unconfigured
1350       self._FastAssemble()
1351     else:
1352       # we have to recheck the local and network status and try to fix
1353       # the device
1354       self._SlowAssemble()
1355
1356   def _SlowAssemble(self):
1357     """Assembles the DRBD device from a (partially) configured device.
1358
1359     In case of partially attached (local device matches but no network
1360     setup), we perform the network attach. If successful, we re-test
1361     the attach if can return success.
1362
1363     """
1364     net_data = (self._lhost, self._lport, self._rhost, self._rport)
1365     for minor in (self._aminor,):
1366       info = self._GetDevInfo(self._GetShowData(minor))
1367       match_l = self._MatchesLocal(info)
1368       match_r = self._MatchesNet(info)
1369
1370       if match_l and match_r:
1371         # everything matches
1372         break
1373
1374       if match_l and not match_r and "local_addr" not in info:
1375         # disk matches, but not attached to network, attach and recheck
1376         self._AssembleNet(minor, net_data, constants.DRBD_NET_PROTOCOL,
1377                           hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1378         if self._MatchesNet(self._GetDevInfo(self._GetShowData(minor))):
1379           break
1380         else:
1381           _ThrowError("drbd%d: network attach successful, but 'drbdsetup"
1382                       " show' disagrees", minor)
1383
1384       if match_r and "local_dev" not in info:
1385         # no local disk, but network attached and it matches
1386         self._AssembleLocal(minor, self._children[0].dev_path,
1387                             self._children[1].dev_path)
1388         if self._MatchesNet(self._GetDevInfo(self._GetShowData(minor))):
1389           break
1390         else:
1391           _ThrowError("drbd%d: disk attach successful, but 'drbdsetup"
1392                       " show' disagrees", minor)
1393
1394       # this case must be considered only if we actually have local
1395       # storage, i.e. not in diskless mode, because all diskless
1396       # devices are equal from the point of view of local
1397       # configuration
1398       if (match_l and "local_dev" in info and
1399           not match_r and "local_addr" in info):
1400         # strange case - the device network part points to somewhere
1401         # else, even though its local storage is ours; as we own the
1402         # drbd space, we try to disconnect from the remote peer and
1403         # reconnect to our correct one
1404         try:
1405           self._ShutdownNet(minor)
1406         except errors.BlockDeviceError, err:
1407           _ThrowError("drbd%d: device has correct local storage, wrong"
1408                       " remote peer and is unable to disconnect in order"
1409                       " to attach to the correct peer: %s", minor, str(err))
1410         # note: _AssembleNet also handles the case when we don't want
1411         # local storage (i.e. one or more of the _[lr](host|port) is
1412         # None)
1413         self._AssembleNet(minor, net_data, constants.DRBD_NET_PROTOCOL,
1414                           hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1415         if self._MatchesNet(self._GetDevInfo(self._GetShowData(minor))):
1416           break
1417         else:
1418           _ThrowError("drbd%d: network attach successful, but 'drbdsetup"
1419                       " show' disagrees", minor)
1420
1421     else:
1422       minor = None
1423
1424     self._SetFromMinor(minor)
1425     if minor is None:
1426       _ThrowError("drbd%d: cannot activate, unknown or unhandled reason",
1427                   self._aminor)
1428
1429   def _FastAssemble(self):
1430     """Assemble the drbd device from zero.
1431
1432     This is run when in Assemble we detect our minor is unused.
1433
1434     """
1435     minor = self._aminor
1436     if self._children and self._children[0] and self._children[1]:
1437       self._AssembleLocal(minor, self._children[0].dev_path,
1438                           self._children[1].dev_path)
1439     if self._lhost and self._lport and self._rhost and self._rport:
1440       self._AssembleNet(minor,
1441                         (self._lhost, self._lport, self._rhost, self._rport),
1442                         constants.DRBD_NET_PROTOCOL,
1443                         hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1444     self._SetFromMinor(minor)
1445
1446   @classmethod
1447   def _ShutdownLocal(cls, minor):
1448     """Detach from the local device.
1449
1450     I/Os will continue to be served from the remote device. If we
1451     don't have a remote device, this operation will fail.
1452
1453     """
1454     result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "detach"])
1455     if result.failed:
1456       _ThrowError("drbd%d: can't detach local disk: %s", minor, result.output)
1457
1458   @classmethod
1459   def _ShutdownNet(cls, minor):
1460     """Disconnect from the remote peer.
1461
1462     This fails if we don't have a local device.
1463
1464     """
1465     result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "disconnect"])
1466     if result.failed:
1467       _ThrowError("drbd%d: can't shutdown network: %s", minor, result.output)
1468
1469   @classmethod
1470   def _ShutdownAll(cls, minor):
1471     """Deactivate the device.
1472
1473     This will, of course, fail if the device is in use.
1474
1475     """
1476     result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "down"])
1477     if result.failed:
1478       _ThrowError("drbd%d: can't shutdown drbd device: %s",
1479                   minor, result.output)
1480
1481   def Shutdown(self):
1482     """Shutdown the DRBD device.
1483
1484     """
1485     if self.minor is None and not self.Attach():
1486       logging.info("drbd%d: not attached during Shutdown()", self._aminor)
1487       return
1488     minor = self.minor
1489     self.minor = None
1490     self.dev_path = None
1491     self._ShutdownAll(minor)
1492
1493   def Remove(self):
1494     """Stub remove for DRBD devices.
1495
1496     """
1497     self.Shutdown()
1498
1499   @classmethod
1500   def Create(cls, unique_id, children, size):
1501     """Create a new DRBD8 device.
1502
1503     Since DRBD devices are not created per se, just assembled, this
1504     function only initializes the metadata.
1505
1506     """
1507     if len(children) != 2:
1508       raise errors.ProgrammerError("Invalid setup for the drbd device")
1509     # check that the minor is unused
1510     aminor = unique_id[4]
1511     proc_info = cls._MassageProcData(cls._GetProcData())
1512     if aminor in proc_info:
1513       status = DRBD8Status(proc_info[aminor])
1514       in_use = status.is_in_use
1515     else:
1516       in_use = False
1517     if in_use:
1518       _ThrowError("drbd%d: minor is already in use at Create() time", aminor)
1519     meta = children[1]
1520     meta.Assemble()
1521     if not meta.Attach():
1522       _ThrowError("drbd%d: can't attach to meta device '%s'",
1523                   aminor, meta)
1524     cls._CheckMetaSize(meta.dev_path)
1525     cls._InitMeta(aminor, meta.dev_path)
1526     return cls(unique_id, children)
1527
1528   def Grow(self, amount):
1529     """Resize the DRBD device and its backing storage.
1530
1531     """
1532     if self.minor is None:
1533       _ThrowError("drbd%d: Grow called while not attached", self._aminor)
1534     if len(self._children) != 2 or None in self._children:
1535       _ThrowError("drbd%d: cannot grow diskless device", self.minor)
1536     self._children[0].Grow(amount)
1537     result = utils.RunCmd(["drbdsetup", self.dev_path, "resize"])
1538     if result.failed:
1539       _ThrowError("drbd%d: resize failed: %s", self.minor, result.output)
1540
1541
1542 class FileStorage(BlockDev):
1543   """File device.
1544
1545   This class represents the a file storage backend device.
1546
1547   The unique_id for the file device is a (file_driver, file_path) tuple.
1548
1549   """
1550   def __init__(self, unique_id, children):
1551     """Initalizes a file device backend.
1552
1553     """
1554     if children:
1555       raise errors.BlockDeviceError("Invalid setup for file device")
1556     super(FileStorage, self).__init__(unique_id, children)
1557     if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
1558       raise ValueError("Invalid configuration data %s" % str(unique_id))
1559     self.driver = unique_id[0]
1560     self.dev_path = unique_id[1]
1561     self.Attach()
1562
1563   def Assemble(self):
1564     """Assemble the device.
1565
1566     Checks whether the file device exists, raises BlockDeviceError otherwise.
1567
1568     """
1569     if not os.path.exists(self.dev_path):
1570       _ThrowError("File device '%s' does not exist" % self.dev_path)
1571
1572   def Shutdown(self):
1573     """Shutdown the device.
1574
1575     This is a no-op for the file type, as we don't deacivate
1576     the file on shutdown.
1577
1578     """
1579     pass
1580
1581   def Open(self, force=False):
1582     """Make the device ready for I/O.
1583
1584     This is a no-op for the file type.
1585
1586     """
1587     pass
1588
1589   def Close(self):
1590     """Notifies that the device will no longer be used for I/O.
1591
1592     This is a no-op for the file type.
1593
1594     """
1595     pass
1596
1597   def Remove(self):
1598     """Remove the file backing the block device.
1599
1600     @rtype: boolean
1601     @return: True if the removal was successful
1602
1603     """
1604     try:
1605       os.remove(self.dev_path)
1606     except OSError, err:
1607       if err.errno != errno.ENOENT:
1608         _ThrowError("Can't remove file '%s': %s", self.dev_path, err)
1609
1610   def Attach(self):
1611     """Attach to an existing file.
1612
1613     Check if this file already exists.
1614
1615     @rtype: boolean
1616     @return: True if file exists
1617
1618     """
1619     self.attached = os.path.exists(self.dev_path)
1620     return self.attached
1621
1622   @classmethod
1623   def Create(cls, unique_id, children, size):
1624     """Create a new file.
1625
1626     @param size: the size of file in MiB
1627
1628     @rtype: L{bdev.FileStorage}
1629     @return: an instance of FileStorage
1630
1631     """
1632     if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
1633       raise ValueError("Invalid configuration data %s" % str(unique_id))
1634     dev_path = unique_id[1]
1635     if os.path.exists(dev_path):
1636       _ThrowError("File already existing: %s", dev_path)
1637     try:
1638       f = open(dev_path, 'w')
1639       f.truncate(size * 1024 * 1024)
1640       f.close()
1641     except IOError, err:
1642       _ThrowError("Error in file creation: %", str(err))
1643
1644     return FileStorage(unique_id, children)
1645
1646
1647 DEV_MAP = {
1648   constants.LD_LV: LogicalVolume,
1649   constants.LD_DRBD8: DRBD8,
1650   constants.LD_FILE: FileStorage,
1651   }
1652
1653
1654 def FindDevice(dev_type, unique_id, children):
1655   """Search for an existing, assembled device.
1656
1657   This will succeed only if the device exists and is assembled, but it
1658   does not do any actions in order to activate the device.
1659
1660   """
1661   if dev_type not in DEV_MAP:
1662     raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
1663   device = DEV_MAP[dev_type](unique_id, children)
1664   if not device.attached:
1665     return None
1666   return device
1667
1668
1669 def Assemble(dev_type, unique_id, children):
1670   """Try to attach or assemble an existing device.
1671
1672   This will attach to assemble the device, as needed, to bring it
1673   fully up. It must be safe to run on already-assembled devices.
1674
1675   """
1676   if dev_type not in DEV_MAP:
1677     raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
1678   device = DEV_MAP[dev_type](unique_id, children)
1679   device.Assemble()
1680   return device
1681
1682
1683 def Create(dev_type, unique_id, children, size):
1684   """Create a device.
1685
1686   """
1687   if dev_type not in DEV_MAP:
1688     raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
1689   device = DEV_MAP[dev_type].Create(unique_id, children, size)
1690   return device