Statistics
| Branch: | Tag: | Revision:

root / lib / storage / drbd_info.py @ 5ee24e45

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
                       "\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
  def __repr__(self):
145
    return ("<%s: cstatus=%s, lrole=%s, rrole=%s, ldisk=%s, rdisk=%s>" %
146
            (self.__class__, self.cstatus, self.lrole, self.rrole,
147
             self.ldisk, self.rdisk))
148

    
149

    
150
class DRBD8Info(object):
151
  """Represents information DRBD exports (usually via /proc/drbd).
152

153
  An instance of this class is created by one of the CreateFrom... methods.
154

155
  """
156

    
157
  _VERSION_RE = re.compile(r"^version: (\d+)\.(\d+)\.(\d+)(?:\.(\d+))?"
158
                           r" \(api:(\d+)/proto:(\d+)(?:-(\d+))?\)")
159
  _VALID_LINE_RE = re.compile("^ *([0-9]+): cs:([^ ]+).*$")
160

    
161
  def __init__(self, lines):
162
    self._version = self._ParseVersion(lines)
163
    self._minors, self._line_per_minor = self._JoinLinesPerMinor(lines)
164

    
165
  def GetVersion(self):
166
    """Return the DRBD version.
167

168
    This will return a dict with keys:
169
      - k_major
170
      - k_minor
171
      - k_point
172
      - k_fix (only on some drbd versions)
173
      - api
174
      - proto
175
      - proto2 (only on drbd > 8.2.X)
176

177
    """
178
    return self._version
179

    
180
  def GetVersionString(self):
181
    """Return the DRBD version as a single string.
182

183
    """
184
    version = self.GetVersion()
185
    retval = "%d.%d.%d" % \
186
             (version["k_major"], version["k_minor"], version["k_point"])
187
    if "k_fix" in version:
188
      retval += ".%s" % version["k_fix"]
189

    
190
    retval += " (api:%d/proto:%d" % (version["api"], version["proto"])
191
    if "proto2" in version:
192
      retval += "-%s" % version["proto2"]
193
    retval += ")"
194
    return retval
195

    
196
  def GetMinors(self):
197
    """Return a list of minor for which information is available.
198

199
    This list is ordered in exactly the order which was found in the underlying
200
    data.
201

202
    """
203
    return self._minors
204

    
205
  def HasMinorStatus(self, minor):
206
    return minor in self._line_per_minor
207

    
208
  def GetMinorStatus(self, minor):
209
    return DRBD8Status(self._line_per_minor[minor])
210

    
211
  def _ParseVersion(self, lines):
212
    first_line = lines[0].strip()
213
    version = self._VERSION_RE.match(first_line)
214
    if not version:
215
      raise errors.BlockDeviceError("Can't parse DRBD version from '%s'" %
216
                                    first_line)
217

    
218
    values = version.groups()
219
    retval = {
220
      "k_major": int(values[0]),
221
      "k_minor": int(values[1]),
222
      "k_point": int(values[2]),
223
      "api": int(values[4]),
224
      "proto": int(values[5]),
225
      }
226
    if values[3] is not None:
227
      retval["k_fix"] = values[3]
228
    if values[6] is not None:
229
      retval["proto2"] = values[6]
230

    
231
    return retval
232

    
233
  def _JoinLinesPerMinor(self, lines):
234
    """Transform the raw lines into a dictionary based on the minor.
235

236
    @return: a dictionary of minor: joined lines from /proc/drbd
237
        for that minor
238

239
    """
240
    minors = []
241
    results = {}
242
    old_minor = old_line = None
243
    for line in lines:
244
      if not line: # completely empty lines, as can be returned by drbd8.0+
245
        continue
246
      lresult = self._VALID_LINE_RE.match(line)
247
      if lresult is not None:
248
        if old_minor is not None:
249
          minors.append(old_minor)
250
          results[old_minor] = old_line
251
        old_minor = int(lresult.group(1))
252
        old_line = line
253
      else:
254
        if old_minor is not None:
255
          old_line += " " + line.strip()
256
    # add last line
257
    if old_minor is not None:
258
      minors.append(old_minor)
259
      results[old_minor] = old_line
260
    return minors, results
261

    
262
  @staticmethod
263
  def CreateFromLines(lines):
264
    return DRBD8Info(lines)
265

    
266
  @staticmethod
267
  def CreateFromFile(filename=constants.DRBD_STATUS_FILE):
268
    try:
269
      lines = utils.ReadFile(filename).splitlines()
270
    except EnvironmentError, err:
271
      if err.errno == errno.ENOENT:
272
        base.ThrowError("The file %s cannot be opened, check if the module"
273
                        " is loaded (%s)", filename, str(err))
274
      else:
275
        base.ThrowError("Can't read the DRBD proc file %s: %s",
276
                        filename, str(err))
277
    if not lines:
278
      base.ThrowError("Can't read any data from %s", filename)
279
    return DRBD8Info.CreateFromLines(lines)
280

    
281

    
282
class BaseShowInfo(object):
283
  """Base class for parsing the `drbdsetup show` output.
284

285
  Holds various common pyparsing expressions which are used by subclasses. Also
286
  provides caching of the constructed parser.
287

288
  """
289
  _PARSE_SHOW = None
290

    
291
  # pyparsing setup
292
  _lbrace = pyp.Literal("{").suppress()
293
  _rbrace = pyp.Literal("}").suppress()
294
  _lbracket = pyp.Literal("[").suppress()
295
  _rbracket = pyp.Literal("]").suppress()
296
  _semi = pyp.Literal(";").suppress()
297
  _colon = pyp.Literal(":").suppress()
298
  # this also converts the value to an int
299
  _number = pyp.Word(pyp.nums).setParseAction(lambda s, l, t: int(t[0]))
300

    
301
  _comment = pyp.Literal("#") + pyp.Optional(pyp.restOfLine)
302
  _defa = pyp.Literal("_is_default").suppress()
303
  _dbl_quote = pyp.Literal('"').suppress()
304

    
305
  _keyword = pyp.Word(pyp.alphanums + "-")
306

    
307
  # value types
308
  _value = pyp.Word(pyp.alphanums + "_-/.:")
309
  _quoted = _dbl_quote + pyp.CharsNotIn('"') + _dbl_quote
310
  _ipv4_addr = (pyp.Optional(pyp.Literal("ipv4")).suppress() +
311
                pyp.Word(pyp.nums + ".") + _colon + _number)
312
  _ipv6_addr = (pyp.Optional(pyp.Literal("ipv6")).suppress() +
313
                pyp.Optional(_lbracket) + pyp.Word(pyp.hexnums + ":") +
314
                pyp.Optional(_rbracket) + _colon + _number)
315
  # meta device, extended syntax
316
  _meta_value = ((_value ^ _quoted) + _lbracket + _number + _rbracket)
317
  # device name, extended syntax
318
  _device_value = pyp.Literal("minor").suppress() + _number
319

    
320
  # a statement
321
  _stmt = (~_rbrace + _keyword + ~_lbrace +
322
           pyp.Optional(_ipv4_addr ^ _ipv6_addr ^ _value ^ _quoted ^
323
                        _meta_value ^ _device_value) +
324
           pyp.Optional(_defa) + _semi +
325
           pyp.Optional(pyp.restOfLine).suppress())
326

    
327
  @classmethod
328
  def GetDevInfo(cls, show_data):
329
    """Parse details about a given DRBD minor.
330

331
    This returns, if available, the local backing device (as a path)
332
    and the local and remote (ip, port) information from a string
333
    containing the output of the `drbdsetup show` command as returned
334
    by DRBD8Dev._GetShowData.
335

336
    This will return a dict with keys:
337
      - local_dev
338
      - meta_dev
339
      - meta_index
340
      - local_addr
341
      - remote_addr
342

343
    """
344
    if not show_data:
345
      return {}
346

    
347
    try:
348
      # run pyparse
349
      results = (cls._GetShowParser()).parseString(show_data)
