Package ganeti :: Package storage :: Module drbd_info
[hide private]
[frames] | no frames]

Source Code for Module ganeti.storage.drbd_info

  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.storage 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 r"\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 r"(?:\s|M)" 49 r"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 self.peer_disk_uptodate = self.rdisk == self.DS_UPTODATE 123 124 self.is_in_resync = self.cstatus in self.CSET_SYNC 125 self.is_in_use = self.cstatus != self.CS_UNCONFIGURED 126 127 m = self.SYNC_RE.match(procline) 128 if m: 129 self.sync_percent = float(m.group(1)) 130 hours = int(m.group(2)) 131 minutes = int(m.group(3)) 132 seconds = int(m.group(4)) 133 self.est_time = hours * 3600 + minutes * 60 + seconds 134 else: 135 # we have (in this if branch) no percent information, but if 136 # we're resyncing we need to 'fake' a sync percent information, 137 # as this is how cmdlib determines if it makes sense to wait for 138 # resyncing or not 139 if self.is_in_resync: 140 self.sync_percent = 0 141 else: 142 self.sync_percent = None 143 self.est_time = None
144
145 146 -class DRBD8Info(object):
147 """Represents information DRBD exports (usually via /proc/drbd). 148 149 An instance of this class is created by one of the CreateFrom... methods. 150 151 """ 152 153 _VERSION_RE = re.compile(r"^version: (\d+)\.(\d+)\.(\d+)(?:\.(\d+))?" 154 r" \(api:(\d+)/proto:(\d+)(?:-(\d+))?\)") 155 _VALID_LINE_RE = re.compile("^ *([0-9]+): cs:([^ ]+).*$") 156
157 - def __init__(self, lines):
158 self._version = self._ParseVersion(lines) 159 self._minors, self._line_per_minor = self._JoinLinesPerMinor(lines)
160
161 - def GetVersion(self):
162 """Return the DRBD version. 163 164 This will return a dict with keys: 165 - k_major 166 - k_minor 167 - k_point 168 - k_fix (only on some drbd versions) 169 - api 170 - proto 171 - proto2 (only on drbd > 8.2.X) 172 173 """ 174 return self._version
175
176 - def GetVersionString(self):
177 """Return the DRBD version as a single string. 178 179 """ 180 version = self.GetVersion() 181 retval = "%d.%d.%d" % \ 182 (version["k_major"], version["k_minor"], version["k_point"]) 183 if "k_fix" in version: 184 retval += ".%s" % version["k_fix"] 185 186 retval += " (api:%d/proto:%d" % (version["api"], version["proto"]) 187 if "proto2" in version: 188 retval += "-%s" % version["proto2"] 189 retval += ")" 190 return retval
191
192 - def GetMinors(self):
193 """Return a list of minor for which information is available. 194 195 This list is ordered in exactly the order which was found in the underlying 196 data. 197 198 """ 199 return self._minors
200
201 - def HasMinorStatus(self, minor):
202 return minor in self._line_per_minor
203
204 - def GetMinorStatus(self, minor):
205 return DRBD8Status(self._line_per_minor[minor])
206
207 - def _ParseVersion(self, lines):
208 first_line = lines[0].strip() 209 version = self._VERSION_RE.match(first_line) 210 if not version: 211 raise errors.BlockDeviceError("Can't parse DRBD version from '%s'" % 212 first_line) 213 214 values = version.groups() 215 retval = { 216 "k_major": int(values[0]), 217 "k_minor": int(values[1]), 218 "k_point": int(values[2]), 219 "api": int(values[4]), 220 "proto": int(values[5]), 221 } 222 if values[3] is not None: 223 retval["k_fix"] = values[3] 224 if values[6] is not None: 225 retval["proto2"] = values[6] 226 227 return retval
228
229 - def _JoinLinesPerMinor(self, lines):
230 """Transform the raw lines into a dictionary based on the minor. 231 232 @return: a dictionary of minor: joined lines from /proc/drbd 233 for that minor 234 235 """ 236 minors = [] 237 results = {} 238 old_minor = old_line = None 239 for line in lines: 240 if not line: # completely empty lines, as can be returned by drbd8.0+ 241 continue 242 lresult = self._VALID_LINE_RE.match(line) 243 if lresult is not None: 244 if old_minor is not None: 245 minors.append(old_minor) 246 results[old_minor] = old_line 247 old_minor = int(lresult.group(1)) 248 old_line = line 249 else: 250 if old_minor is not None: 251 old_line += " " + line.strip() 252 # add last line 253 if old_minor is not None: 254 minors.append(old_minor) 255 results[old_minor] = old_line 256 return minors, results
257 258 @staticmethod
259 - def CreateFromLines(lines):
260 return DRBD8Info(lines)
261 262 @staticmethod
264 try: 265 lines = utils.ReadFile(filename).splitlines() 266 except EnvironmentError, err: 267 if err.errno == errno.ENOENT: 268 base.ThrowError("The file %s cannot be opened, check if the module" 269 " is loaded (%s)", filename, str(err)) 270 else: 271 base.ThrowError("Can't read the DRBD proc file %s: %s", 272 filename, str(err)) 273 if not lines: 274 base.ThrowError("Can't read any data from %s", filename) 275 return DRBD8Info.CreateFromLines(lines)
276
277 278 -class BaseShowInfo(object):
279 """Base class for parsing the `drbdsetup show` output. 280 281 Holds various common pyparsing expressions which are used by subclasses. Also 282 provides caching of the constructed parser. 283 284 """ 285 _PARSE_SHOW = None 286 287 # pyparsing setup 288 _lbrace = pyp.Literal("{").suppress() 289 _rbrace = pyp.Literal("}").suppress() 290 _lbracket = pyp.Literal("[").suppress() 291 _rbracket = pyp.Literal("]").suppress() 292 _semi = pyp.Literal(";").suppress() 293 _colon = pyp.Literal(":").suppress() 294 # this also converts the value to an int 295 _number = pyp.Word(pyp.nums).setParseAction(lambda s, l, t: int(t[0])) 296 297 _comment = pyp.Literal("#") + pyp.Optional(pyp.restOfLine) 298 _defa = pyp.Literal("_is_default").suppress() 299 _dbl_quote = pyp.Literal('"').suppress() 300 301 _keyword = pyp.Word(pyp.alphanums + "-") 302 303 # value types 304 _value = pyp.Word(pyp.alphanums + "_-/.:") 305 _quoted = _dbl_quote + pyp.CharsNotIn('"') + _dbl_quote 306 _ipv4_addr = (pyp.Optional(pyp.Literal("ipv4")).suppress() + 307 pyp.Word(pyp.nums + ".") + _colon + _number) 308 _ipv6_addr = (pyp.Optional(pyp.Literal("ipv6")).suppress() + 309 pyp.Optional(_lbracket) + pyp.Word(pyp.hexnums + ":") + 310 pyp.Optional(_rbracket) + _colon + _number) 311 # meta device, extended syntax 312 _meta_value = ((_value ^ _quoted) + _lbracket + _number + _rbracket) 313 # device name, extended syntax 314 _device_value = pyp.Literal("minor").suppress() + _number 315 316 # a statement 317 _stmt = (~_rbrace + _keyword + ~_lbrace + 318 pyp.Optional(_ipv4_addr ^ _ipv6_addr ^ _value ^ _quoted ^ 319 _meta_value ^ _device_value) + 320 pyp.Optional(_defa) + _semi + 321 pyp.Optional(pyp.restOfLine).suppress()) 322 323 @classmethod
324 - def GetDevInfo(cls, show_data):
325 """Parse details about a given DRBD minor. 326 327 This returns, if available, the local backing device (as a path) 328 and the local and remote (ip, port) information from a string 329 containing the output of the `drbdsetup show` command as returned 330 by DRBD8Dev._GetShowData. 331 332 This will return a dict with keys: 333 - local_dev 334 - meta_dev 335 - meta_index 336 - local_addr 337 - remote_addr 338 339 """ 340 if not show_data: 341 return {} 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 return cls._TransformParseResult(results)
350 351 @classmethod
352 - def _TransformParseResult(cls, parse_result):
353 raise NotImplementedError
354 355 @classmethod
356 - def _GetShowParser(cls):
357 """Return a parser for `drbd show` output. 358 359 This will either create or return an already-created parser for the 360 output of the command `drbd show`. 361 362 """ 363 if cls._PARSE_SHOW is None: 364 cls._PARSE_SHOW = cls._ConstructShowParser() 365 366 return cls._PARSE_SHOW
367 368 @classmethod
369 - def _ConstructShowParser(cls):
370 raise NotImplementedError
371
372 373 -class DRBD83ShowInfo(BaseShowInfo):
374 @classmethod
375 - def _ConstructShowParser(cls):
376 # an entire section 377 section_name = pyp.Word(pyp.alphas + "_") 378 section = section_name + \ 379 cls._lbrace + \ 380 pyp.ZeroOrMore(pyp.Group(cls._stmt)) + \ 381 cls._rbrace 382 383 bnf = pyp.ZeroOrMore(pyp.Group(section ^ cls._stmt)) 384 bnf.ignore(cls._comment) 385 386 return bnf
387 388 @classmethod
389 - def _TransformParseResult(cls, parse_result):
390 retval = {} 391 for section in parse_result: 392 sname = section[0] 393 if sname == "_this_host": 394 for lst in section[1:]: 395 if lst[0] == "disk": 396 retval["local_dev"] = lst[1] 397 elif lst[0] == "meta-disk": 398 retval["meta_dev"] = lst[1] 399 retval["meta_index"] = lst[2] 400 elif lst[0] == "address": 401 retval["local_addr"] = tuple(lst[1:]) 402 elif sname == "_remote_host": 403 for lst in section[1:]: 404 if lst[0] == "address": 405 retval["remote_addr"] = tuple(lst[1:]) 406 return retval
407
408 409 -class DRBD84ShowInfo(BaseShowInfo):
410 @classmethod
411 - def _ConstructShowParser(cls):
412 # an entire section (sections can be nested in DRBD 8.4, and there exist 413 # sections like "volume 0") 414 section_name = pyp.Word(pyp.alphas + "_") + \ 415 pyp.Optional(pyp.Word(pyp.nums)).suppress() # skip volume idx 416 section = pyp.Forward() 417 # pylint: disable=W0106 418 section << (section_name + 419 cls._lbrace + 420 pyp.ZeroOrMore(pyp.Group(cls._stmt ^ section)) + 421 cls._rbrace) 422 423 resource_name = pyp.Word(pyp.alphanums + "_-.") 424 resource = (pyp.Literal("resource") + resource_name).suppress() + \ 425 cls._lbrace + \ 426 pyp.ZeroOrMore(pyp.Group(section)) + \ 427 cls._rbrace 428 429 resource.ignore(cls._comment) 430 431 return resource
432 433 @classmethod
434 - def _TransformVolumeSection(cls, vol_content, retval):
435 for entry in vol_content: 436 if entry[0] == "disk" and len(entry) == 2 and \ 437 isinstance(entry[1], basestring): 438 retval["local_dev"] = entry[1] 439 elif entry[0] == "meta-disk": 440 if len(entry) > 1: 441 retval["meta_dev"] = entry[1] 442 if len(entry) > 2: 443 retval["meta_index"] = entry[2]
444 445 @classmethod
446 - def _TransformParseResult(cls, parse_result):
447 retval = {} 448 for section in parse_result: 449 sname = section[0] 450 if sname == "_this_host": 451 for lst in section[1:]: 452 if lst[0] == "address": 453 retval["local_addr"] = tuple(lst[1:]) 454 elif lst[0] == "volume": 455 cls._TransformVolumeSection(lst[1:], retval) 456 elif sname == "_remote_host": 457 for lst in section[1:]: 458 if lst[0] == "address": 459 retval["remote_addr"] = tuple(lst[1:]) 460 return retval
461