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