Statistics
| Branch: | Tag: | Revision:

root / lib / storage / drbd_info.py @ 30b12688

History | View | Annotate | Download (14.3 kB)

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
263
  def CreateFromFile(filename=constants.DRBD_STATUS_FILE):
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