| # |
| # |
| |
| # Copyright (C) 2006, 2007, 2010, 2011, 2012, 2013 Google Inc. |
| # |
| # This program is free software; you can redistribute it and/or modify |
| # it under the terms of the GNU General Public License as published by |
| # the Free Software Foundation; either version 2 of the License, or |
| # (at your option) any later version. |
| # |
| # This program is distributed in the hope that it will be useful, but |
| # WITHOUT ANY WARRANTY; without even the implied warranty of |
| # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
| # General Public License for more details. |
| # |
| # You should have received a copy of the GNU General Public License |
| # along with this program; if not, write to the Free Software |
| # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA |
| # 02110-1301, USA. |
| |
| |
| """DRBD block device related functionality""" |
| |
| import errno |
| import logging |
| import time |
| |
| from ganeti import constants |
| from ganeti import utils |
| from ganeti import errors |
| from ganeti import netutils |
| from ganeti import objects |
| from ganeti.storage import base |
| from ganeti.storage.drbd_info import DRBD8Info |
| from ganeti.storage import drbd_info |
| from ganeti.storage import drbd_cmdgen |
| |
| |
| # Size of reads in _CanReadDevice |
| |
| _DEVICE_READ_SIZE = 128 * 1024 |
| |
| |
| class DRBD8(object): |
| """Various methods to deals with the DRBD system as a whole. |
| |
| This class provides a set of methods to deal with the DRBD installation on |
| the node or with uninitialized devices as opposed to a DRBD device. |
| |
| """ |
| _USERMODE_HELPER_FILE = "/sys/module/drbd/parameters/usermode_helper" |
| |
| _MAX_MINORS = 255 |
| |
| @staticmethod |
| def GetUsermodeHelper(filename=_USERMODE_HELPER_FILE): |
| """Returns DRBD usermode_helper currently set. |
| |
| @type filename: string |
| @param filename: the filename to read the usermode helper from |
| @rtype: string |
| @return: the currently configured DRBD usermode helper |
| |
| """ |
| try: |
| helper = utils.ReadFile(filename).splitlines()[0] |
| except EnvironmentError, err: |
| if err.errno == errno.ENOENT: |
| base.ThrowError("The file %s cannot be opened, check if the module" |
| " is loaded (%s)", filename, str(err)) |
| else: |
| base.ThrowError("Can't read DRBD helper file %s: %s", |
| filename, str(err)) |
| if not helper: |
| base.ThrowError("Can't read any data from %s", filename) |
| return helper |
| |
| @staticmethod |
| def GetProcInfo(): |
| """Reads and parses information from /proc/drbd. |
| |
| @rtype: DRBD8Info |
| @return: a L{DRBD8Info} instance containing the current /proc/drbd info |
| |
| """ |
| return DRBD8Info.CreateFromFile() |
| |
| @staticmethod |
| def GetUsedDevs(): |
| """Compute the list of used DRBD minors. |
| |
| @rtype: list of ints |
| |
| """ |
| info = DRBD8.GetProcInfo() |
| return filter(lambda m: not info.GetMinorStatus(m).is_unconfigured, |
| info.GetMinors()) |
| |
| @staticmethod |
| def FindUnusedMinor(): |
| """Find an unused DRBD device. |
| |
| This is specific to 8.x as the minors are allocated dynamically, |
| so non-existing numbers up to a max minor count are actually free. |
| |
| @rtype: int |
| |
| """ |
| highest = None |
| info = DRBD8.GetProcInfo() |
| for minor in info.GetMinors(): |
| status = info.GetMinorStatus(minor) |
| if not status.is_in_use: |
| return minor |
| highest = max(highest, minor) |
| |
| if highest is None: # there are no minors in use at all |
| return 0 |
| if highest >= DRBD8._MAX_MINORS: |
| logging.error("Error: no free drbd minors!") |
| raise errors.BlockDeviceError("Can't find a free DRBD minor") |
| |
| return highest + 1 |
| |
| @staticmethod |
| def GetCmdGenerator(info): |
| """Creates a suitable L{BaseDRBDCmdGenerator} based on the given info. |
| |
| @type info: DRBD8Info |
| @rtype: BaseDRBDCmdGenerator |
| |
| """ |
| version = info.GetVersion() |
| if version["k_minor"] <= 3: |
| return drbd_cmdgen.DRBD83CmdGenerator(version) |
| else: |
| return drbd_cmdgen.DRBD84CmdGenerator(version) |
| |
| @staticmethod |
| def ShutdownAll(minor): |
| """Deactivate the device. |
| |
| This will, of course, fail if the device is in use. |
| |
| @type minor: int |
| @param minor: the minor to shut down |
| |
| """ |
| info = DRBD8.GetProcInfo() |
| cmd_gen = DRBD8.GetCmdGenerator(info) |
| |
| cmd = cmd_gen.GenDownCmd(minor) |
| result = utils.RunCmd(cmd) |
| if result.failed: |
| base.ThrowError("drbd%d: can't shutdown drbd device: %s", |
| minor, result.output) |
| |
| |
| class DRBD8Dev(base.BlockDev): |
| """DRBD v8.x block device. |
| |
| This implements the local host part of the DRBD device, i.e. it |
| doesn't do anything to the supposed peer. If you need a fully |
| connected DRBD pair, you need to use this class on both hosts. |
| |
| The unique_id for the drbd device is a (local_ip, local_port, |
| remote_ip, remote_port, local_minor, secret) tuple, and it must have |
| two children: the data device and the meta_device. The meta device |
| is checked for valid size and is zeroed on create. |
| |
| """ |
| _DRBD_MAJOR = 147 |
| |
| # timeout constants |
| _NET_RECONFIG_TIMEOUT = 60 |
| |
| def __init__(self, unique_id, children, size, params): |
| if children and children.count(None) > 0: |
| children = [] |
| if len(children) not in (0, 2): |
| raise ValueError("Invalid configuration data %s" % str(children)) |
| if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 6: |
| raise ValueError("Invalid configuration data %s" % str(unique_id)) |
| (self._lhost, self._lport, |
| self._rhost, self._rport, |
| self._aminor, self._secret) = unique_id |
| if children: |
| if not _CanReadDevice(children[1].dev_path): |
| logging.info("drbd%s: Ignoring unreadable meta device", self._aminor) |
| children = [] |
| super(DRBD8Dev, self).__init__(unique_id, children, size, params) |
| self.major = self._DRBD_MAJOR |
| |
| info = DRBD8.GetProcInfo() |
| version = info.GetVersion() |
| if version["k_major"] != 8: |
| base.ThrowError("Mismatch in DRBD kernel version and requested ganeti" |
| " usage: kernel is %s.%s, ganeti wants 8.x", |
| version["k_major"], version["k_minor"]) |
| |
| if version["k_minor"] <= 3: |
| self._show_info_cls = drbd_info.DRBD83ShowInfo |
| else: |
| self._show_info_cls = drbd_info.DRBD84ShowInfo |
| |
| self._cmd_gen = DRBD8.GetCmdGenerator(info) |
| |
| if (self._lhost is not None and self._lhost == self._rhost and |
| self._lport == self._rport): |
| raise ValueError("Invalid configuration data, same local/remote %s" % |
| (unique_id,)) |
| self.Attach() |
| |
| @staticmethod |
| def _DevPath(minor): |
| """Return the path to a drbd device for a given minor. |
| |
| @type minor: int |
| @rtype: string |
| |
| """ |
| return "/dev/drbd%d" % minor |
| |
| def _SetFromMinor(self, minor): |
| """Set our parameters based on the given minor. |
| |
| This sets our minor variable and our dev_path. |
| |
| @type minor: int |
| |
| """ |
| if minor is None: |
| self.minor = self.dev_path = None |
| self.attached = False |
| else: |
| self.minor = minor |
| self.dev_path = self._DevPath(minor) |
| self.attached = True |
| |
| @staticmethod |
| def _CheckMetaSize(meta_device): |
| """Check if the given meta device looks like a valid one. |
| |
| This currently only checks the size, which must be around |
| 128MiB. |
| |
| @type meta_device: string |
| @param meta_device: the path to the device to check |
| |
| """ |
| result = utils.RunCmd(["blockdev", "--getsize", meta_device]) |
| if result.failed: |
| base.ThrowError("Failed to get device size: %s - %s", |
| result.fail_reason, result.output) |
| try: |
| sectors = int(result.stdout) |
| except (TypeError, ValueError): |
| base.ThrowError("Invalid output from blockdev: '%s'", result.stdout) |
| num_bytes = sectors * 512 |
| if num_bytes < 128 * 1024 * 1024: # less than 128MiB |
| base.ThrowError("Meta device too small (%.2fMib)", |
| (num_bytes / 1024 / 1024)) |
| # the maximum *valid* size of the meta device when living on top |
| # of LVM is hard to compute: it depends on the number of stripes |
| # and the PE size; e.g. a 2-stripe, 64MB PE will result in a 128MB |
| # (normal size), but an eight-stripe 128MB PE will result in a 1GB |
| # size meta device; as such, we restrict it to 1GB (a little bit |
| # too generous, but making assumptions about PE size is hard) |
| if num_bytes > 1024 * 1024 * 1024: |
| base.ThrowError("Meta device too big (%.2fMiB)", |
| (num_bytes / 1024 / 1024)) |
| |
| def _GetShowData(self, minor): |
| """Return the `drbdsetup show` data. |
| |
| @type minor: int |
| @param minor: the minor to collect show output for |
| @rtype: string |
| |
| """ |
| result = utils.RunCmd(self._cmd_gen.GenShowCmd(minor)) |
| if result.failed: |
| logging.error("Can't display the drbd config: %s - %s", |
| result.fail_reason, result.output) |
| return None |
| return result.stdout |
| |
| def _GetShowInfo(self, minor): |
| """Return parsed information from `drbdsetup show`. |
| |
| @type minor: int |
| @param minor: the minor to return information for |
| @rtype: dict as described in L{drbd_info.BaseShowInfo.GetDevInfo} |
| |
| """ |
| return self._show_info_cls.GetDevInfo(self._GetShowData(minor)) |
| |
| def _MatchesLocal(self, info): |
| """Test if our local config matches with an existing device. |
| |
| The parameter should be as returned from `_GetShowInfo()`. This |
| method tests if our local backing device is the same as the one in |
| the info parameter, in effect testing if we look like the given |
| device. |
| |
| @type info: dict as described in L{drbd_info.BaseShowInfo.GetDevInfo} |
| @rtype: boolean |
| |
| """ |
| if self._children: |
| backend, meta = self._children |
| else: |
| backend = meta = None |
| |
| if backend is not None: |
| retval = ("local_dev" in info and info["local_dev"] == backend.dev_path) |
| else: |
| retval = ("local_dev" not in info) |
| |
| if meta is not None: |
| retval = retval and ("meta_dev" in info and |
| info["meta_dev"] == meta.dev_path) |
| if "meta_index" in info: |
| retval = retval and info["meta_index"] == 0 |
| else: |
| retval = retval and ("meta_dev" not in info and |
| "meta_index" not in info) |
| return retval |
| |
| def _MatchesNet(self, info): |
| """Test if our network config matches with an existing device. |
| |
| The parameter should be as returned from `_GetShowInfo()`. This |
| method tests if our network configuration is the same as the one |
| in the info parameter, in effect testing if we look like the given |
| device. |
| |
| @type info: dict as described in L{drbd_info.BaseShowInfo.GetDevInfo} |
| @rtype: boolean |
| |
| """ |
| if (((self._lhost is None and not ("local_addr" in info)) and |
| (self._rhost is None and not ("remote_addr" in info)))): |
| return True |
| |
| if self._lhost is None: |
| return False |
| |
| if not ("local_addr" in info and |
| "remote_addr" in info): |
| return False |
| |
| retval = (info["local_addr"] == (self._lhost, self._lport)) |
| retval = (retval and |
| info["remote_addr"] == (self._rhost, self._rport)) |
| return retval |
| |
| def _AssembleLocal(self, minor, backend, meta, size): |
| """Configure the local part of a DRBD device. |
| |
| @type minor: int |
| @param minor: the minor to assemble locally |
| @type backend: string |
| @param backend: path to the data device to use |
| @type meta: string |
| @param meta: path to the meta device to use |
| @type size: int |
| @param size: size in MiB |
| |
| """ |
| cmds = self._cmd_gen.GenLocalInitCmds(minor, backend, meta, |
| size, self.params) |
| |
| for cmd in cmds: |
| result = utils.RunCmd(cmd) |
| if result.failed: |
| base.ThrowError("drbd%d: can't attach local disk: %s", |
| minor, result.output) |
| |
| def _AssembleNet(self, minor, net_info, dual_pri=False, hmac=None, |
| secret=None): |
| """Configure the network part of the device. |
| |
| @type minor: int |
| @param minor: the minor to assemble the network for |
| @type net_info: (string, int, string, int) |
| @param net_info: tuple containing the local address, local port, remote |
| address and remote port |
| @type dual_pri: boolean |
| @param dual_pri: whether two primaries should be allowed or not |
| @type hmac: string |
| @param hmac: the HMAC algorithm to use |
| @type secret: string |
| @param secret: the shared secret to use |
| |
| """ |
| lhost, lport, rhost, rport = net_info |
| if None in net_info: |
| # we don't want network connection and actually want to make |
| # sure its shutdown |
| self._ShutdownNet(minor) |
| return |
| |
| if dual_pri: |
| protocol = constants.DRBD_MIGRATION_NET_PROTOCOL |
| else: |
| protocol = self.params[constants.LDP_PROTOCOL] |
| |
| # Workaround for a race condition. When DRBD is doing its dance to |
| # establish a connection with its peer, it also sends the |
| # synchronization speed over the wire. In some cases setting the |
| # sync speed only after setting up both sides can race with DRBD |
| # connecting, hence we set it here before telling DRBD anything |
| # about its peer. |
| sync_errors = self._SetMinorSyncParams(minor, self.params) |
| if sync_errors: |
| base.ThrowError("drbd%d: can't set the synchronization parameters: %s" % |
| (minor, utils.CommaJoin(sync_errors))) |
| |
| family = self._GetNetFamily(minor, lhost, rhost) |
| |
| cmd = self._cmd_gen.GenNetInitCmd(minor, family, lhost, lport, |
| rhost, rport, protocol, |
| dual_pri, hmac, secret, self.params) |
| |
| result = utils.RunCmd(cmd) |
| if result.failed: |
| base.ThrowError("drbd%d: can't setup network: %s - %s", |
| minor, result.fail_reason, result.output) |
| |
| def _CheckNetworkConfig(): |
| info = self._GetShowInfo(minor) |
| if not "local_addr" in info or not "remote_addr" in info: |
| raise utils.RetryAgain() |
| |
| if (info["local_addr"] != (lhost, lport) or |
| info["remote_addr"] != (rhost, rport)): |
| raise utils.RetryAgain() |
| |
| try: |
| utils.Retry(_CheckNetworkConfig, 1.0, 10.0) |
| except utils.RetryTimeout: |
| base.ThrowError("drbd%d: timeout while configuring network", minor) |
| |
| @staticmethod |
| def _GetNetFamily(minor, lhost, rhost): |
| if netutils.IP6Address.IsValid(lhost): |
| if not netutils.IP6Address.IsValid(rhost): |
| base.ThrowError("drbd%d: can't connect ip %s to ip %s" % |
| (minor, lhost, rhost)) |
| return "ipv6" |
| elif netutils.IP4Address.IsValid(lhost): |
| if not netutils.IP4Address.IsValid(rhost): |
| base.ThrowError("drbd%d: can't connect ip %s to ip %s" % |
| (minor, lhost, rhost)) |
| return "ipv4" |
| else: |
| base.ThrowError("drbd%d: Invalid ip %s" % (minor, lhost)) |
| |
| def AddChildren(self, devices): |
| """Add a disk to the DRBD device. |
| |
| @type devices: list of L{BlockDev} |
| @param devices: a list of exactly two L{BlockDev} objects; the first |
| denotes the data device, the second the meta device for this DRBD device |
| |
| """ |
| if self.minor is None: |
| base.ThrowError("drbd%d: can't attach to dbrd8 during AddChildren", |
| self._aminor) |
| if len(devices) != 2: |
| base.ThrowError("drbd%d: need two devices for AddChildren", self.minor) |
| info = self._GetShowInfo(self.minor) |
| if "local_dev" in info: |
| base.ThrowError("drbd%d: already attached to a local disk", self.minor) |
| backend, meta = devices |
| if backend.dev_path is None or meta.dev_path is None: |
| base.ThrowError("drbd%d: children not ready during AddChildren", |
| self.minor) |
| backend.Open() |
| meta.Open() |
| self._CheckMetaSize(meta.dev_path) |
| self._InitMeta(DRBD8.FindUnusedMinor(), meta.dev_path) |
| |
| self._AssembleLocal(self.minor, backend.dev_path, meta.dev_path, self.size) |
| self._children = devices |
| |
| def RemoveChildren(self, devices): |
| """Detach the drbd device from local storage. |
| |
| @type devices: list of L{BlockDev} |
| @param devices: a list of exactly two L{BlockDev} objects; the first |
| denotes the data device, the second the meta device for this DRBD device |
| |
| """ |
| if self.minor is None: |
| base.ThrowError("drbd%d: can't attach to drbd8 during RemoveChildren", |
| self._aminor) |
| # early return if we don't actually have backing storage |
| info = self._GetShowInfo(self.minor) |
| if "local_dev" not in info: |
| return |
| if len(self._children) != 2: |
| base.ThrowError("drbd%d: we don't have two children: %s", self.minor, |
| self._children) |
| if self._children.count(None) == 2: # we don't actually have children :) |
| logging.warning("drbd%d: requested detach while detached", self.minor) |
| return |
| if len(devices) != 2: |
| base.ThrowError("drbd%d: we need two children in RemoveChildren", |
| self.minor) |
| for child, dev in zip(self._children, devices): |
| if dev != child.dev_path: |
| base.ThrowError("drbd%d: mismatch in local storage (%s != %s) in" |
| " RemoveChildren", self.minor, dev, child.dev_path) |
| |
| self._ShutdownLocal(self.minor) |
| self._children = [] |
| |
| def _SetMinorSyncParams(self, minor, params): |
| """Set the parameters of the DRBD syncer. |
| |
| This is the low-level implementation. |
| |
| @type minor: int |
| @param minor: the drbd minor whose settings we change |
| @type params: dict |
| @param params: LD level disk parameters related to the synchronization |
| @rtype: list |
| @return: a list of error messages |
| |
| """ |
| cmd = self._cmd_gen.GenSyncParamsCmd(minor, params) |
| result = utils.RunCmd(cmd) |
| if result.failed: |
| msg = ("Can't change syncer rate: %s - %s" % |
| (result.fail_reason, result.output)) |
| logging.error(msg) |
| return [msg] |
| |
| return [] |
| |
| def SetSyncParams(self, params): |
| """Set the synchronization parameters of the DRBD syncer. |
| |
| See L{BlockDev.SetSyncParams} for parameter description. |
| |
| """ |
| if self.minor is None: |
| err = "Not attached during SetSyncParams" |
| logging.info(err) |
| return [err] |
| |
| children_result = super(DRBD8Dev, self).SetSyncParams(params) |
| children_result.extend(self._SetMinorSyncParams(self.minor, params)) |
| return children_result |
| |
| def PauseResumeSync(self, pause): |
| """Pauses or resumes the sync of a DRBD device. |
| |
| See L{BlockDev.PauseResumeSync} for parameter description. |
| |
| """ |
| if self.minor is None: |
| logging.info("Not attached during PauseSync") |
| return False |
| |
| children_result = super(DRBD8Dev, self).PauseResumeSync(pause) |
| |
| if pause: |
| cmd = self._cmd_gen.GenPauseSyncCmd(self.minor) |
| else: |
| cmd = self._cmd_gen.GenResumeSyncCmd(self.minor) |
| |
| result = utils.RunCmd(cmd) |
| if result.failed: |
| logging.error("Can't %s: %s - %s", cmd, |
| result.fail_reason, result.output) |
| return not result.failed and children_result |
| |
| def GetProcStatus(self): |
| """Return the current status data from /proc/drbd for this device. |
| |
| @rtype: DRBD8Status |
| |
| """ |
| if self.minor is None: |
| base.ThrowError("drbd%d: GetStats() called while not attached", |
| self._aminor) |
| info = DRBD8.GetProcInfo() |
| if not info.HasMinorStatus(self.minor): |
| base.ThrowError("drbd%d: can't find myself in /proc", self.minor) |
| return info.GetMinorStatus(self.minor) |
| |
| def GetSyncStatus(self): |
| """Returns the sync status of the device. |
| |
| If sync_percent is None, it means all is ok |
| If estimated_time is None, it means we can't estimate |
| the time needed, otherwise it's the time left in seconds. |
| |
| We set the is_degraded parameter to True on two conditions: |
| network not connected or local disk missing. |
| |
| We compute the ldisk parameter based on whether we have a local |
| disk or not. |
| |
| @rtype: objects.BlockDevStatus |
| |
| """ |
| if self.minor is None and not self.Attach(): |
| base.ThrowError("drbd%d: can't Attach() in GetSyncStatus", self._aminor) |
| |
| stats = self.GetProcStatus() |
| is_degraded = not stats.is_connected or not stats.is_disk_uptodate |
| |
| if stats.is_disk_uptodate: |
| ldisk_status = constants.LDS_OKAY |
| elif stats.is_diskless: |
| ldisk_status = constants.LDS_FAULTY |
| else: |
| ldisk_status = constants.LDS_UNKNOWN |
| |
| return objects.BlockDevStatus(dev_path=self.dev_path, |
| major=self.major, |
| minor=self.minor, |
| sync_percent=stats.sync_percent, |
| estimated_time=stats.est_time, |
| is_degraded=is_degraded, |
| ldisk_status=ldisk_status) |
| |
| def Open(self, force=False): |
| """Make the local state primary. |
| |
| If the 'force' parameter is given, DRBD is instructed to switch the device |
| into primary mode. Since this is a potentially dangerous operation, the |
| force flag should be only given after creation, when it actually is |
| mandatory. |
| |
| """ |
| if self.minor is None and not self.Attach(): |
| logging.error("DRBD cannot attach to a device during open") |
| return False |
| |
| cmd = self._cmd_gen.GenPrimaryCmd(self.minor, force) |
| |
| result = utils.RunCmd(cmd) |
| if result.failed: |
| base.ThrowError("drbd%d: can't make drbd device primary: %s", self.minor, |
| result.output) |
| |
| def Close(self): |
| """Make the local state secondary. |
| |
| This will, of course, fail if the device is in use. |
| |
| """ |
| if self.minor is None and not self.Attach(): |
| base.ThrowError("drbd%d: can't Attach() in Close()", self._aminor) |
| cmd = self._cmd_gen.GenSecondaryCmd(self.minor) |
| result = utils.RunCmd(cmd) |
| if result.failed: |
| base.ThrowError("drbd%d: can't switch drbd device to secondary: %s", |
| self.minor, result.output) |
| |
| def DisconnectNet(self): |
| """Removes network configuration. |
| |
| This method shutdowns the network side of the device. |
| |
| The method will wait up to a hardcoded timeout for the device to |
| go into standalone after the 'disconnect' command before |
| re-configuring it, as sometimes it takes a while for the |
| disconnect to actually propagate and thus we might issue a 'net' |
| command while the device is still connected. If the device will |
| still be attached to the network and we time out, we raise an |
| exception. |
| |
| """ |
| if self.minor is None: |
| base.ThrowError("drbd%d: disk not attached in re-attach net", |
| self._aminor) |
| |
| if None in (self._lhost, self._lport, self._rhost, self._rport): |
| base.ThrowError("drbd%d: DRBD disk missing network info in" |
| " DisconnectNet()", self.minor) |
| |
| class _DisconnectStatus: |
| def __init__(self, ever_disconnected): |
| self.ever_disconnected = ever_disconnected |
| |
| dstatus = _DisconnectStatus(base.IgnoreError(self._ShutdownNet, self.minor)) |
| |
| def _WaitForDisconnect(): |
| if self.GetProcStatus().is_standalone: |
| return |
| |
| # retry the disconnect, it seems possible that due to a well-time |
| # disconnect on the peer, my disconnect command might be ignored and |
| # forgotten |
| dstatus.ever_disconnected = \ |
| base.IgnoreError(self._ShutdownNet, self.minor) or \ |
| dstatus.ever_disconnected |
| |
| raise utils.RetryAgain() |
| |
| # Keep start time |
| start_time = time.time() |
| |
| try: |
| # Start delay at 100 milliseconds and grow up to 2 seconds |
| utils.Retry(_WaitForDisconnect, (0.1, 1.5, 2.0), |
| self._NET_RECONFIG_TIMEOUT) |
| except utils.RetryTimeout: |
| if dstatus.ever_disconnected: |
| msg = ("drbd%d: device did not react to the" |
| " 'disconnect' command in a timely manner") |
| else: |
| msg = "drbd%d: can't shutdown network, even after multiple retries" |
| |
| base.ThrowError(msg, self.minor) |
| |
| reconfig_time = time.time() - start_time |
| if reconfig_time > (self._NET_RECONFIG_TIMEOUT * 0.25): |
| logging.info("drbd%d: DisconnectNet: detach took %.3f seconds", |
| self.minor, reconfig_time) |
| |
| def AttachNet(self, multimaster): |
| """Reconnects the network. |
| |
| This method connects the network side of the device with a |
| specified multi-master flag. The device needs to be 'Standalone' |
| but have valid network configuration data. |
| |
| @type multimaster: boolean |
| @param multimaster: init the network in dual-primary mode |
| |
| """ |
| if self.minor is None: |
| base.ThrowError("drbd%d: device not attached in AttachNet", self._aminor) |
| |
| if None in (self._lhost, self._lport, self._rhost, self._rport): |
| base.ThrowError("drbd%d: missing network info in AttachNet()", self.minor) |
| |
| status = self.GetProcStatus() |
| |
| if not status.is_standalone: |
| base.ThrowError("drbd%d: device is not standalone in AttachNet", |
| self.minor) |
| |
| self._AssembleNet(self.minor, |
| (self._lhost, self._lport, self._rhost, self._rport), |
| dual_pri=multimaster, hmac=constants.DRBD_HMAC_ALG, |
| secret=self._secret) |
| |
| def Attach(self): |
| """Check if our minor is configured. |
| |
| This doesn't do any device configurations - it only checks if the |
| minor is in a state different from Unconfigured. |
| |
| Note that this function will not change the state of the system in |
| any way (except in case of side-effects caused by reading from |
| /proc). |
| |
| """ |
| used_devs = DRBD8.GetUsedDevs() |
| if self._aminor in used_devs: |
| minor = self._aminor |
| else: |
| minor = None |
| |
| self._SetFromMinor(minor) |
| return minor is not None |
| |
| def Assemble(self): |
| """Assemble the drbd. |
| |
| Method: |
| - if we have a configured device, we try to ensure that it matches |
| our config |
| - if not, we create it from zero |
| - anyway, set the device parameters |
| |
| """ |
| super(DRBD8Dev, self).Assemble() |
| |
| self.Attach() |
| if self.minor is None: |
| # local device completely unconfigured |
| self._FastAssemble() |
| else: |
| # we have to recheck the local and network status and try to fix |
| # the device |
| self._SlowAssemble() |
| |
| sync_errors = self.SetSyncParams(self.params) |
| if sync_errors: |
| base.ThrowError("drbd%d: can't set the synchronization parameters: %s" % |
| (self.minor, utils.CommaJoin(sync_errors))) |
| |
| def _SlowAssemble(self): |
| """Assembles the DRBD device from a (partially) configured device. |
| |
| In case of partially attached (local device matches but no network |
| setup), we perform the network attach. If successful, we re-test |
| the attach if can return success. |
| |
| """ |
| # TODO: Rewrite to not use a for loop just because there is 'break' |
| # pylint: disable=W0631 |
| net_data = (self._lhost, self._lport, self._rhost, self._rport) |
| for minor in (self._aminor,): |
| info = self._GetShowInfo(minor) |
| match_l = self._MatchesLocal(info) |
| match_r = self._MatchesNet(info) |
| |
| if match_l and match_r: |
| # everything matches |
| break |
| |
| if match_l and not match_r and "local_addr" not in info: |
| # disk matches, but not attached to network, attach and recheck |
| self._AssembleNet(minor, net_data, hmac=constants.DRBD_HMAC_ALG, |
| secret=self._secret) |
| if self._MatchesNet(self._GetShowInfo(minor)): |
| break |
| else: |
| base.ThrowError("drbd%d: network attach successful, but 'drbdsetup" |
| " show' disagrees", minor) |
| |
| if match_r and "local_dev" not in info: |
| # no local disk, but network attached and it matches |
| self._AssembleLocal(minor, self._children[0].dev_path, |
| self._children[1].dev_path, self.size) |
| if self._MatchesLocal(self._GetShowInfo(minor)): |
| break |
| else: |
| base.ThrowError("drbd%d: disk attach successful, but 'drbdsetup" |
| " show' disagrees", minor) |
| |
| # this case must be considered only if we actually have local |
| # storage, i.e. not in diskless mode, because all diskless |
| # devices are equal from the point of view of local |
| # configuration |
| if (match_l and "local_dev" in info and |
| not match_r and "local_addr" in info): |
| # strange case - the device network part points to somewhere |
| # else, even though its local storage is ours; as we own the |
| # drbd space, we try to disconnect from the remote peer and |
| # reconnect to our correct one |
| try: |
| self._ShutdownNet(minor) |
| except errors.BlockDeviceError, err: |
| base.ThrowError("drbd%d: device has correct local storage, wrong" |
| " remote peer and is unable to disconnect in order" |
| " to attach to the correct peer: %s", minor, str(err)) |
| # note: _AssembleNet also handles the case when we don't want |
| # local storage (i.e. one or more of the _[lr](host|port) is |
| # None) |
| self._AssembleNet(minor, net_data, hmac=constants.DRBD_HMAC_ALG, |
| secret=self._secret) |
| if self._MatchesNet(self._GetShowInfo(minor)): |
| break |
| else: |
| base.ThrowError("drbd%d: network attach successful, but 'drbdsetup" |
| " show' disagrees", minor) |
| |
| else: |
| minor = None |
| |
| self._SetFromMinor(minor) |
| if minor is None: |
| base.ThrowError("drbd%d: cannot activate, unknown or unhandled reason", |
| self._aminor) |
| |
| def _FastAssemble(self): |
| """Assemble the drbd device from zero. |
| |
| This is run when in Assemble we detect our minor is unused. |
| |
| """ |
| minor = self._aminor |
| if self._children and self._children[0] and self._children[1]: |
| self._AssembleLocal(minor, self._children[0].dev_path, |
| self._children[1].dev_path, self.size) |
| if self._lhost and self._lport and self._rhost and self._rport: |
| self._AssembleNet(minor, |
| (self._lhost, self._lport, self._rhost, self._rport), |
| hmac=constants.DRBD_HMAC_ALG, secret=self._secret) |
| self._SetFromMinor(minor) |
| |
| def _ShutdownLocal(self, minor): |
| """Detach from the local device. |
| |
| I/Os will continue to be served from the remote device. If we |
| don't have a remote device, this operation will fail. |
| |
| @type minor: int |
| @param minor: the device to detach from the local device |
| |
| """ |
| cmd = self._cmd_gen.GenDetachCmd(minor) |
| result = utils.RunCmd(cmd) |
| if result.failed: |
| base.ThrowError("drbd%d: can't detach local disk: %s", |
| minor, result.output) |
| |
| def _ShutdownNet(self, minor): |
| """Disconnect from the remote peer. |
| |
| This fails if we don't have a local device. |
| |
| @type minor: boolean |
| @param minor: the device to disconnect from the remote peer |
| |
| """ |
| family = self._GetNetFamily(minor, self._lhost, self._rhost) |
| cmd = self._cmd_gen.GenDisconnectCmd(minor, family, |
| self._lhost, self._lport, |
| self._rhost, self._rport) |
| result = utils.RunCmd(cmd) |
| if result.failed: |
| base.ThrowError("drbd%d: can't shutdown network: %s", |
| minor, result.output) |
| |
| def Shutdown(self): |
| """Shutdown the DRBD device. |
| |
| """ |
| if self.minor is None and not self.Attach(): |
| logging.info("drbd%d: not attached during Shutdown()", self._aminor) |
| return |
| |
| try: |
| DRBD8.ShutdownAll(self.minor) |
| finally: |
| self.minor = None |
| self.dev_path = None |
| |
| def Remove(self): |
| """Stub remove for DRBD devices. |
| |
| """ |
| self.Shutdown() |
| |
| def Rename(self, new_id): |
| """Rename a device. |
| |
| This is not supported for drbd devices. |
| |
| """ |
| raise errors.ProgrammerError("Can't rename a drbd device") |
| |
| def Grow(self, amount, dryrun, backingstore, excl_stor): |
| """Resize the DRBD device and its backing storage. |
| |
| See L{BlockDev.Grow} for parameter description. |
| |
| """ |
| if self.minor is None: |
| base.ThrowError("drbd%d: Grow called while not attached", self._aminor) |
| if len(self._children) != 2 or None in self._children: |
| base.ThrowError("drbd%d: cannot grow diskless device", self.minor) |
| self._children[0].Grow(amount, dryrun, backingstore, excl_stor) |
| if dryrun or backingstore: |
| # DRBD does not support dry-run mode and is not backing storage, |
| # so we'll return here |
| return |
| cmd = self._cmd_gen.GenResizeCmd(self.minor, self.size + amount) |
| result = utils.RunCmd(cmd) |
| if result.failed: |
| base.ThrowError("drbd%d: resize failed: %s", self.minor, result.output) |
| |
| @classmethod |
| def _InitMeta(cls, minor, dev_path): |
| """Initialize a meta device. |
| |
| This will not work if the given minor is in use. |
| |
| @type minor: int |
| @param minor: the DRBD minor whose (future) meta device should be |
| initialized |
| @type dev_path: string |
| @param dev_path: path to the meta device to initialize |
| |
| """ |
| # Zero the metadata first, in order to make sure drbdmeta doesn't |
| # try to auto-detect existing filesystems or similar (see |
| # http://code.google.com/p/ganeti/issues/detail?id=182); we only |
| # care about the first 128MB of data in the device, even though it |
| # can be bigger |
| result = utils.RunCmd([constants.DD_CMD, |
| "if=/dev/zero", "of=%s" % dev_path, |
| "bs=1048576", "count=128", "oflag=direct"]) |
| if result.failed: |
| base.ThrowError("Can't wipe the meta device: %s", result.output) |
| |
| info = DRBD8.GetProcInfo() |
| cmd_gen = DRBD8.GetCmdGenerator(info) |
| cmd = cmd_gen.GenInitMetaCmd(minor, dev_path) |
| |
| result = utils.RunCmd(cmd) |
| if result.failed: |
| base.ThrowError("Can't initialize meta device: %s", result.output) |
| |
| @classmethod |
| def Create(cls, unique_id, children, size, spindles, params, excl_stor): |
| """Create a new DRBD8 device. |
| |
| Since DRBD devices are not created per se, just assembled, this |
| function only initializes the metadata. |
| |
| """ |
| if len(children) != 2: |
| raise errors.ProgrammerError("Invalid setup for the drbd device") |
| if excl_stor: |
| raise errors.ProgrammerError("DRBD device requested with" |
| " exclusive_storage") |
| # check that the minor is unused |
| aminor = unique_id[4] |
| |
| info = DRBD8.GetProcInfo() |
| if info.HasMinorStatus(aminor): |
| status = info.GetMinorStatus(aminor) |
| in_use = status.is_in_use |
| else: |
| in_use = False |
| if in_use: |
| base.ThrowError("drbd%d: minor is already in use at Create() time", |
| aminor) |
| meta = children[1] |
| meta.Assemble() |
| if not meta.Attach(): |
| base.ThrowError("drbd%d: can't attach to meta device '%s'", |
| aminor, meta) |
| cls._CheckMetaSize(meta.dev_path) |
| cls._InitMeta(aminor, meta.dev_path) |
| return cls(unique_id, children, size, params) |
| |
| |
| def _CanReadDevice(path): |
| """Check if we can read from the given device. |
| |
| This tries to read the first 128k of the device. |
| |
| @type path: string |
| |
| """ |
| try: |
| utils.ReadFile(path, size=_DEVICE_READ_SIZE) |
| return True |
| except EnvironmentError: |
| logging.warning("Can't read from device %s", path, exc_info=True) |
| return False |