Statistics
| Branch: | Tag: | Revision:

root / lib / storage / drbd_info.py @ 78f99abb

History | View | Annotate | Download (14.2 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

    
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 _TransformVolumeSection(cls, vol_content, retval):
434
    for entry in vol_content:
435
      if entry[0] == "disk" and len(entry) == 2 and \
436
          isinstance(entry[1], basestring):
437
        retval["local_dev"] = entry[1]
438
      elif entry[0] == "meta-disk":
439
        if len(entry) > 1:
440
          retval["meta_dev"] = entry[1]
441
        if len(entry) > 2:
442
          retval["meta_index"] = entry[2]
443

    
444
  @classmethod
445
  def _TransformParseResult(cls, parse_result):
446
    retval = {}
447
    for section in parse_result:
448
      sname = section[0]
449
      if sname == "_this_host":
450
        for lst in section[1:]:
451
          if lst[0] == "address":
452
            retval["local_addr"] = tuple(lst[1:])
453
          elif lst[0] == "volume":
454
            cls._TransformVolumeSection(lst[1:], retval)
455
      elif sname == "_remote_host":
456
        for lst in section[1:]:
457
          if lst[0] == "address":
458
            retval["remote_addr"] = tuple(lst[1:])
459
    return retval