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:
169 - proto2 (only on drbd > 8.2.X)
175 """Return a list of minor for which information is available.
177 This list is ordered in exactly the order which was found in the underlying
183 def HasMinorStatus(self, minor):
184 return minor in self._line_per_minor
186 def GetMinorStatus(self, minor):
187 return DRBD8Status(self._line_per_minor[minor])
189 def _ParseVersion(self, lines):
190 first_line = lines[0].strip()
191 version = self._VERSION_RE.match(first_line)
193 raise errors.BlockDeviceError("Can't parse DRBD version from '%s'" %
196 values = version.groups()
198 "k_major": int(values[0]),
199 "k_minor": int(values[1]),
200 "k_point": int(values[2]),
201 "api": int(values[3]),
202 "proto": int(values[4]),
204 if values[5] is not None:
205 retval["proto2"] = values[5]
209 def _JoinLinesPerMinor(self, lines):
210 """Transform the raw lines into a dictionary based on the minor.
212 @return: a dictionary of minor: joined lines from /proc/drbd
218 old_minor = old_line = None
220 if not line: # completely empty lines, as can be returned by drbd8.0+
222 lresult = self._VALID_LINE_RE.match(line)
223 if lresult is not None:
224 if old_minor is not None:
225 minors.append(old_minor)
226 results[old_minor] = old_line
227 old_minor = int(lresult.group(1))
230 if old_minor is not None:
231 old_line += " " + line.strip()
233 if old_minor is not None:
234 minors.append(old_minor)
235 results[old_minor] = old_line
236 return minors, results
239 def CreateFromLines(lines):
240 return DRBD8Info(lines)
243 def CreateFromFile(filename=constants.DRBD_STATUS_FILE):
245 lines = utils.ReadFile(filename).splitlines()
246 except EnvironmentError, err:
247 if err.errno == errno.ENOENT:
248 base.ThrowError("The file %s cannot be opened, check if the module"
249 " is loaded (%s)", filename, str(err))
251 base.ThrowError("Can't read the DRBD proc file %s: %s",
254 base.ThrowError("Can't read any data from %s", filename)
255 return DRBD8Info.CreateFromLines(lines)
258 class DRBD8ShowInfo(object):
259 """Helper class which parses the output of drbdsetup show
265 def _GetShowParser(cls):
266 """Return a parser for `drbd show` output.
268 This will either create or return an already-created parser for the
269 output of the command `drbd show`.
272 if cls._PARSE_SHOW is not None:
273 return cls._PARSE_SHOW
276 lbrace = pyp.Literal("{").suppress()
277 rbrace = pyp.Literal("}").suppress()
278 lbracket = pyp.Literal("[").suppress()
279 rbracket = pyp.Literal("]").suppress()
280 semi = pyp.Literal(";").suppress()
281 colon = pyp.Literal(":").suppress()
282 # this also converts the value to an int
283 number = pyp.Word(pyp.nums).setParseAction(lambda s, l, t: int(t[0]))
285 comment = pyp.Literal("#") + pyp.Optional(pyp.restOfLine)
286 defa = pyp.Literal("_is_default").suppress()
287 dbl_quote = pyp.Literal('"').suppress()
289 keyword = pyp.Word(pyp.alphanums + "-")
292 value = pyp.Word(pyp.alphanums + "_-/.:")
293 quoted = dbl_quote + pyp.CharsNotIn('"') + dbl_quote
294 ipv4_addr = (pyp.Optional(pyp.Literal("ipv4")).suppress() +
295 pyp.Word(pyp.nums + ".") + colon + number)
296 ipv6_addr = (pyp.Optional(pyp.Literal("ipv6")).suppress() +
297 pyp.Optional(lbracket) + pyp.Word(pyp.hexnums + ":") +
298 pyp.Optional(rbracket) + colon + number)
299 # meta device, extended syntax
300 meta_value = ((value ^ quoted) + lbracket + number + rbracket)
301 # device name, extended syntax
302 device_value = pyp.Literal("minor").suppress() + number
305 stmt = (~rbrace + keyword + ~lbrace +
306 pyp.Optional(ipv4_addr ^ ipv6_addr ^ value ^ quoted ^ meta_value ^
308 pyp.Optional(defa) + semi +
309 pyp.Optional(pyp.restOfLine).suppress())
312 section_name = pyp.Word(pyp.alphas + "_")
313 section = section_name + lbrace + pyp.ZeroOrMore(pyp.Group(stmt)) + rbrace
315 bnf = pyp.ZeroOrMore(pyp.Group(section ^ stmt))
318 cls._PARSE_SHOW = bnf
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 DRBD8._GetShowData.
331 This will return a dict with keys:
345 results = (cls._GetShowParser()).parseString(show_data)
346 except pyp.ParseException, err:
347 base.ThrowError("Can't parse drbdsetup show output: %s", str(err))
349 # and massage the results into our desired format
350 for section in results:
352 if sname == "_this_host":
353 for lst in section[1:]:
355 retval["local_dev"] = lst[1]
356 elif lst[0] == "meta-disk":
357 retval["meta_dev"] = lst[1]
358 retval["meta_index"] = lst[2]
359 elif lst[0] == "address":
360 retval["local_addr"] = tuple(lst[1:])
361 elif sname == "_remote_host":
362 for lst in section[1:]:
363 if lst[0] == "address":
364 retval["remote_addr"] = tuple(lst[1:])