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