350
    except pyp.ParseException, err:
351
      base.ThrowError("Can't parse drbdsetup show output: %s", str(err))
352

    
353
    return cls._TransformParseResult(results)
354

    
355
  @classmethod
356
  def _TransformParseResult(cls, parse_result):
357
    raise NotImplementedError
358

    
359
  @classmethod
360
  def _GetShowParser(cls):
361
    """Return a parser for `drbd show` output.
362

363
    This will either create or return an already-created parser for the
364
    output of the command `drbd show`.
365

366
    """
367
    if cls._PARSE_SHOW is None:
368
      cls._PARSE_SHOW = cls._ConstructShowParser()
369

    
370
    return cls._PARSE_SHOW
371

    
372
  @classmethod
373
  def _ConstructShowParser(cls):
374
    raise NotImplementedError
375

    
376

    
377
class DRBD83ShowInfo(BaseShowInfo):
378
  @classmethod
379
  def _ConstructShowParser(cls):
380
    # an entire section
381
    section_name = pyp.Word(pyp.alphas + "_")
382
    section = section_name + \
383
              cls._lbrace + \
384
              pyp.ZeroOrMore(pyp.Group(cls._stmt)) + \
385
              cls._rbrace
386

    
387
    bnf = pyp.ZeroOrMore(pyp.Group(section ^ cls._stmt))
388
    bnf.ignore(cls._comment)
389

    
390
    return bnf
391

    
392
  @classmethod
393
  def _TransformParseResult(cls, parse_result):
394
    retval = {}
395
    for section in parse_result:
396
      sname = section[0]
397
      if sname == "_this_host":
398
        for lst in section[1:]:
399
          if lst[0] == "disk":
400
            retval["local_dev"] = lst[1]
401
          elif lst[0] == "meta-disk":
402
            retval["meta_dev"] = lst[1]
403
            retval["meta_index"] = lst[2]
404
          elif lst[0] == "address":
405
            retval["local_addr"] = tuple(lst[1:])
406
      elif sname == "_remote_host":
407
        for lst in section[1:]:
408
          if lst[0] == "address":
409
            retval["remote_addr"] = tuple(lst[1:])
410
    return retval
411

    
412

    
413
class DRBD84ShowInfo(BaseShowInfo):
414
  @classmethod
415
  def _ConstructShowParser(cls):
416
    # an entire section (sections can be nested in DRBD 8.4, and there exist
417
    # sections like "volume 0")
418
    section_name = pyp.Word(pyp.alphas + "_") + \
419
                   pyp.Optional(pyp.Word(pyp.nums)).suppress() # skip volume idx
420
    section = pyp.Forward()
421
    # pylint: disable=W0106
422
    section << (section_name +
423
                cls._lbrace +
424
                pyp.ZeroOrMore(pyp.Group(cls._stmt ^ section)) +
425
                cls._rbrace)
426

    
427
    resource_name = pyp.Word(pyp.alphanums + "_-.")
428
    resource = (pyp.Literal("resource") + resource_name).suppress() + \
429
               cls._lbrace + \
430
               pyp.ZeroOrMore(pyp.Group(section)) + \
431
               cls._rbrace
432

    
433
    resource.ignore(cls._comment)
434

    
435
    return resource
436

    
437
  @classmethod
438
  def _TransformParseResult(cls, parse_result):
439
    retval = {}
440
    for section in parse_result:
441
      sname = section[0]
442
      if sname == "_this_host":
443
        for lst in section[1:]:
444
          if lst[0] == "address":
445
            retval["local_addr"] = tuple(lst[1:])
446
          elif lst[0] == "volume":
447
            for inner in lst[1:]:
448
              if inner[0] == "disk" and len(inner) == 2:
449
                retval["local_dev"] = inner[1]
450
              elif inner[0] == "meta-disk":
451
                if len(inner) > 1:
452
                  retval["meta_dev"] = inner[1]
453
                if len(inner) > 2:
454
                  retval["meta_index"] = inner[2]
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