| # |
| # |
| |
| # 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 information parsing utilities""" |
| |
| import errno |
| import pyparsing as pyp |
| import re |
| |
| from ganeti import constants |
| from ganeti import utils |
| from ganeti import errors |
| from ganeti import compat |
| from ganeti.storage import base |
| |
| |
| class DRBD8Status(object): # pylint: disable=R0902 |
| """A DRBD status representation class. |
| |
| Note that this class is meant to be used to parse one of the entries returned |
| from L{DRBD8Info._JoinLinesPerMinor}. |
| |
| """ |
| UNCONF_RE = re.compile(r"\s*[0-9]+:\s*cs:Unconfigured$") |
| LINE_RE = re.compile(r"\s*[0-9]+:\s*cs:(\S+)\s+(?:st|ro):([^/]+)/(\S+)" |
| r"\s+ds:([^/]+)/(\S+)\s+.*$") |
| SYNC_RE = re.compile(r"^.*\ssync'ed:\s*([0-9.]+)%.*" |
| # Due to a bug in drbd in the kernel, introduced in |
| # commit 4b0715f096 (still unfixed as of 2011-08-22) |
| r"(?:\s|M)" |
| r"finish: ([0-9]+):([0-9]+):([0-9]+)\s.*$") |
| |
| CS_UNCONFIGURED = "Unconfigured" |
| CS_STANDALONE = "StandAlone" |
| CS_WFCONNECTION = "WFConnection" |
| CS_WFREPORTPARAMS = "WFReportParams" |
| CS_CONNECTED = "Connected" |
| CS_STARTINGSYNCS = "StartingSyncS" |
| CS_STARTINGSYNCT = "StartingSyncT" |
| CS_WFBITMAPS = "WFBitMapS" |
| CS_WFBITMAPT = "WFBitMapT" |
| CS_WFSYNCUUID = "WFSyncUUID" |
| CS_SYNCSOURCE = "SyncSource" |
| CS_SYNCTARGET = "SyncTarget" |
| CS_PAUSEDSYNCS = "PausedSyncS" |
| CS_PAUSEDSYNCT = "PausedSyncT" |
| CSET_SYNC = compat.UniqueFrozenset([ |
| CS_WFREPORTPARAMS, |
| CS_STARTINGSYNCS, |
| CS_STARTINGSYNCT, |
| CS_WFBITMAPS, |
| CS_WFBITMAPT, |
| CS_WFSYNCUUID, |
| CS_SYNCSOURCE, |
| CS_SYNCTARGET, |
| CS_PAUSEDSYNCS, |
| CS_PAUSEDSYNCT, |
| ]) |
| |
| DS_DISKLESS = "Diskless" |
| DS_ATTACHING = "Attaching" # transient state |
| DS_FAILED = "Failed" # transient state, next: diskless |
| DS_NEGOTIATING = "Negotiating" # transient state |
| DS_INCONSISTENT = "Inconsistent" # while syncing or after creation |
| DS_OUTDATED = "Outdated" |
| DS_DUNKNOWN = "DUnknown" # shown for peer disk when not connected |
| DS_CONSISTENT = "Consistent" |
| DS_UPTODATE = "UpToDate" # normal state |
| |
| RO_PRIMARY = "Primary" |
| RO_SECONDARY = "Secondary" |
| RO_UNKNOWN = "Unknown" |
| |
| def __init__(self, procline): |
| u = self.UNCONF_RE.match(procline) |
| if u: |
| self.cstatus = self.CS_UNCONFIGURED |
| self.lrole = self.rrole = self.ldisk = self.rdisk = None |
| else: |
| m = self.LINE_RE.match(procline) |
| if not m: |
| raise errors.BlockDeviceError("Can't parse input data '%s'" % procline) |
| self.cstatus = m.group(1) |
| self.lrole = m.group(2) |
| self.rrole = m.group(3) |
| self.ldisk = m.group(4) |
| self.rdisk = m.group(5) |
| |
| # end reading of data from the LINE_RE or UNCONF_RE |
| |
| self.is_standalone = self.cstatus == self.CS_STANDALONE |
| self.is_wfconn = self.cstatus == self.CS_WFCONNECTION |
| self.is_connected = self.cstatus == self.CS_CONNECTED |
| self.is_unconfigured = self.cstatus == self.CS_UNCONFIGURED |
| self.is_primary = self.lrole == self.RO_PRIMARY |
| self.is_secondary = self.lrole == self.RO_SECONDARY |
| self.peer_primary = self.rrole == self.RO_PRIMARY |
| self.peer_secondary = self.rrole == self.RO_SECONDARY |
| self.both_primary = self.is_primary and self.peer_primary |
| self.both_secondary = self.is_secondary and self.peer_secondary |
| |
| self.is_diskless = self.ldisk == self.DS_DISKLESS |
| self.is_disk_uptodate = self.ldisk == self.DS_UPTODATE |
| self.peer_disk_uptodate = self.rdisk == self.DS_UPTODATE |
| |
| self.is_in_resync = self.cstatus in self.CSET_SYNC |
| self.is_in_use = self.cstatus != self.CS_UNCONFIGURED |
| |
| m = self.SYNC_RE.match(procline) |
| if m: |
| self.sync_percent = float(m.group(1)) |
| hours = int(m.group(2)) |
| minutes = int(m.group(3)) |
| seconds = int(m.group(4)) |
| self.est_time = hours * 3600 + minutes * 60 + seconds |
| else: |
| # we have (in this if branch) no percent information, but if |
| # we're resyncing we need to 'fake' a sync percent information, |
| # as this is how cmdlib determines if it makes sense to wait for |
| # resyncing or not |
| if self.is_in_resync: |
| self.sync_percent = 0 |
| else: |
| self.sync_percent = None |
| self.est_time = None |
| |
| |
| class DRBD8Info(object): |
| """Represents information DRBD exports (usually via /proc/drbd). |
| |
| An instance of this class is created by one of the CreateFrom... methods. |
| |
| """ |
| |
| _VERSION_RE = re.compile(r"^version: (\d+)\.(\d+)\.(\d+)(?:\.(\d+))?" |
| r" \(api:(\d+)/proto:(\d+)(?:-(\d+))?\)") |
| _VALID_LINE_RE = re.compile("^ *([0-9]+): cs:([^ ]+).*$") |
| |
| def __init__(self, lines): |
| self._version = self._ParseVersion(lines) |
| self._minors, self._line_per_minor = self._JoinLinesPerMinor(lines) |
| |
| def GetVersion(self): |
| """Return the DRBD version. |
| |
| This will return a dict with keys: |
| - k_major |
| - k_minor |
| - k_point |
| - k_fix (only on some drbd versions) |
| - api |
| - proto |
| - proto2 (only on drbd > 8.2.X) |
| |
| """ |
| return self._version |
| |
| def GetVersionString(self): |
| """Return the DRBD version as a single string. |
| |
| """ |
| version = self.GetVersion() |
| retval = "%d.%d.%d" % \ |
| (version["k_major"], version["k_minor"], version["k_point"]) |
| if "k_fix" in version: |
| retval += ".%s" % version["k_fix"] |
| |
| retval += " (api:%d/proto:%d" % (version["api"], version["proto"]) |
| if "proto2" in version: |
| retval += "-%s" % version["proto2"] |
| retval += ")" |
| return retval |
| |
| def GetMinors(self): |
| """Return a list of minor for which information is available. |
| |
| This list is ordered in exactly the order which was found in the underlying |
| data. |
| |
| """ |
| return self._minors |
| |
| def HasMinorStatus(self, minor): |
| return minor in self._line_per_minor |
| |
| def GetMinorStatus(self, minor): |
| return DRBD8Status(self._line_per_minor[minor]) |
| |
| def _ParseVersion(self, lines): |
| first_line = lines[0].strip() |
| version = self._VERSION_RE.match(first_line) |
| if not version: |
| raise errors.BlockDeviceError("Can't parse DRBD version from '%s'" % |
| first_line) |
| |
| values = version.groups() |
| retval = { |
| "k_major": int(values[0]), |
| "k_minor": int(values[1]), |
| "k_point": int(values[2]), |
| "api": int(values[4]), |
| "proto": int(values[5]), |
| } |
| if values[3] is not None: |
| retval["k_fix"] = values[3] |
| if values[6] is not None: |
| retval["proto2"] = values[6] |
| |
| return retval |
| |
| def _JoinLinesPerMinor(self, lines): |
| """Transform the raw lines into a dictionary based on the minor. |
| |
| @return: a dictionary of minor: joined lines from /proc/drbd |
| for that minor |
| |
| """ |
| minors = [] |
| results = {} |
| old_minor = old_line = None |
| for line in lines: |
| if not line: # completely empty lines, as can be returned by drbd8.0+ |
| continue |
| lresult = self._VALID_LINE_RE.match(line) |
| if lresult is not None: |
| if old_minor is not None: |
| minors.append(old_minor) |
| results[old_minor] = old_line |
| old_minor = int(lresult.group(1)) |
| old_line = line |
| else: |
| if old_minor is not None: |
| old_line += " " + line.strip() |
| # add last line |
| if old_minor is not None: |
| minors.append(old_minor) |
| results[old_minor] = old_line |
| return minors, results |
| |
| @staticmethod |
| def CreateFromLines(lines): |
| return DRBD8Info(lines) |
| |
| @staticmethod |
| def CreateFromFile(filename=constants.DRBD_STATUS_FILE): |
| try: |
| lines = utils.ReadFile(filename).splitlines() |
| 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 the DRBD proc file %s: %s", |
| filename, str(err)) |
| if not lines: |
| base.ThrowError("Can't read any data from %s", filename) |
| return DRBD8Info.CreateFromLines(lines) |
| |
| |
| class BaseShowInfo(object): |
| """Base class for parsing the `drbdsetup show` output. |
| |
| Holds various common pyparsing expressions which are used by subclasses. Also |
| provides caching of the constructed parser. |
| |
| """ |
| _PARSE_SHOW = None |
| |
| # pyparsing setup |
| _lbrace = pyp.Literal("{").suppress() |
| _rbrace = pyp.Literal("}").suppress() |
| _lbracket = pyp.Literal("[").suppress() |
| _rbracket = pyp.Literal("]").suppress() |
| _semi = pyp.Literal(";").suppress() |
| _colon = pyp.Literal(":").suppress() |
| # this also converts the value to an int |
| _number = pyp.Word(pyp.nums).setParseAction(lambda s, l, t: 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 |
| _ipv4_addr = (pyp.Optional(pyp.Literal("ipv4")).suppress() + |
| pyp.Word(pyp.nums + ".") + _colon + _number) |
| _ipv6_addr = (pyp.Optional(pyp.Literal("ipv6")).suppress() + |
| pyp.Optional(_lbracket) + pyp.Word(pyp.hexnums + ":") + |
| pyp.Optional(_rbracket) + _colon + _number) |
| # meta device, extended syntax |
| _meta_value = ((_value ^ _quoted) + _lbracket + _number + _rbracket) |
| # device name, extended syntax |
| _device_value = pyp.Literal("minor").suppress() + _number |
| |
| # a statement |
| _stmt = (~_rbrace + _keyword + ~_lbrace + |
| pyp.Optional(_ipv4_addr ^ _ipv6_addr ^ _value ^ _quoted ^ |
| _meta_value ^ _device_value) + |
| pyp.Optional(_defa) + _semi + |
| pyp.Optional(pyp.restOfLine).suppress()) |
| |
| @classmethod |
| def GetDevInfo(cls, show_data): |
| """Parse details about a given DRBD minor. |
| |
| This returns, if available, the local backing device (as a path) |
| and the local and remote (ip, port) information from a string |
| containing the output of the `drbdsetup show` command as returned |
| by DRBD8Dev._GetShowData. |
| |
| This will return a dict with keys: |
| - local_dev |
| - meta_dev |
| - meta_index |
| - local_addr |
| - remote_addr |
| |
| """ |
| if not show_data: |
| return {} |
| |
| try: |
| # run pyparse |
| results = (cls._GetShowParser()).parseString(show_data) |
| except pyp.ParseException, err: |
| base.ThrowError("Can't parse drbdsetup show output: %s", str(err)) |
| |
| return cls._TransformParseResult(results) |
| |
| @classmethod |
| def _TransformParseResult(cls, parse_result): |
| raise NotImplementedError |
| |
| @classmethod |
| def _GetShowParser(cls): |
| """Return a parser for `drbd show` output. |
| |
| This will either create or return an already-created parser for the |
| output of the command `drbd show`. |
| |
| """ |
| if cls._PARSE_SHOW is None: |
| cls._PARSE_SHOW = cls._ConstructShowParser() |
| |
| return cls._PARSE_SHOW |
| |
| @classmethod |
| def _ConstructShowParser(cls): |
| raise NotImplementedError |
| |
| |
| class DRBD83ShowInfo(BaseShowInfo): |
| @classmethod |
| def _ConstructShowParser(cls): |
| # an entire section |
| section_name = pyp.Word(pyp.alphas + "_") |
| section = section_name + \ |
| cls._lbrace + \ |
| pyp.ZeroOrMore(pyp.Group(cls._stmt)) + \ |
| cls._rbrace |
| |
| bnf = pyp.ZeroOrMore(pyp.Group(section ^ cls._stmt)) |
| bnf.ignore(cls._comment) |
| |
| return bnf |
| |
| @classmethod |
| def _TransformParseResult(cls, parse_result): |
| retval = {} |
| for section in parse_result: |
| sname = section[0] |
| if sname == "_this_host": |
| for lst in section[1:]: |
| if lst[0] == "disk": |
| retval["local_dev"] = lst[1] |
| elif lst[0] == "meta-disk": |
| retval["meta_dev"] = lst[1] |
| retval["meta_index"] = lst[2] |
| elif lst[0] == "address": |
| retval["local_addr"] = tuple(lst[1:]) |
| elif sname == "_remote_host": |
| for lst in section[1:]: |
| if lst[0] == "address": |
| retval["remote_addr"] = tuple(lst[1:]) |
| return retval |
| |
| |
| class DRBD84ShowInfo(BaseShowInfo): |
| @classmethod |
| def _ConstructShowParser(cls): |
| # an entire section (sections can be nested in DRBD 8.4, and there exist |
| # sections like "volume 0") |
| section_name = pyp.Word(pyp.alphas + "_") + \ |
| pyp.Optional(pyp.Word(pyp.nums)).suppress() # skip volume idx |
| section = pyp.Forward() |
| # pylint: disable=W0106 |
| section << (section_name + |
| cls._lbrace + |
| pyp.ZeroOrMore(pyp.Group(cls._stmt ^ section)) + |
| cls._rbrace) |
| |
| resource_name = pyp.Word(pyp.alphanums + "_-.") |
| resource = (pyp.Literal("resource") + resource_name).suppress() + \ |
| cls._lbrace + \ |
| pyp.ZeroOrMore(pyp.Group(section)) + \ |
| cls._rbrace |
| |
| resource.ignore(cls._comment) |
| |
| return resource |
| |
| @classmethod |
| def _TransformVolumeSection(cls, vol_content, retval): |
| for entry in vol_content: |
| if entry[0] == "disk" and len(entry) == 2 and \ |
| isinstance(entry[1], basestring): |
| retval["local_dev"] = entry[1] |
| elif entry[0] == "meta-disk": |
| if len(entry) > 1: |
| retval["meta_dev"] = entry[1] |
| if len(entry) > 2: |
| retval["meta_index"] = entry[2] |
| |
| @classmethod |
| def _TransformParseResult(cls, parse_result): |
| retval = {} |
| for section in parse_result: |
| sname = section[0] |
| if sname == "_this_host": |
| for lst in section[1:]: |
| if lst[0] == "address": |
| retval["local_addr"] = tuple(lst[1:]) |
| elif lst[0] == "volume": |
| cls._TransformVolumeSection(lst[1:], retval) |
| elif sname == "_remote_host": |
| for lst in section[1:]: |
| if lst[0] == "address": |
| retval["remote_addr"] = tuple(lst[1:]) |
| return retval |