(2.11) Make BlockDev subclasses adhere to new interface
[ganeti-local] / lib / storage / drbd.py
1 #
2 #
3
4 # Copyright (C) 2006, 2007, 2010, 2011, 2012, 2013 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 """DRBD block device related functionality"""
23
24 import errno
25 import logging
26 import time
27
28 from ganeti import constants
29 from ganeti import utils
30 from ganeti import errors
31 from ganeti import netutils
32 from ganeti import objects
33 from ganeti.storage import base
34 from ganeti.storage.drbd_info import DRBD8Info
35 from ganeti.storage import drbd_info
36 from ganeti.storage import drbd_cmdgen
37
38
39 # Size of reads in _CanReadDevice
40
41 _DEVICE_READ_SIZE = 128 * 1024
42
43
44 class DRBD8(object):
45   """Various methods to deals with the DRBD system as a whole.
46
47   This class provides a set of methods to deal with the DRBD installation on
48   the node or with uninitialized devices as opposed to a DRBD device.
49
50   """
51   _USERMODE_HELPER_FILE = "/sys/module/drbd/parameters/usermode_helper"
52
53   _MAX_MINORS = 255
54
55   @staticmethod
56   def GetUsermodeHelper(filename=_USERMODE_HELPER_FILE):
57     """Returns DRBD usermode_helper currently set.
58
59     @type filename: string
60     @param filename: the filename to read the usermode helper from
61     @rtype: string
62     @return: the currently configured DRBD usermode helper
63
64     """
65     try:
66       helper = utils.ReadFile(filename).splitlines()[0]
67     except EnvironmentError, err:
68       if err.errno == errno.ENOENT:
69         base.ThrowError("The file %s cannot be opened, check if the module"
70                         " is loaded (%s)", filename, str(err))
71       else:
72         base.ThrowError("Can't read DRBD helper file %s: %s",
73                         filename, str(err))
74     if not helper:
75       base.ThrowError("Can't read any data from %s", filename)
76     return helper
77
78   @staticmethod
79   def GetProcInfo():
80     """Reads and parses information from /proc/drbd.
81
82     @rtype: DRBD8Info
83     @return: a L{DRBD8Info} instance containing the current /proc/drbd info
84
85     """
86     return DRBD8Info.CreateFromFile()
87
88   @staticmethod
89   def GetUsedDevs():
90     """Compute the list of used DRBD minors.
91
92     @rtype: list of ints
93
94     """
95     info = DRBD8.GetProcInfo()
96     return filter(lambda m: not info.GetMinorStatus(m).is_unconfigured,
97                   info.GetMinors())
98
99   @staticmethod
100   def FindUnusedMinor():
101     """Find an unused DRBD device.
102
103     This is specific to 8.x as the minors are allocated dynamically,
104     so non-existing numbers up to a max minor count are actually free.
105
106     @rtype: int
107
108     """
109     highest = None
110     info = DRBD8.GetProcInfo()
111     for minor in info.GetMinors():
112       status = info.GetMinorStatus(minor)
113       if not status.is_in_use:
114         return minor
115       highest = max(highest, minor)
116
117     if highest is None: # there are no minors in use at all
118       return 0
119     if highest >= DRBD8._MAX_MINORS:
120       logging.error("Error: no free drbd minors!")
121       raise errors.BlockDeviceError("Can't find a free DRBD minor")
122
123     return highest + 1
124
125   @staticmethod
126   def GetCmdGenerator(info):
127     """Creates a suitable L{BaseDRBDCmdGenerator} based on the given info.
128
129     @type info: DRBD8Info
130     @rtype: BaseDRBDCmdGenerator
131
132     """
133     version = info.GetVersion()
134     if version["k_minor"] <= 3:
135       return drbd_cmdgen.DRBD83CmdGenerator(version)
136     else:
137       return drbd_cmdgen.DRBD84CmdGenerator(version)
138
139   @staticmethod
140   def ShutdownAll(minor):
141     """Deactivate the device.
142
143     This will, of course, fail if the device is in use.
144
145     @type minor: int
146     @param minor: the minor to shut down
147
148     """
149     info = DRBD8.GetProcInfo()
150     cmd_gen = DRBD8.GetCmdGenerator(info)
151
152     cmd = cmd_gen.GenDownCmd(minor)
153     result = utils.RunCmd(cmd)
154     if result.failed:
155       base.ThrowError("drbd%d: can't shutdown drbd device: %s",
156                       minor, result.output)
157
158
159 class DRBD8Dev(base.BlockDev):
160   """DRBD v8.x block device.
161
162   This implements the local host part of the DRBD device, i.e. it
163   doesn't do anything to the supposed peer. If you need a fully
164   connected DRBD pair, you need to use this class on both hosts.
165
166   The unique_id for the drbd device is a (pnode_uuid, snode_uuid,
167   port, pnode_minor, lnode_minor, secret) tuple, and it must have
168   two children: the data device and the meta_device. The meta
169   device is checked for valid size and is zeroed on create.
170
171   """
172   _DRBD_MAJOR = 147
173
174   # timeout constants
175   _NET_RECONFIG_TIMEOUT = 60
176
177   def __init__(self, unique_id, children, size, params, dyn_params, *args):
178     if children and children.count(None) > 0:
179       children = []
180     if len(children) not in (0, 2):
181       raise ValueError("Invalid configuration data %s" % str(children))
182     if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 6:
183       raise ValueError("Invalid configuration data %s" % str(unique_id))
184     if constants.DDP_LOCAL_IP not in dyn_params or \
185        constants.DDP_REMOTE_IP not in dyn_params or \
186        constants.DDP_LOCAL_MINOR not in dyn_params or \
187        constants.DDP_REMOTE_MINOR not in dyn_params:
188       raise ValueError("Invalid dynamic parameters %s" % str(dyn_params))
189
190     self._lhost = dyn_params[constants.DDP_LOCAL_IP]
191     self._lport = unique_id[2]
192     self._rhost = dyn_params[constants.DDP_REMOTE_IP]
193     self._rport = unique_id[2]
194     self._aminor = dyn_params[constants.DDP_LOCAL_MINOR]
195     self._secret = unique_id[5]
196
197     if children:
198       if not _CanReadDevice(children[1].dev_path):
199         logging.info("drbd%s: Ignoring unreadable meta device", self._aminor)
200         children = []
201     super(DRBD8Dev, self).__init__(unique_id, children, size, params,
202                                    dyn_params, *args)
203     self.major = self._DRBD_MAJOR
204
205     info = DRBD8.GetProcInfo()
206     version = info.GetVersion()
207     if version["k_major"] != 8:
208       base.ThrowError("Mismatch in DRBD kernel version and requested ganeti"
209                       " usage: kernel is %s.%s, ganeti wants 8.x",
210                       version["k_major"], version["k_minor"])
211
212     if version["k_minor"] <= 3:
213       self._show_info_cls = drbd_info.DRBD83ShowInfo
214     else:
215       self._show_info_cls = drbd_info.DRBD84ShowInfo
216
217     self._cmd_gen = DRBD8.GetCmdGenerator(info)
218
219     if (self._lhost is not None and self._lhost == self._rhost and
220             self._lport == self._rport):
221       raise ValueError("Invalid configuration data, same local/remote %s, %s" %
222                        (unique_id, dyn_params))
223     self.Attach()
224
225   @staticmethod
226   def _DevPath(minor):
227     """Return the path to a drbd device for a given minor.
228
229     @type minor: int
230     @rtype: string
231
232     """
233     return "/dev/drbd%d" % minor
234
235   def _SetFromMinor(self, minor):
236     """Set our parameters based on the given minor.
237
238     This sets our minor variable and our dev_path.
239
240     @type minor: int
241
242     """
243     if minor is None:
244       self.minor = self.dev_path = None
245       self.attached = False
246     else:
247       self.minor = minor
248       self.dev_path = self._DevPath(minor)
249       self.attached = True
250
251   @staticmethod
252   def _CheckMetaSize(meta_device):
253     """Check if the given meta device looks like a valid one.
254
255     This currently only checks the size, which must be around
256     128MiB.
257
258     @type meta_device: string
259     @param meta_device: the path to the device to check
260
261     """
262     result = utils.RunCmd(["blockdev", "--getsize", meta_device])
263     if result.failed:
264       base.ThrowError("Failed to get device size: %s - %s",
265                       result.fail_reason, result.output)
266     try:
267       sectors = int(result.stdout)
268     except (TypeError, ValueError):
269       base.ThrowError("Invalid output from blockdev: '%s'", result.stdout)
270     num_bytes = sectors * 512
271     if num_bytes < 128 * 1024 * 1024: # less than 128MiB
272       base.ThrowError("Meta device too small (%.2fMib)",
273                       (num_bytes / 1024 / 1024))
274     # the maximum *valid* size of the meta device when living on top
275     # of LVM is hard to compute: it depends on the number of stripes
276     # and the PE size; e.g. a 2-stripe, 64MB PE will result in a 128MB
277     # (normal size), but an eight-stripe 128MB PE will result in a 1GB
278     # size meta device; as such, we restrict it to 1GB (a little bit
279     # too generous, but making assumptions about PE size is hard)
280     if num_bytes > 1024 * 1024 * 1024:
281       base.ThrowError("Meta device too big (%.2fMiB)",
282                       (num_bytes / 1024 / 1024))
283
284   def _GetShowData(self, minor):
285     """Return the `drbdsetup show` data.
286
287     @type minor: int
288     @param minor: the minor to collect show output for
289     @rtype: string
290
291     """
292     result = utils.RunCmd(self._cmd_gen.GenShowCmd(minor))
293     if result.failed:
294       logging.error("Can't display the drbd config: %s - %s",
295                     result.fail_reason, result.output)
296       return None
297     return result.stdout
298
299   def _GetShowInfo(self, minor):
300     """Return parsed information from `drbdsetup show`.
301
302     @type minor: int
303     @param minor: the minor to return information for
304     @rtype: dict as described in L{drbd_info.BaseShowInfo.GetDevInfo}
305
306     """
307     return self._show_info_cls.GetDevInfo(self._GetShowData(minor))
308
309   def _MatchesLocal(self, info):
310     """Test if our local config matches with an existing device.
311
312     The parameter should be as returned from `_GetShowInfo()`. This
313     method tests if our local backing device is the same as the one in
314     the info parameter, in effect testing if we look like the given
315     device.
316
317     @type info: dict as described in L{drbd_info.BaseShowInfo.GetDevInfo}
318     @rtype: boolean
319
320     """
321     if self._children:
322       backend, meta = self._children
323     else:
324       backend = meta = None
325
326     if backend is not None:
327       retval = ("local_dev" in info and info["local_dev"] == backend.dev_path)
328     else:
329       retval = ("local_dev" not in info)
330
331     if meta is not None:
332       retval = retval and ("meta_dev" in info and
333                            info["meta_dev"] == meta.dev_path)
334       if "meta_index" in info:
335         retval = retval and info["meta_index"] == 0
336     else:
337       retval = retval and ("meta_dev" not in info and
338                            "meta_index" not in info)
339     return retval
340
341   def _MatchesNet(self, info):
342     """Test if our network config matches with an existing device.
343
344     The parameter should be as returned from `_GetShowInfo()`. This
345     method tests if our network configuration is the same as the one
346     in the info parameter, in effect testing if we look like the given
347     device.
348
349     @type info: dict as described in L{drbd_info.BaseShowInfo.GetDevInfo}
350     @rtype: boolean
351
352     """
353     if (((self._lhost is None and not ("local_addr" in info)) and
354          (self._rhost is None and not ("remote_addr" in info)))):
355       return True
356
357     if self._lhost is None:
358       return False
359
360     if not ("local_addr" in info and
361             "remote_addr" in info):
362       return False
363
364     retval = (info["local_addr"] == (self._lhost, self._lport))
365     retval = (retval and
366               info["remote_addr"] == (self._rhost, self._rport))
367     return retval
368
369   def _AssembleLocal(self, minor, backend, meta, size):
370     """Configure the local part of a DRBD device.
371
372     @type minor: int
373     @param minor: the minor to assemble locally
374     @type backend: string
375     @param backend: path to the data device to use
376     @type meta: string
377     @param meta: path to the meta device to use
378     @type size: int
379     @param size: size in MiB
380
381     """
382     cmds = self._cmd_gen.GenLocalInitCmds(minor, backend, meta,
383                                           size, self.params)
384
385     for cmd in cmds:
386       result = utils.RunCmd(cmd)
387       if result.failed:
388         base.ThrowError("drbd%d: can't attach local disk: %s",
389                         minor, result.output)
390
391   def _AssembleNet(self, minor, net_info, dual_pri=False, hmac=None,
392                    secret=None):
393     """Configure the network part of the device.
394
395     @type minor: int
396     @param minor: the minor to assemble the network for
397     @type net_info: (string, int, string, int)
398     @param net_info: tuple containing the local address, local port, remote
399       address and remote port
400     @type dual_pri: boolean
401     @param dual_pri: whether two primaries should be allowed or not
402     @type hmac: string
403     @param hmac: the HMAC algorithm to use
404     @type secret: string
405     @param secret: the shared secret to use
406
407     """
408     lhost, lport, rhost, rport = net_info
409     if None in net_info:
410       # we don't want network connection and actually want to make
411       # sure its shutdown
412       self._ShutdownNet(minor)
413       return
414
415     if dual_pri:
416       protocol = constants.DRBD_MIGRATION_NET_PROTOCOL
417     else:
418       protocol = self.params[constants.LDP_PROTOCOL]
419
420     # Workaround for a race condition. When DRBD is doing its dance to
421     # establish a connection with its peer, it also sends the
422     # synchronization speed over the wire. In some cases setting the
423     # sync speed only after setting up both sides can race with DRBD
424     # connecting, hence we set it here before telling DRBD anything
425     # about its peer.
426     sync_errors = self._SetMinorSyncParams(minor, self.params)
427     if sync_errors:
428       base.ThrowError("drbd%d: can't set the synchronization parameters: %s" %
429                       (minor, utils.CommaJoin(sync_errors)))
430
431     family = self._GetNetFamily(minor, lhost, rhost)
432
433     cmd = self._cmd_gen.GenNetInitCmd(minor, family, lhost, lport,
434                                       rhost, rport, protocol,
435                                       dual_pri, hmac, secret, self.params)
436
437     result = utils.RunCmd(cmd)
438     if result.failed:
439       base.ThrowError("drbd%d: can't setup network: %s - %s",
440                       minor, result.fail_reason, result.output)
441
442     def _CheckNetworkConfig():
443       info = self._GetShowInfo(minor)
444       if not "local_addr" in info or not "remote_addr" in info:
445         raise utils.RetryAgain()
446
447       if (info["local_addr"] != (lhost, lport) or
448           info["remote_addr"] != (rhost, rport)):
449         raise utils.RetryAgain()
450
451     try:
452       utils.Retry(_CheckNetworkConfig, 1.0, 10.0)
453     except utils.RetryTimeout:
454       base.ThrowError("drbd%d: timeout while configuring network", minor)
455
456     # Once the assembly is over, try to set the synchronization parameters
457     try:
458       # The minor may not have been set yet, requiring us to set it at least
459       # temporarily
460       old_minor = self.minor
461       self._SetFromMinor(minor)
462       sync_errors = self.SetSyncParams(self.params)
463       if sync_errors:
464         base.ThrowError("drbd%d: can't set the synchronization parameters: %s" %
465                         (self.minor, utils.CommaJoin(sync_errors)))
466     finally:
467       # Undo the change, regardless of whether it will have to be done again
468       # soon
469       self._SetFromMinor(old_minor)
470
471   @staticmethod
472   def _GetNetFamily(minor, lhost, rhost):
473     if netutils.IP6Address.IsValid(lhost):
474       if not netutils.IP6Address.IsValid(rhost):
475         base.ThrowError("drbd%d: can't connect ip %s to ip %s" %
476                         (minor, lhost, rhost))
477       return "ipv6"
478     elif netutils.IP4Address.IsValid(lhost):
479       if not netutils.IP4Address.IsValid(rhost):
480         base.ThrowError("drbd%d: can't connect ip %s to ip %s" %
481                         (minor, lhost, rhost))
482       return "ipv4"
483     else:
484       base.ThrowError("drbd%d: Invalid ip %s" % (minor, lhost))
485
486   def AddChildren(self, devices):
487     """Add a disk to the DRBD device.
488
489     @type devices: list of L{BlockDev}
490     @param devices: a list of exactly two L{BlockDev} objects; the first
491       denotes the data device, the second the meta device for this DRBD device
492
493     """
494     if self.minor is None:
495       base.ThrowError("drbd%d: can't attach to dbrd8 during AddChildren",
496                       self._aminor)
497     if len(devices) != 2:
498       base.ThrowError("drbd%d: need two devices for AddChildren", self.minor)
499     info = self._GetShowInfo(self.minor)
500     if "local_dev" in info:
501       base.ThrowError("drbd%d: already attached to a local disk", self.minor)
502     backend, meta = devices
503     if backend.dev_path is None or meta.dev_path is None:
504       base.ThrowError("drbd%d: children not ready during AddChildren",
505                       self.minor)
506     backend.Open()
507     meta.Open()
508     self._CheckMetaSize(meta.dev_path)
509     self._InitMeta(DRBD8.FindUnusedMinor(), meta.dev_path)
510
511     self._AssembleLocal(self.minor, backend.dev_path, meta.dev_path, self.size)
512     self._children = devices
513
514   def RemoveChildren(self, devices):
515     """Detach the drbd device from local storage.
516
517     @type devices: list of L{BlockDev}
518     @param devices: a list of exactly two L{BlockDev} objects; the first
519       denotes the data device, the second the meta device for this DRBD device
520
521     """
522     if self.minor is None:
523       base.ThrowError("drbd%d: can't attach to drbd8 during RemoveChildren",
524                       self._aminor)
525     # early return if we don't actually have backing storage
526     info = self._GetShowInfo(self.minor)
527     if "local_dev" not in info:
528       return
529     if len(self._children) != 2:
530       base.ThrowError("drbd%d: we don't have two children: %s", self.minor,
531                       self._children)
532     if self._children.count(None) == 2: # we don't actually have children :)
533       logging.warning("drbd%d: requested detach while detached", self.minor)
534       return
535     if len(devices) != 2:
536       base.ThrowError("drbd%d: we need two children in RemoveChildren",
537                       self.minor)
538     for child, dev in zip(self._children, devices):
539       if dev != child.dev_path:
540         base.ThrowError("drbd%d: mismatch in local storage (%s != %s) in"
541                         " RemoveChildren", self.minor, dev, child.dev_path)
542
543     self._ShutdownLocal(self.minor)
544     self._children = []
545
546   def _SetMinorSyncParams(self, minor, params):
547     """Set the parameters of the DRBD syncer.
548
549     This is the low-level implementation.
550
551     @type minor: int
552     @param minor: the drbd minor whose settings we change
553     @type params: dict
554     @param params: LD level disk parameters related to the synchronization
555     @rtype: list
556     @return: a list of error messages
557
558     """
559     cmd = self._cmd_gen.GenSyncParamsCmd(minor, params)
560     result = utils.RunCmd(cmd)
561     if result.failed:
562       msg = ("Can't change syncer rate: %s - %s" %
563              (result.fail_reason, result.output))
564       logging.error(msg)
565       return [msg]
566
567     return []
568
569   def SetSyncParams(self, params):
570     """Set the synchronization parameters of the DRBD syncer.
571
572     See L{BlockDev.SetSyncParams} for parameter description.
573
574     """
575     if self.minor is None:
576       err = "Not attached during SetSyncParams"
577       logging.info(err)
578       return [err]
579
580     children_result = super(DRBD8Dev, self).SetSyncParams(params)
581     children_result.extend(self._SetMinorSyncParams(self.minor, params))
582     return children_result
583
584   def PauseResumeSync(self, pause):
585     """Pauses or resumes the sync of a DRBD device.
586
587     See L{BlockDev.PauseResumeSync} for parameter description.
588
589     """
590     if self.minor is None:
591       logging.info("Not attached during PauseSync")
592       return False
593
594     children_result = super(DRBD8Dev, self).PauseResumeSync(pause)
595
596     if pause:
597       cmd = self._cmd_gen.GenPauseSyncCmd(self.minor)
598     else:
599       cmd = self._cmd_gen.GenResumeSyncCmd(self.minor)
600
601     result = utils.RunCmd(cmd)
602     if result.failed:
603       logging.error("Can't %s: %s - %s", cmd,
604                     result.fail_reason, result.output)
605     return not result.failed and children_result
606
607   def GetProcStatus(self):
608     """Return the current status data from /proc/drbd for this device.
609
610     @rtype: DRBD8Status
611
612     """
613     if self.minor is None:
614       base.ThrowError("drbd%d: GetStats() called while not attached",
615                       self._aminor)
616     info = DRBD8.GetProcInfo()
617     if not info.HasMinorStatus(self.minor):
618       base.ThrowError("drbd%d: can't find myself in /proc", self.minor)
619     return info.GetMinorStatus(self.minor)
620
621   def GetSyncStatus(self):
622     """Returns the sync status of the device.
623
624     If sync_percent is None, it means all is ok
625     If estimated_time is None, it means we can't estimate
626     the time needed, otherwise it's the time left in seconds.
627
628     We set the is_degraded parameter to True on two conditions:
629     network not connected or local disk missing.
630
631     We compute the ldisk parameter based on whether we have a local
632     disk or not.
633
634     @rtype: objects.BlockDevStatus
635
636     """
637     if self.minor is None and not self.Attach():
638       base.ThrowError("drbd%d: can't Attach() in GetSyncStatus", self._aminor)
639
640     stats = self.GetProcStatus()
641     is_degraded = not stats.is_connected or not stats.is_disk_uptodate
642
643     if stats.is_disk_uptodate:
644       ldisk_status = constants.LDS_OKAY
645     elif stats.is_diskless:
646       ldisk_status = constants.LDS_FAULTY
647     else:
648       ldisk_status = constants.LDS_UNKNOWN
649
650     return objects.BlockDevStatus(dev_path=self.dev_path,
651                                   major=self.major,
652                                   minor=self.minor,
653                                   sync_percent=stats.sync_percent,
654                                   estimated_time=stats.est_time,
655                                   is_degraded=is_degraded,
656                                   ldisk_status=ldisk_status)
657
658   def Open(self, force=False):
659     """Make the local state primary.
660
661     If the 'force' parameter is given, DRBD is instructed to switch the device
662     into primary mode. Since this is a potentially dangerous operation, the
663     force flag should be only given after creation, when it actually is
664     mandatory.
665
666     """
667     if self.minor is None and not self.Attach():
668       logging.error("DRBD cannot attach to a device during open")
669       return False
670
671     cmd = self._cmd_gen.GenPrimaryCmd(self.minor, force)
672
673     result = utils.RunCmd(cmd)
674     if result.failed:
675       base.ThrowError("drbd%d: can't make drbd device primary: %s", self.minor,
676                       result.output)
677
678   def Close(self):
679     """Make the local state secondary.
680
681     This will, of course, fail if the device is in use.
682
683     """
684     if self.minor is None and not self.Attach():
685       base.ThrowError("drbd%d: can't Attach() in Close()", self._aminor)
686     cmd = self._cmd_gen.GenSecondaryCmd(self.minor)
687     result = utils.RunCmd(cmd)
688     if result.failed:
689       base.ThrowError("drbd%d: can't switch drbd device to secondary: %s",
690                       self.minor, result.output)
691
692   def DisconnectNet(self):
693     """Removes network configuration.
694
695     This method shutdowns the network side of the device.
696
697     The method will wait up to a hardcoded timeout for the device to
698     go into standalone after the 'disconnect' command before
699     re-configuring it, as sometimes it takes a while for the
700     disconnect to actually propagate and thus we might issue a 'net'
701     command while the device is still connected. If the device will
702     still be attached to the network and we time out, we raise an
703     exception.
704
705     """
706     if self.minor is None:
707       base.ThrowError("drbd%d: disk not attached in re-attach net",
708                       self._aminor)
709
710     if None in (self._lhost, self._lport, self._rhost, self._rport):
711       base.ThrowError("drbd%d: DRBD disk missing network info in"
712                       " DisconnectNet()", self.minor)
713
714     class _DisconnectStatus:
715       def __init__(self, ever_disconnected):
716         self.ever_disconnected = ever_disconnected
717
718     dstatus = _DisconnectStatus(base.IgnoreError(self._ShutdownNet, self.minor))
719
720     def _WaitForDisconnect():
721       if self.GetProcStatus().is_standalone:
722         return
723
724       # retry the disconnect, it seems possible that due to a well-time
725       # disconnect on the peer, my disconnect command might be ignored and
726       # forgotten
727       dstatus.ever_disconnected = \
728         base.IgnoreError(self._ShutdownNet, self.minor) or \
729         dstatus.ever_disconnected
730
731       raise utils.RetryAgain()
732
733     # Keep start time
734     start_time = time.time()
735
736     try:
737       # Start delay at 100 milliseconds and grow up to 2 seconds
738       utils.Retry(_WaitForDisconnect, (0.1, 1.5, 2.0),
739                   self._NET_RECONFIG_TIMEOUT)
740     except utils.RetryTimeout:
741       if dstatus.ever_disconnected:
742         msg = ("drbd%d: device did not react to the"
743                " 'disconnect' command in a timely manner")
744       else:
745         msg = "drbd%d: can't shutdown network, even after multiple retries"
746
747       base.ThrowError(msg, self.minor)
748
749     reconfig_time = time.time() - start_time
750     if reconfig_time > (self._NET_RECONFIG_TIMEOUT * 0.25):
751       logging.info("drbd%d: DisconnectNet: detach took %.3f seconds",
752                    self.minor, reconfig_time)
753
754   def AttachNet(self, multimaster):
755     """Reconnects the network.
756
757     This method connects the network side of the device with a
758     specified multi-master flag. The device needs to be 'Standalone'
759     but have valid network configuration data.
760
761     @type multimaster: boolean
762     @param multimaster: init the network in dual-primary mode
763
764     """
765     if self.minor is None:
766       base.ThrowError("drbd%d: device not attached in AttachNet", self._aminor)
767
768     if None in (self._lhost, self._lport, self._rhost, self._rport):
769       base.ThrowError("drbd%d: missing network info in AttachNet()", self.minor)
770
771     status = self.GetProcStatus()
772
773     if not status.is_standalone:
774       base.ThrowError("drbd%d: device is not standalone in AttachNet",
775                       self.minor)
776
777     self._AssembleNet(self.minor,
778                       (self._lhost, self._lport, self._rhost, self._rport),
779                       dual_pri=multimaster, hmac=constants.DRBD_HMAC_ALG,
780                       secret=self._secret)
781
782   def Attach(self):
783     """Check if our minor is configured.
784
785     This doesn't do any device configurations - it only checks if the
786     minor is in a state different from Unconfigured.
787
788     Note that this function will not change the state of the system in
789     any way (except in case of side-effects caused by reading from
790     /proc).
791
792     """
793     used_devs = DRBD8.GetUsedDevs()
794     if self._aminor in used_devs:
795       minor = self._aminor
796     else:
797       minor = None
798
799     self._SetFromMinor(minor)
800     return minor is not None
801
802   def Assemble(self):
803     """Assemble the drbd.
804
805     Method:
806       - if we have a configured device, we try to ensure that it matches
807         our config
808       - if not, we create it from zero
809       - anyway, set the device parameters
810
811     """
812     super(DRBD8Dev, self).Assemble()
813
814     self.Attach()
815     if self.minor is None:
816       # local device completely unconfigured
817       self._FastAssemble()
818     else:
819       # we have to recheck the local and network status and try to fix
820       # the device
821       self._SlowAssemble()
822
823   def _SlowAssemble(self):
824     """Assembles the DRBD device from a (partially) configured device.
825
826     In case of partially attached (local device matches but no network
827     setup), we perform the network attach. If successful, we re-test
828     the attach if can return success.
829
830     """
831     # TODO: Rewrite to not use a for loop just because there is 'break'
832     # pylint: disable=W0631
833     net_data = (self._lhost, self._lport, self._rhost, self._rport)
834     for minor in (self._aminor,):
835       info = self._GetShowInfo(minor)
836       match_l = self._MatchesLocal(info)
837       match_r = self._MatchesNet(info)
838
839       if match_l and match_r:
840         # everything matches
841         break
842
843       if match_l and not match_r and "local_addr" not in info:
844         # disk matches, but not attached to network, attach and recheck
845         self._AssembleNet(minor, net_data, hmac=constants.DRBD_HMAC_ALG,
846                           secret=self._secret)
847         if self._MatchesNet(self._GetShowInfo(minor)):
848           break
849         else:
850           base.ThrowError("drbd%d: network attach successful, but 'drbdsetup"
851                           " show' disagrees", minor)
852
853       if match_r and "local_dev" not in info:
854         # no local disk, but network attached and it matches
855         self._AssembleLocal(minor, self._children[0].dev_path,
856                             self._children[1].dev_path, self.size)
857         if self._MatchesLocal(self._GetShowInfo(minor)):
858           break
859         else:
860           base.ThrowError("drbd%d: disk attach successful, but 'drbdsetup"
861                           " show' disagrees", minor)
862
863       # this case must be considered only if we actually have local
864       # storage, i.e. not in diskless mode, because all diskless
865       # devices are equal from the point of view of local
866       # configuration
867       if (match_l and "local_dev" in info and
868           not match_r and "local_addr" in info):
869         # strange case - the device network part points to somewhere
870         # else, even though its local storage is ours; as we own the
871         # drbd space, we try to disconnect from the remote peer and
872         # reconnect to our correct one
873         try:
874           self._ShutdownNet(minor)
875         except errors.BlockDeviceError, err:
876           base.ThrowError("drbd%d: device has correct local storage, wrong"
877                           " remote peer and is unable to disconnect in order"
878                           " to attach to the correct peer: %s", minor, str(err))
879         # note: _AssembleNet also handles the case when we don't want
880         # local storage (i.e. one or more of the _[lr](host|port) is
881         # None)
882         self._AssembleNet(minor, net_data, hmac=constants.DRBD_HMAC_ALG,
883                           secret=self._secret)
884         if self._MatchesNet(self._GetShowInfo(minor)):
885           break
886         else:
887           base.ThrowError("drbd%d: network attach successful, but 'drbdsetup"
888                           " show' disagrees", minor)
889
890     else:
891       minor = None
892
893     self._SetFromMinor(minor)
894     if minor is None:
895       base.ThrowError("drbd%d: cannot activate, unknown or unhandled reason",
896                       self._aminor)
897
898   def _FastAssemble(self):
899     """Assemble the drbd device from zero.
900
901     This is run when in Assemble we detect our minor is unused.
902
903     """
904     minor = self._aminor
905     if self._children and self._children[0] and self._children[1]:
906       self._AssembleLocal(minor, self._children[0].dev_path,
907                           self._children[1].dev_path, self.size)
908     if self._lhost and self._lport and self._rhost and self._rport:
909       self._AssembleNet(minor,
910                         (self._lhost, self._lport, self._rhost, self._rport),
911                         hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
912     self._SetFromMinor(minor)
913
914   def _ShutdownLocal(self, minor):
915     """Detach from the local device.
916
917     I/Os will continue to be served from the remote device. If we
918     don't have a remote device, this operation will fail.
919
920     @type minor: int
921     @param minor: the device to detach from the local device
922
923     """
924     cmd = self._cmd_gen.GenDetachCmd(minor)
925     result = utils.RunCmd(cmd)
926     if result.failed:
927       base.ThrowError("drbd%d: can't detach local disk: %s",
928                       minor, result.output)
929
930   def _ShutdownNet(self, minor):
931     """Disconnect from the remote peer.
932
933     This fails if we don't have a local device.
934
935     @type minor: boolean
936     @param minor: the device to disconnect from the remote peer
937
938     """
939     family = self._GetNetFamily(minor, self._lhost, self._rhost)
940     cmd = self._cmd_gen.GenDisconnectCmd(minor, family,
941                                          self._lhost, self._lport,
942                                          self._rhost, self._rport)
943     result = utils.RunCmd(cmd)
944     if result.failed:
945       base.ThrowError("drbd%d: can't shutdown network: %s",
946                       minor, result.output)
947
948   def Shutdown(self):
949     """Shutdown the DRBD device.
950
951     """
952     if self.minor is None and not self.Attach():
953       logging.info("drbd%d: not attached during Shutdown()", self._aminor)
954       return
955
956     try:
957       DRBD8.ShutdownAll(self.minor)
958     finally:
959       self.minor = None
960       self.dev_path = None
961
962   def Remove(self):
963     """Stub remove for DRBD devices.
964
965     """
966     self.Shutdown()
967
968   def Rename(self, new_id):
969     """Rename a device.
970
971     This is not supported for drbd devices.
972
973     """
974     raise errors.ProgrammerError("Can't rename a drbd device")
975
976   def Grow(self, amount, dryrun, backingstore, excl_stor):
977     """Resize the DRBD device and its backing storage.
978
979     See L{BlockDev.Grow} for parameter description.
980
981     """
982     if self.minor is None:
983       base.ThrowError("drbd%d: Grow called while not attached", self._aminor)
984     if len(self._children) != 2 or None in self._children:
985       base.ThrowError("drbd%d: cannot grow diskless device", self.minor)
986     self._children[0].Grow(amount, dryrun, backingstore, excl_stor)
987     if dryrun or backingstore:
988       # DRBD does not support dry-run mode and is not backing storage,
989       # so we'll return here
990       return
991     cmd = self._cmd_gen.GenResizeCmd(self.minor, self.size + amount)
992     result = utils.RunCmd(cmd)
993     if result.failed:
994       base.ThrowError("drbd%d: resize failed: %s", self.minor, result.output)
995
996   @classmethod
997   def _InitMeta(cls, minor, dev_path):
998     """Initialize a meta device.
999
1000     This will not work if the given minor is in use.
1001
1002     @type minor: int
1003     @param minor: the DRBD minor whose (future) meta device should be
1004       initialized
1005     @type dev_path: string
1006     @param dev_path: path to the meta device to initialize
1007
1008     """
1009     # Zero the metadata first, in order to make sure drbdmeta doesn't
1010     # try to auto-detect existing filesystems or similar (see
1011     # http://code.google.com/p/ganeti/issues/detail?id=182); we only
1012     # care about the first 128MB of data in the device, even though it
1013     # can be bigger
1014     result = utils.RunCmd([constants.DD_CMD,
1015                            "if=/dev/zero", "of=%s" % dev_path,
1016                            "bs=1048576", "count=128", "oflag=direct"])
1017     if result.failed:
1018       base.ThrowError("Can't wipe the meta device: %s", result.output)
1019
1020     info = DRBD8.GetProcInfo()
1021     cmd_gen = DRBD8.GetCmdGenerator(info)
1022     cmd = cmd_gen.GenInitMetaCmd(minor, dev_path)
1023
1024     result = utils.RunCmd(cmd)
1025     if result.failed:
1026       base.ThrowError("Can't initialize meta device: %s", result.output)
1027
1028   @classmethod
1029   def Create(cls, unique_id, children, size, spindles, params, excl_stor,
1030              dyn_params):
1031     """Create a new DRBD8 device.
1032
1033     Since DRBD devices are not created per se, just assembled, this
1034     function only initializes the metadata.
1035
1036     """
1037     if len(children) != 2:
1038       raise errors.ProgrammerError("Invalid setup for the drbd device")
1039     if excl_stor:
1040       raise errors.ProgrammerError("DRBD device requested with"
1041                                    " exclusive_storage")
1042     if constants.DDP_LOCAL_MINOR not in dyn_params:
1043       raise errors.ProgrammerError("Invalid dynamic params for drbd device %s"
1044                                    % dyn_params)
1045     # check that the minor is unused
1046     aminor = dyn_params[constants.DDP_LOCAL_MINOR]
1047
1048     info = DRBD8.GetProcInfo()
1049     if info.HasMinorStatus(aminor):
1050       status = info.GetMinorStatus(aminor)
1051       in_use = status.is_in_use
1052     else:
1053       in_use = False
1054     if in_use:
1055       base.ThrowError("drbd%d: minor is already in use at Create() time",
1056                       aminor)
1057     meta = children[1]
1058     meta.Assemble()
1059     if not meta.Attach():
1060       base.ThrowError("drbd%d: can't attach to meta device '%s'",
1061                       aminor, meta)
1062     cls._CheckMetaSize(meta.dev_path)
1063     cls._InitMeta(aminor, meta.dev_path)
1064     return cls(unique_id, children, size, params, dyn_params)
1065
1066
1067 def _CanReadDevice(path):
1068   """Check if we can read from the given device.
1069
1070   This tries to read the first 128k of the device.
1071
1072   @type path: string
1073
1074   """
1075   try:
1076     utils.ReadFile(path, size=_DEVICE_READ_SIZE)
1077     return True
1078   except EnvironmentError:
1079     logging.warning("Can't read from device %s", path, exc_info=True)
1080     return False