Extract DRBD info parsing into drbd_info.py
[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       - api
168       - proto
169       - proto2 (only on drbd > 8.2.X)
170
171     """
172     return self._version
173
174   def GetMinors(self):
175     """Return a list of minor for which information is available.
176
177     This list is ordered in exactly the order which was found in the underlying
178     data.
179
180     """
181     return self._minors
182
183   def HasMinorStatus(self, minor):
184     return minor in self._line_per_minor
185
186   def GetMinorStatus(self, minor):
187     return DRBD8Status(self._line_per_minor[minor])
188
189   def _ParseVersion(self, lines):
190     first_line = lines[0].strip()
191     version = self._VERSION_RE.match(first_line)
192     if not version:
193       raise errors.BlockDeviceError("Can't parse DRBD version from '%s'" %
194                                     first_line)
195
196     values = version.groups()
197     retval = {
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]),
203       }
204     if values[5] is not None:
205       retval["proto2"] = values[5]
206
207     return retval
208
209   def _JoinLinesPerMinor(self, lines):
210     """Transform the raw lines into a dictionary based on the minor.
211
212     @return: a dictionary of minor: joined lines from /proc/drbd
213         for that minor
214
215     """
216     minors = []
217     results = {}
218     old_minor = old_line = None
219     for line in lines:
220       if not line: # completely empty lines, as can be returned by drbd8.0+
221         continue
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))
228         old_line = line
229       else:
230         if old_minor is not None:
231           old_line += " " + line.strip()
232     # add last line
233     if old_minor is not None:
234       minors.append(old_minor)
235       results[old_minor] = old_line
236     return minors, results
237
238   @staticmethod
239   def CreateFromLines(lines):
240     return DRBD8Info(lines)
241
242   @staticmethod
243   def CreateFromFile(filename=constants.DRBD_STATUS_FILE):
244     try:
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))
250       else:
251         base.ThrowError("Can't read the DRBD proc file %s: %s",
252                         filename, str(err))
253     if not lines:
254       base.ThrowError("Can't read any data from %s", filename)
255     return DRBD8Info.CreateFromLines(lines)
256
257
258 class DRBD8ShowInfo(object):
259   """Helper class which parses the output of drbdsetup show
260
261   """
262   _PARSE_SHOW = None
263
264   @classmethod
265   def _GetShowParser(cls):
266     """Return a parser for `drbd show` output.
267
268     This will either create or return an already-created parser for the
269     output of the command `drbd show`.
270
271     """
272     if cls._PARSE_SHOW is not None:
273       return cls._PARSE_SHOW
274
275     # pyparsing setup
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]))
284
285     comment = pyp.Literal("#") + pyp.Optional(pyp.restOfLine)
286     defa = pyp.Literal("_is_default").suppress()
287     dbl_quote = pyp.Literal('"').suppress()
288
289     keyword = pyp.Word(pyp.alphanums + "-")
290
291     # value types
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
303
304     # a statement
305     stmt = (~rbrace + keyword + ~lbrace +
306             pyp.Optional(ipv4_addr ^ ipv6_addr ^ value ^ quoted ^ meta_value ^
307                          device_value) +
308             pyp.Optional(defa) + semi +
309             pyp.Optional(pyp.restOfLine).suppress())
310
311     # an entire section
312     section_name = pyp.Word(pyp.alphas + "_")
313     section = section_name + lbrace + pyp.ZeroOrMore(pyp.Group(stmt)) + rbrace
314
315     bnf = pyp.ZeroOrMore(pyp.Group(section ^ stmt))
316     bnf.ignore(comment)
317
318     cls._PARSE_SHOW = bnf
319
320     return bnf
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 DRBD8._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     retval = {}
340     if not show_data:
341       return retval
342
343     try:
344       # run pyparse
345       results = (cls._GetShowParser()).parseString(show_data)
346     except pyp.ParseException, err:
347       base.ThrowError("Can't parse drbdsetup show output: %s", str(err))
348
349     # and massage the results into our desired format
350     for section in results:
351       sname = section[0]
352       if sname == "_this_host":
353         for lst in section[1:]:
354           if lst[0] == "disk":
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:])
365     return retval