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