Rename DRBD8 to DRBD8Dev
[ganeti-local] / lib / block / drbd_info.py
1 #
2 #
3
4 # Copyright (C) 2006, 2007, 2010, 2011, 2012, 2013 Google Inc.
5 #
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.
10 #
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.
15 #
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
19 # 02110-1301, USA.
20
21
22 """DRBD information parsing utilities"""
23
24 import errno
25 import pyparsing as pyp
26 import re
27
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
33
34
35 class DRBD8Status(object): # pylint: disable=R0902
36   """A DRBD status representation class.
37
38   Note that this class is meant to be used to parse one of the entries returned
39   from L{DRBD8Info._JoinLinesPerMinor}.
40
41   """
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)
48                        "(?:\s|M)"
49                        "finish: ([0-9]+):([0-9]+):([0-9]+)\s.*$")
50
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([
66     CS_WFREPORTPARAMS,
67     CS_STARTINGSYNCS,
68     CS_STARTINGSYNCT,
69     CS_WFBITMAPS,
70     CS_WFBITMAPT,
71     CS_WFSYNCUUID,
72     CS_SYNCSOURCE,
73     CS_SYNCTARGET,
74     CS_PAUSEDSYNCS,
75     CS_PAUSEDSYNCT,
76     ])
77
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
87
88   RO_PRIMARY = "Primary"
89   RO_SECONDARY = "Secondary"
90   RO_UNKNOWN = "Unknown"
91
92   def __init__(self, procline):
93     u = self.UNCONF_RE.match(procline)
94     if u:
95       self.cstatus = self.CS_UNCONFIGURED
96       self.lrole = self.rrole = self.ldisk = self.rdisk = None
97     else:
98       m = self.LINE_RE.match(procline)
99       if not m:
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)
106
107     # end reading of data from the LINE_RE or UNCONF_RE
108
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
119
120     self.is_diskless = self.ldisk == self.DS_DISKLESS
121     self.is_disk_uptodate = self.ldisk == self.DS_UPTODATE
122
123     self.is_in_resync = self.cstatus in self.CSET_SYNC
124     self.is_in_use = self.cstatus != self.CS_UNCONFIGURED
125
126     m = self.SYNC_RE.match(procline)
127     if m:
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
133     else:
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
137       # resyncing or not
138       if self.is_in_resync:
139         self.sync_percent = 0
140       else:
141         self.sync_percent = None
142       self.est_time = None
143
144
145 class DRBD8Info(object):
146   """Represents information DRBD exports (usually via /proc/drbd).
147
148   An instance of this class is created by one of the CreateFrom... methods.
149
150   """
151
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:([^ ]+).*$")
155
156   def __init__(self, lines):
157     self._version = self._ParseVersion(lines)
158     self._minors, self._line_per_minor = self._JoinLinesPerMinor(lines)
159
160   def GetVersion(self):
161     """Return the DRBD version.
162
163     This will return a dict with keys:
164       - k_major
165       - k_minor
166       - k_point
167       - k_fix (only on some drbd versions)
168       - api
169       - proto
170       - proto2 (only on drbd > 8.2.X)
171
172     """
173     return self._version
174
175   def GetVersionString(self):
176     """Return the DRBD version as a single string.
177
178     """
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"]
184
185     retval += " (api:%d/proto:%d" % (version["api"], version["proto"])
186     if "proto2" in version:
187       retval += "-%s" % version["proto2"]
188     retval += ")"
189     return retval
190
191   def GetMinors(self):
192     """Return a list of minor for which information is available.
193
194     This list is ordered in exactly the order which was found in the underlying
195     data.
196
197     """
198     return self._minors
199
200   def HasMinorStatus(self, minor):
201     return minor in self._line_per_minor
202
203   def GetMinorStatus(self, minor):
204     return DRBD8Status(self._line_per_minor[minor])
205
206   def _ParseVersion(self, lines):
207     first_line = lines[0].strip()
208     version = self._VERSION_RE.match(first_line)
209     if not version:
210       raise errors.BlockDeviceError("Can't parse DRBD version from '%s'" %
211                                     first_line)
212
213     values = version.groups()
214     retval = {
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]),
220       }
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]
225
226     return retval
227
228   def _JoinLinesPerMinor(self, lines):
229     """Transform the raw lines into a dictionary based on the minor.
230
231     @return: a dictionary of minor: joined lines from /proc/drbd
232         for that minor
233
234     """
235     minors = []
236     results = {}
237     old_minor = old_line = None
238     for line in lines:
239       if not line: # completely empty lines, as can be returned by drbd8.0+
240         continue
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))
247         old_line = line
248       else:
249         if old_minor is not None:
250           old_line += " " + line.strip()
251     # add last line
252     if old_minor is not None:
253       minors.append(old_minor)
254       results[old_minor] = old_line
255     return minors, results
256
257   @staticmethod
258   def CreateFromLines(lines):
259     return DRBD8Info(lines)
260
261   @staticmethod
262   def CreateFromFile(filename=constants.DRBD_STATUS_FILE):
263     try:
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))
269       else:
270         base.ThrowError("Can't read the DRBD proc file %s: %s",
271                         filename, str(err))
272     if not lines:
273       base.ThrowError("Can't read any data from %s", filename)
274     return DRBD8Info.CreateFromLines(lines)
275
276
277 class BaseShowInfo(object):
278   """Base class for parsing the `drbdsetup show` output.
279
280   Holds various common pyparsing expressions which are used by subclasses. Also
281   provides caching of the constructed parser.
282
283   """
284   _PARSE_SHOW = None
285
286   # pyparsing setup
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]))
295
296   _comment = pyp.Literal("#") + pyp.Optional(pyp.restOfLine)
297   _defa = pyp.Literal("_is_default").suppress()
298   _dbl_quote = pyp.Literal('"').suppress()
299
300   _keyword = pyp.Word(pyp.alphanums + "-")
301
302   # value types
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
314
315   # a statement
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())
321
322   @classmethod
323   def GetDevInfo(cls, show_data):
324     """Parse details about a given DRBD minor.
325
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.
330
331     This will return a dict with keys:
332       - local_dev
333       - meta_dev
334       - meta_index
335       - local_addr
336       - remote_addr
337
338     """
339     if not show_data:
340       return {}
341
342     try:
343       # run pyparse
344       results = (cls._GetShowParser()).parseString(show_data)
345     except pyp.ParseException, err:
346       base.ThrowError("Can't parse drbdsetup show output: %s", str(err))
347
348     return cls._TransformParseResult(results)
349
350   @classmethod
351   def _TransformParseResult(cls, parse_result):
352     raise NotImplementedError
353
354   @classmethod
355   def _GetShowParser(cls):
356     """Return a parser for `drbd show` output.
357
358     This will either create or return an already-created parser for the
359     output of the command `drbd show`.
360
361     """
362     if cls._PARSE_SHOW is None:
363       cls._PARSE_SHOW = cls._ConstructShowParser()
364
365     return cls._PARSE_SHOW
366
367   @classmethod
368   def _ConstructShowParser(cls):
369     raise NotImplementedError
370
371
372 class DRBD83ShowInfo(BaseShowInfo):
373   @classmethod
374   def _ConstructShowParser(cls):
375     # an entire section
376     section_name = pyp.Word(pyp.alphas + "_")
377     section = section_name + \
378               cls._lbrace + \
379               pyp.ZeroOrMore(pyp.Group(cls._stmt)) + \
380               cls._rbrace
381
382     bnf = pyp.ZeroOrMore(pyp.Group(section ^ cls._stmt))
383     bnf.ignore(cls._comment)
384
385     return bnf
386
387   @classmethod
388   def _TransformParseResult(cls, parse_result):
389     retval = {}
390     for section in parse_result:
391       sname = section[0]
392       if sname == "_this_host":
393         for lst in section[1:]:
394           if lst[0] == "disk":
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:])
405     return retval
406
407
408 class DRBD84ShowInfo(BaseShowInfo):
409   @classmethod
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 +
418                 cls._lbrace +
419                 pyp.ZeroOrMore(pyp.Group(cls._stmt ^ section)) +
420                 cls._rbrace)
421
422     resource_name = pyp.Word(pyp.alphanums + "_-.")
423     resource = (pyp.Literal("resource") + resource_name).suppress() + \
424                cls._lbrace + \
425                pyp.ZeroOrMore(pyp.Group(section)) + \
426                cls._rbrace
427
428     resource.ignore(cls._comment)
429
430     return resource
431
432   @classmethod
433   def _TransformParseResult(cls, parse_result):
434     retval = {}
435     for section in parse_result:
436       sname = section[0]
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:])
452     return retval