Commit a2cfdea2 authored by Iustin Pop's avatar Iustin Pop

Add DRBD8 class for handling drbd version 8.x

This duplicates some code from the DRBDev class, but not very much, and
it will be expanded with the new functionality available for the 8.x
version. Currently the code is not accessible outside the module.

This patch introduces a dependency on the pyparsing module.

Reviewed-by: imsnah
parent ae26a287
......@@ -36,6 +36,8 @@ Before installing, please verify that you have the following programs:
http://pyopenssl.sourceforge.net/
- simplejson Python module
http://www.undefined.org/python/#simplejson
- pyparsing Python module
http://pyparsing.wikispaces.com/
For testing, you also need the YAML module for Python (http://pyyaml.org/).
......
......@@ -408,6 +408,11 @@ skip resource "r1" {
url="http://www.undefined.org/python/#simplejson">simplejson Python
module</ulink></simpara>
</listitem>
<listitem>
<simpara><ulink
url="http://pyparsing.wikispaces.com/">pyparsing Python
module</ulink></simpara>
</listitem>
</itemizedlist>
<para>
......
......@@ -24,6 +24,7 @@
import re
import time
import errno
import pyparsing as pyp
from ganeti import utils
from ganeti import logger
......@@ -1549,6 +1550,535 @@ class DRBDev(BaseDRBD):
"""
return self.Shutdown()
class DRBD8(BaseDRBD):
"""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 the (local_ip, local_port,
remote_ip, remote_port) 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
_MAX_MINORS = 255
_PARSE_SHOW = None
def __init__(self, unique_id, children):
super(DRBD8, self).__init__(unique_id, children)
self.major = self._DRBD_MAJOR
[kmaj, kmin, kfix, api, proto] = self._GetVersion()
if kmaj != 8:
raise errors.BlockDeviceError("Mismatch in DRBD kernel version and"
" requested ganeti usage: kernel is"
" %s.%s, ganeti wants 8.x" % (kmaj, kmin))
if len(children) != 2:
raise ValueError("Invalid configuration data %s" % str(children))
if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 4:
raise ValueError("Invalid configuration data %s" % str(unique_id))
self._lhost, self._lport, self._rhost, self._rport = unique_id
self.Attach()
@classmethod
def _InitMeta(cls, minor, dev_path):
"""Initialize a meta device.
This will not work if the given minor is in use.
"""
result = utils.RunCmd(["drbdmeta", "--force", cls._DevPath(minor),
"v08", dev_path, "0", "create-md"])
if result.failed:
raise errors.BlockDeviceError("Can't initialize meta device: %s" %
result.output)
@classmethod
def _FindUnusedMinor(cls):
"""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.
"""
data = cls._GetProcData()
unused_line = re.compile("^ *([0-9]+): cs:Unconfigured$")
used_line = re.compile("^ *([0-9]+): cs:")
highest = None
for line in data:
match = unused_line.match(line)
if match:
return int(match.group(1))
match = used_line.match(line)
if match:
minor = int(match.group(1))
highest = max(highest, minor)
if highest is None: # there are no minors in use at all
return 0
if highest >= cls._MAX_MINORS:
logger.Error("Error: no free drbd minors!")
raise errors.BlockDeviceError("Can't find a free DRBD minor")
return highest + 1
@classmethod
def _IsValidMeta(cls, meta_device):
"""Check if the given meta device looks like a valid one.
"""
minor = cls._FindUnusedMinor()
minor_path = cls._DevPath(minor)
result = utils.RunCmd(["drbdmeta", minor_path,
"v08", meta_device, "0",
"dstate"])
if result.failed:
logger.Error("Invalid meta device %s: %s" % (meta_device, result.output))
return False
return True
@classmethod
def _GetShowParser(cls):
"""Return a parser for `drbd show` output.
This will either create or return an already-create parser for the
output of the command `drbd show`.
"""
if cls._PARSE_SHOW is not None:
return cls._PARSE_SHOW
# pyparsing setup
lbrace = pyp.Literal("{").suppress()
rbrace = pyp.Literal("}").suppress()
semi = pyp.Literal(";").suppress()
# this also converts the value to an int
number = pyp.Word(pyp.nums).setParseAction(lambda s, l, t:(l, [int(t[0])]))
comment = pyp.Literal ("#") + pyp.Optional(pyp.restOfLine)
defa = pyp.Literal("_is_default").suppress()
dbl_quote = pyp.Literal('"').suppress()
keyword = pyp.Word(pyp.alphanums + '-')
# value types
value = pyp.Word(pyp.alphanums + '_-/.:')
quoted = dbl_quote + pyp.CharsNotIn('"') + dbl_quote
addr_port = (pyp.Word(pyp.nums + '.') + pyp.Literal(':').suppress() +
number)
# meta device, extended syntax
meta_value = ((value ^ quoted) + pyp.Literal('[').suppress() +
number + pyp.Word(']').suppress())
# a statement
stmt = (~rbrace + keyword + ~lbrace +
(addr_port ^ value ^ quoted ^ meta_value) +
pyp.Optional(defa) + semi +
pyp.Optional(pyp.restOfLine).suppress())
# an entire section
section_name = pyp.Word(pyp.alphas + '_')
section = section_name + lbrace + pyp.ZeroOrMore(pyp.Group(stmt)) + rbrace
bnf = pyp.ZeroOrMore(pyp.Group(section ^ stmt))
bnf.ignore(comment)
cls._PARSE_SHOW = bnf
return bnf
@classmethod
def _GetDevInfo(cls, minor):
"""Get details about a given DRBD minor.
This return, if available, the local backing device (as a path)
and the local and remote (ip, port) information.
"""
data = {}
result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "show"])
if result.failed:
logger.Error("Can't display the drbd config: %s" % result.fail_reason)
return data
out = result.stdout
if not out:
return data
bnf = cls._GetShowParser()
# run pyparse
try:
results = bnf.parseString(out)
except pyp.ParseException, err:
raise errors.BlockDeviceError("Can't parse drbdsetup show output: %s" %
str(err))
# and massage the results into our desired format
for section in results:
sname = section[0]
if sname == "_this_host":
for lst in section[1:]:
if lst[0] == "disk":
data["local_dev"] = lst[1]
elif lst[0] == "meta-disk":
data["meta_dev"] = lst[1]
data["meta_index"] = lst[2]
elif lst[0] == "address":
data["local_addr"] = tuple(lst[1:])
elif sname == "_remote_host":
for lst in section[1:]:
if lst[0] == "address":
data["remote_addr"] = tuple(lst[1:])
return data
def _MatchesLocal(self, info):
"""Test if our local config matches with an existing device.
The parameter should be as returned from `_GetDevInfo()`. 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.
"""
backend = self._children[0]
if backend is not None:
retval = (info["local_dev"] == backend.dev_path)
else:
retval = ("local_dev" not in info)
meta = self._children[1]
if meta is not None:
retval = retval and (info["meta_dev"] == meta.dev_path)
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 `_GetDevInfo()`. 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.
"""
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
@classmethod
def _AssembleLocal(cls, minor, backend, meta):
"""Configure the local part of a DRBD device.
This is the first thing that must be done on an unconfigured DRBD
device. And it must be done only once.
"""
if not cls._IsValidMeta(meta):
return False
result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "disk",
backend, meta, "0", "-e", "detach",
"--create-device"])
if result.failed:
logger.Error("Can't attach local disk: %s" % result.output)
return not result.failed
@classmethod
def _AssembleNet(cls, minor, net_info, protocol,
dual_pri=False, hmac=None, secret=None):
"""Configure the network part of the device.
"""
lhost, lport, rhost, rport = net_info
args = ["drbdsetup", cls._DevPath(minor), "net",
"%s:%s" % (lhost, lport), "%s:%s" % (rhost, rport),
protocol]
if dual_pri:
args.append("-m")
if hmac and secret:
args.extend(["-a", hmac, "-x", secret])
result = utils.RunCmd(args)
if result.failed:
logger.Error("Can't setup network for dbrd device: %s" %
result.fail_reason)
return False
timeout = time.time() + 10
ok = False
while time.time() < timeout:
info = cls._GetDevInfo(minor)
if not "local_addr" in info or not "remote_addr" in info:
time.sleep(1)
continue
if (info["local_addr"] != (lhost, lport) or
info["remote_addr"] != (rhost, rport)):
time.sleep(1)
continue
ok = True
break
if not ok:
logger.Error("Timeout while configuring network")
return False
return True
def SetSyncSpeed(self, kbytes):
"""Set the speed of the DRBD syncer.
"""
children_result = super(DRBD8, self).SetSyncSpeed(kbytes)
if self.minor is None:
logger.Info("Instance not attached to a device")
return False
result = utils.RunCmd(["drbdsetup", self.dev_path, "syncer", "-r", "%d" %
kbytes])
if result.failed:
logger.Error("Can't change syncer rate: %s " % result.fail_reason)
return not result.failed and children_result
def GetSyncStatus(self):
"""Returns the sync status of the device.
Returns:
(sync_percent, estimated_time)
If sync_percent is None, it means all is ok
If estimated_time is None, it means we can't esimate
the time needed, otherwise it's the time left in seconds
"""
if self.minor is None and not self.Attach():
raise errors.BlockDeviceError("Can't attach to device in GetSyncStatus")
proc_info = self._MassageProcData(self._GetProcData())
if self.minor not in proc_info:
raise errors.BlockDeviceError("Can't find myself in /proc (minor %d)" %
self.minor)
line = proc_info[self.minor]
match = re.match("^.*sync'ed: *([0-9.]+)%.*"
" finish: ([0-9]+):([0-9]+):([0-9]+) .*$", line)
if match:
sync_percent = float(match.group(1))
hours = int(match.group(2))
minutes = int(match.group(3))
seconds = int(match.group(4))
est_time = hours * 3600 + minutes * 60 + seconds
else:
sync_percent = None
est_time = None
match = re.match("^ *[0-9]+: cs:([^ ]+).*$", line)
if not match:
raise errors.BlockDeviceError("Can't find my data in /proc (minor %d)" %
self.minor)
client_state = match.group(1)
is_degraded = client_state != "Connected"
return sync_percent, est_time, is_degraded
def GetStatus(self):
"""Compute the status of the DRBD device
Note that DRBD devices don't have the STATUS_EXISTING state.
"""
if self.minor is None and not self.Attach():
return self.STATUS_UNKNOWN
data = self._GetProcData()
match = re.compile("^ *%d: cs:[^ ]+ st:(Primary|Secondary)/.*$" %
self.minor)
for line in data:
mresult = match.match(line)
if mresult:
break
else:
logger.Error("Can't find myself!")
return self.STATUS_UNKNOWN
state = mresult.group(2)
if state == "Primary":
result = self.STATUS_ONLINE
else:
result = self.STATUS_STANDBY
return result
def Open(self, force=False):
"""Make the local state primary.
If the 'force' parameter is given, the '--do-what-I-say' parameter
is given. Since this is a pottentialy dangerous operation, the
force flag should be only given after creation, when it actually
has to be given.
"""
if self.minor is None and not self.Attach():
logger.Error("DRBD cannot attach to a device during open")
return False
cmd = ["drbdsetup", self.dev_path, "primary"]
if force:
cmd.append("-o")
result = utils.RunCmd(cmd)
if result.failed:
logger.Error("Can't make drbd device primary: %s" % result.output)
return False
return True
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():
logger.Info("Instance not attached to a device")
raise errors.BlockDeviceError("Can't find device")
result = utils.RunCmd(["drbdsetup", self.dev_path, "secondary"])
if result.failed:
logger.Error("Can't switch drbd device to secondary: %s" % result.output)
raise errors.BlockDeviceError("Can't switch drbd device to secondary")
def Attach(self):
"""Find a DRBD device which matches our config and attach to it.
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.
"""
for minor in self._GetUsedDevs():
info = self._GetDevInfo(minor)
match_l = self._MatchesLocal(info)
match_r = self._MatchesNet(info)
if match_l and match_r:
break
if match_l and not match_r and "local_addr" not in info:
res_r = self._AssembleNet(minor,
(self._lhost, self._lport,
self._rhost, self._rport),
"C")
if res_r and self._MatchesNet(self._GetDevInfo(minor)):
break
else:
minor = None
self._SetFromMinor(minor)
return minor is not None
def Assemble(self):
"""Assemble the drbd.
Method:
- if we have a local backing device, we bind to it by:
- checking the list of used drbd devices
- check if the local minor use of any of them is our own device
- if yes, abort?
- if not, bind
- if we have a local/remote net info:
- redo the local backing device step for the remote device
- check if any drbd device is using the local port,
if yes abort
- check if any remote drbd device is using the remote
port, if yes abort (for now)
- bind our net port
- bind the remote net port
"""
self.Attach()
if self.minor is not None:
logger.Info("Already assembled")
return True
result = super(DRBD8, self).Assemble()
if not result:
return result
minor = self._FindUnusedMinor()
need_localdev_teardown = False
if self._children[0]:
result = self._AssembleLocal(minor, self._children[0].dev_path,
self._children[1].dev_path)
if not result:
return False
need_localdev_teardown = True
if self._lhost and self._lport and self._rhost and self._rport:
result = self._AssembleNet(minor,
(self._lhost, self._lport,
self._rhost, self._rport),
"C")
if not result:
if need_localdev_teardown:
# we will ignore failures from this
logger.Error("net setup failed, tearing down local device")
self._ShutdownAll(minor)
return False
self._SetFromMinor(minor)
return True
@classmethod
def _ShutdownAll(cls, minor):
"""Deactivate the device.
This will, of course, fail if the device is in use.
"""
result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "down"])
if result.failed:
logger.Error("Can't shutdown drbd device: %s" % result.output)
return not result.failed
def Shutdown(self):
"""Shutdown the DRBD device.
"""
if self.minor is None and not self.Attach():
logger.Info("DRBD device not attached to a device during Shutdown")
return True
if not self._ShutdownAll(self.minor):
return False
self.minor = None
self.dev_path = None
return True
def Remove(self):
"""Stub remove for DRBD devices.
"""
return self.Shutdown()
@classmethod
def Create(cls, unique_id, children, size):
"""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")
meta = children[1]
meta.Assemble()
if not meta.Attach():
raise errors.BlockDeviceError("Can't attach to meta device")
if not cls._CheckMetaSize(meta.dev_path):
raise errors.BlockDeviceError("Invalid meta device size")
cls._InitMeta(cls._FindUnusedMinor(), meta.dev_path)
if not cls._IsValidMeta(meta.dev_path):
raise errors.BlockDeviceError("Cannot initalize meta device")
return cls(unique_id, children)
DEV_MAP = {
constants.LD_LV: LogicalVolume,
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment