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