Statistics
| Branch: | Tag: | Revision:

root / lib / storage / drbd_info.py @ 178ad717

History | View | Annotate | Download (14.5 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
  def __repr__(self):
146
    return ("<%s: cstatus=%s, lrole=%s, rrole=%s, ldisk=%s, rdisk=%s>" %
147
            (self.__class__, self.cstatus, self.lrole, self.rrole,
148
             self.ldisk, self.rdisk))
149

    
150

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

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

156
  """
157

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

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

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

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

178
    """
179
    return self._version
180

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

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

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

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

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

203
    """
204
    return self._minors
205

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

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

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

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

    
232
    return retval
233

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

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

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

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

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

    
282

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

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

289
  """
290
  _PARSE_SHOW = None
291

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

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

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

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

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

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

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

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

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

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

    
354
    return cls._TransformParseResult(results)
355

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

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

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

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

    
371
    return cls._PARSE_SHOW
372

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

    
377

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

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

    
391
    return bnf
392

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

    
413

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

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

    
434
    resource.ignore(cls._comment)
435

    
436
    return resource
437

    
438
  @classmethod
439
  def _TransformVolumeSection(cls, vol_content, retval):
440
    for entry in vol_content:
441
      if entry[0] == "disk" and len(entry) == 2 and \
442
          isinstance(entry[1], basestring):
443
        retval["local_dev"] = entry[1]
444
      elif entry[0] == "meta-disk":
445
        if len(entry) > 1:
446
          retval["meta_dev"] = entry[1]
447
        if len(entry) > 2:
448
          retval["meta_index"] = entry[2]
449

    
450
  @classmethod
451
  def _TransformParseResult(cls, parse_result):
452
    retval = {}
453
    for section in parse_result:
454
      sname = section[0]
455
      if sname == "_this_host":
456
        for lst in section[1:]:
457
          if lst[0] == "address":
458
            retval["local_addr"] = tuple(lst[1:])
459
          elif lst[0] == "volume":
460
            cls._TransformVolumeSection(lst[1:], retval)
461
      elif sname == "_remote_host":
462
        for lst in section[1:]:
463
          if lst[0] == "address":
464
            retval["remote_addr"] = tuple(lst[1:])
465
    return retval