4 # Copyright (C) 2006, 2007, 2010, 2011, 2012, 2013 Google Inc.
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.
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.
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
22 """DRBD information parsing utilities"""
25 import pyparsing as pyp
28 from ganeti import constants
29 from ganeti import utils
30 from ganeti import errors
31 from ganeti import compat
32 from ganeti.block import base
35 class DRBD8Status(object): # pylint: disable=R0902
36 """A DRBD status representation class.
38 Note that this class is meant to be used to parse one of the entries returned
39 from L{DRBD8Info._JoinLinesPerMinor}.
42 UNCONF_RE = re.compile(r"\s*[0-9]+:\s*cs:Unconfigured$")
43 LINE_RE = re.compile(r"\s*[0-9]+:\s*cs:(\S+)\s+(?:st|ro):([^/]+)/(\S+)"
44 "\s+ds:([^/]+)/(\S+)\s+.*$")
45 SYNC_RE = re.compile(r"^.*\ssync'ed:\s*([0-9.]+)%.*"
46 # Due to a bug in drbd in the kernel, introduced in
47 # commit 4b0715f096 (still unfixed as of 2011-08-22)
49 "finish: ([0-9]+):([0-9]+):([0-9]+)\s.*$")
51 CS_UNCONFIGURED = "Unconfigured"
52 CS_STANDALONE = "StandAlone"
53 CS_WFCONNECTION = "WFConnection"
54 CS_WFREPORTPARAMS = "WFReportParams"
55 CS_CONNECTED = "Connected"
56 CS_STARTINGSYNCS = "StartingSyncS"
57 CS_STARTINGSYNCT = "StartingSyncT"
58 CS_WFBITMAPS = "WFBitMapS"
59 CS_WFBITMAPT = "WFBitMapT"
60 CS_WFSYNCUUID = "WFSyncUUID"
61 CS_SYNCSOURCE = "SyncSource"
62 CS_SYNCTARGET = "SyncTarget"
63 CS_PAUSEDSYNCS = "PausedSyncS"
64 CS_PAUSEDSYNCT = "PausedSyncT"
65 CSET_SYNC = compat.UniqueFrozenset([
78 DS_DISKLESS = "Diskless"
79 DS_ATTACHING = "Attaching" # transient state
80 DS_FAILED = "Failed" # transient state, next: diskless
81 DS_NEGOTIATING = "Negotiating" # transient state
82 DS_INCONSISTENT = "Inconsistent" # while syncing or after creation
83 DS_OUTDATED = "Outdated"
84 DS_DUNKNOWN = "DUnknown" # shown for peer disk when not connected
85 DS_CONSISTENT = "Consistent"
86 DS_UPTODATE = "UpToDate" # normal state
88 RO_PRIMARY = "Primary"
89 RO_SECONDARY = "Secondary"
90 RO_UNKNOWN = "Unknown"
92 def __init__(self, procline):
93 u = self.UNCONF_RE.match(procline)
95 self.cstatus = self.CS_UNCONFIGURED
96 self.lrole = self.rrole = self.ldisk = self.rdisk = None
98 m = self.LINE_RE.match(procline)
100 raise errors.BlockDeviceError("Can't parse input data '%s'" % procline)
101 self.cstatus = m.group(1)
102 self.lrole = m.group(2)
103 self.rrole = m.group(3)
104 self.ldisk = m.group(4)
105 self.rdisk = m.group(5)
107 # end reading of data from the LINE_RE or UNCONF_RE
109 self.is_standalone = self.cstatus == self.CS_STANDALONE
110 self.is_wfconn = self.cstatus == self.CS_WFCONNECTION
111 self.is_connected = self.cstatus == self.CS_CONNECTED
112 self.is_unconfigured = self.cstatus == self.CS_UNCONFIGURED
113 self.is_primary = self.lrole == self.RO_PRIMARY
114 self.is_secondary = self.lrole == self.RO_SECONDARY
115 self.peer_primary = self.rrole == self.RO_PRIMARY
116 self.peer_secondary = self.rrole == self.RO_SECONDARY
117 self.both_primary = self.is_primary and self.peer_primary
118 self.both_secondary = self.is_secondary and self.peer_secondary
120 self.is_diskless = self.ldisk == self.DS_DISKLESS
121 self.is_disk_uptodate = self.ldisk == self.DS_UPTODATE
123 self.is_in_resync = self.cstatus in self.CSET_SYNC
124 self.is_in_use = self.cstatus != self.CS_UNCONFIGURED
126 m = self.SYNC_RE.match(procline)
128 self.sync_percent = float(m.group(1))
129 hours = int(m.group(2))
130 minutes = int(m.group(3))
131 seconds = int(m.group(4))
132 self.est_time = hours * 3600 + minutes * 60 + seconds
134 # we have (in this if branch) no percent information, but if
135 # we're resyncing we need to 'fake' a sync percent information,
136 # as this is how cmdlib determines if it makes sense to wait for
138 if self.is_in_resync:
139 self.sync_percent = 0
141 self.sync_percent = None
145 class DRBD8Info(object):
146 """Represents information DRBD exports (usually via /proc/drbd).
148 An instance of this class is created by one of the CreateFrom... methods.
152 _VERSION_RE = re.compile(r"^version: (\d+)\.(\d+)\.(\d+)(?:\.(\d+))?"
153 r" \(api:(\d+)/proto:(\d+)(?:-(\d+))?\)")
154 _VALID_LINE_RE = re.compile("^ *([0-9]+): cs:([^ ]+).*$")
156 def __init__(self, lines):
157 self._version = self._ParseVersion(lines)
158 self._minors, self._line_per_minor = self._JoinLinesPerMinor(lines)
160 def GetVersion(self):
161 """Return the DRBD version.
163 This will return a dict with keys:
167 - k_fix (only on some drbd versions)
170 - proto2 (only on drbd > 8.2.X)
175 def GetVersionString(self):
176 """Return the DRBD version as a single string.
179 version = self.GetVersion()
180 retval = "%d.%d.%d" % \
181 (version["k_major"], version["k_minor"], version["k_point"])
182 if "k_fix" in version:
183 retval += ".%s" % version["k_fix"]
185 retval += " (api:%d/proto:%d" % (version["api"], version["proto"])
186 if "proto2" in version:
187 retval += "-%s" % version["proto2"]
192 """Return a list of minor for which information is available.
194 This list is ordered in exactly the order which was found in the underlying
200 def HasMinorStatus(self, minor):
201 return minor in self._line_per_minor
203 def GetMinorStatus(self, minor):
204 return DRBD8Status(self._line_per_minor[minor])
206 def _ParseVersion(self, lines):
207 first_line = lines[0].strip()
208 version = self._VERSION_RE.match(first_line)
210 raise errors.BlockDeviceError("Can't parse DRBD version from '%s'" %
213 values = version.groups()
215 "k_major": int(values[0]),
216 "k_minor": int(values[1]),
217 "k_point": int(values[2]),
218 "api": int(values[4]),
219 "proto": int(values[5]),
221 if values[3] is not None:
222 retval["k_fix"] = values[3]
223 if values[6] is not None:
224 retval["proto2"] = values[6]
228 def _JoinLinesPerMinor(self, lines):
229 """Transform the raw lines into a dictionary based on the minor.
231 @return: a dictionary of minor: joined lines from /proc/drbd
237 old_minor = old_line = None
239 if not line: # completely empty lines, as can be returned by drbd8.0+
241 lresult = self._VALID_LINE_RE.match(line)
242 if lresult is not None:
243 if old_minor is not None:
244 minors.append(old_minor)
245 results[old_minor] = old_line
246 old_minor = int(lresult.group(1))
249 if old_minor is not None:
250 old_line += " " + line.strip()
252 if old_minor is not None:
253 minors.append(old_minor)
254 results[old_minor] = old_line
255 return minors, results
258 def CreateFromLines(lines):
259 return DRBD8Info(lines)
262 def CreateFromFile(filename=constants.DRBD_STATUS_FILE):
264 lines = utils.ReadFile(filename).splitlines()
265 except EnvironmentError, err:
266 if err.errno == errno.ENOENT:
267 base.ThrowError("The file %s cannot be opened, check if the module"
268 " is loaded (%s)", filename, str(err))
270 base.ThrowError("Can't read the DRBD proc file %s: %s",
273 base.ThrowError("Can't read any data from %s", filename)
274 return DRBD8Info.CreateFromLines(lines)
277 class BaseShowInfo(object):
278 """Base class for parsing the `drbdsetup show` output.
280 Holds various common pyparsing expressions which are used by subclasses. Also
281 provides caching of the constructed parser.
287 _lbrace = pyp.Literal("{").suppress()
288 _rbrace = pyp.Literal("}").suppress()
289 _lbracket = pyp.Literal("[").suppress()
290 _rbracket = pyp.Literal("]").suppress()
291 _semi = pyp.Literal(";").suppress()
292 _colon = pyp.Literal(":").suppress()
293 # this also converts the value to an int
294 _number = pyp.Word(pyp.nums).setParseAction(lambda s, l, t: int(t[0]))
296 _comment = pyp.Literal("#") + pyp.Optional(pyp.restOfLine)
297 _defa = pyp.Literal("_is_default").suppress()
298 _dbl_quote = pyp.Literal('"').suppress()
300 _keyword = pyp.Word(pyp.alphanums + "-")
303 _value = pyp.Word(pyp.alphanums + "_-/.:")
304 _quoted = _dbl_quote + pyp.CharsNotIn('"') + _dbl_quote
305 _ipv4_addr = (pyp.Optional(pyp.Literal("ipv4")).suppress() +
306 pyp.Word(pyp.nums + ".") + _colon + _number)
307 _ipv6_addr = (pyp.Optional(pyp.Literal("ipv6")).suppress() +
308 pyp.Optional(_lbracket) + pyp.Word(pyp.hexnums + ":") +
309 pyp.Optional(_rbracket) + _colon + _number)
310 # meta device, extended syntax
311 _meta_value = ((_value ^ _quoted) + _lbracket + _number + _rbracket)
312 # device name, extended syntax
313 _device_value = pyp.Literal("minor").suppress() + _number
316 _stmt = (~_rbrace + _keyword + ~_lbrace +
317 pyp.Optional(_ipv4_addr ^ _ipv6_addr ^ _value ^ _quoted ^
318 _meta_value ^ _device_value) +
319 pyp.Optional(_defa) + _semi +
320 pyp.Optional(pyp.restOfLine).suppress())
323 def GetDevInfo(cls, show_data):
324 """Parse details about a given DRBD minor.
326 This returns, if available, the local backing device (as a path)
327 and the local and remote (ip, port) information from a string
328 containing the output of the `drbdsetup show` command as returned
329 by DRBD8Dev._GetShowData.
331 This will return a dict with keys:
344 results = (cls._GetShowParser()).parseString(show_data)
345 except pyp.ParseException, err:
346 base.ThrowError("Can't parse drbdsetup show output: %s", str(err))
348 return cls._TransformParseResult(results)
351 def _TransformParseResult(cls, parse_result):
352 raise NotImplementedError
355 def _GetShowParser(cls):
356 """Return a parser for `drbd show` output.
358 This will either create or return an already-created parser for the
359 output of the command `drbd show`.
362 if cls._PARSE_SHOW is None:
363 cls._PARSE_SHOW = cls._ConstructShowParser()
365 return cls._PARSE_SHOW
368 def _ConstructShowParser(cls):
369 raise NotImplementedError
372 class DRBD83ShowInfo(BaseShowInfo):
374 def _ConstructShowParser(cls):
376 section_name = pyp.Word(pyp.alphas + "_")
377 section = section_name + \
379 pyp.ZeroOrMore(pyp.Group(cls._stmt)) + \
382 bnf = pyp.ZeroOrMore(pyp.Group(section ^ cls._stmt))
383 bnf.ignore(cls._comment)
388 def _TransformParseResult(cls, parse_result):
390 for section in parse_result:
392 if sname == "_this_host":
393 for lst in section[1:]:
395 retval["local_dev"] = lst[1]
396 elif lst[0] == "meta-disk":
397 retval["meta_dev"] = lst[1]
398 retval["meta_index"] = lst[2]
399 elif lst[0] == "address":
400 retval["local_addr"] = tuple(lst[1:])
401 elif sname == "_remote_host":
402 for lst in section[1:]:
403 if lst[0] == "address":
404 retval["remote_addr"] = tuple(lst[1:])
408 class DRBD84ShowInfo(BaseShowInfo):
410 def _ConstructShowParser(cls):
411 # an entire section (sections can be nested in DRBD 8.4, and there exist
412 # sections like "volume 0")
413 section_name = pyp.Word(pyp.alphas + "_") + \
414 pyp.Optional(pyp.Word(pyp.nums)).suppress() # skip volume idx
415 section = pyp.Forward()
416 # pylint: disable=W0106
417 section << (section_name +
419 pyp.ZeroOrMore(pyp.Group(cls._stmt ^ section)) +
422 resource_name = pyp.Word(pyp.alphanums + "_-.")
423 resource = (pyp.Literal("resource") + resource_name).suppress() + \
425 pyp.ZeroOrMore(pyp.Group(section)) + \
428 resource.ignore(cls._comment)
433 def _TransformParseResult(cls, parse_result):
435 for section in parse_result:
437 if sname == "_this_host":
438 for lst in section[1:]:
439 if lst[0] == "address":
440 retval["local_addr"] = tuple(lst[1:])
441 elif lst[0] == "volume":
442 for inner in lst[1:]:
443 if inner[0] == "disk" and len(inner) == 2:
444 retval["local_dev"] = inner[1]
445 elif inner[0] == "meta-disk" and len(inner) == 3:
446 retval["meta_dev"] = inner[1]
447 retval["meta_index"] = inner[2]
448 elif sname == "_remote_host":
449 for lst in section[1:]:
450 if lst[0] == "address":
451 retval["remote_addr"] = tuple(lst[1:])