Statistics
| Branch: | Tag: | Revision:

root / lib / bdev.py @ f208978a

History | View | Annotate | Download (58.9 kB)

1
#
2
#
3

    
4
# Copyright (C) 2006, 2007 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
"""Block device abstraction"""
23

    
24
import re
25
import time
26
import errno
27
import pyparsing as pyp
28
import os
29
import logging
30

    
31
from ganeti import utils
32
from ganeti import errors
33
from ganeti import constants
34
from ganeti import objects
35

    
36

    
37
def _IgnoreError(fn, *args, **kwargs):
38
  """Executes the given function, ignoring BlockDeviceErrors.
39

40
  This is used in order to simplify the execution of cleanup or
41
  rollback functions.
42

43
  @rtype: boolean
44
  @return: True when fn didn't raise an exception, False otherwise
45

46
  """
47
  try:
48
    fn(*args, **kwargs)
49
    return True
50
  except errors.BlockDeviceError, err:
51
    logging.warning("Caught BlockDeviceError but ignoring: %s" % str(err))
52
    return False
53

    
54

    
55
def _ThrowError(msg, *args):
56
  """Log an error to the node daemon and the raise an exception.
57

58
  @type msg: string
59
  @param msg: the text of the exception
60
  @raise errors.BlockDeviceError
61

62
  """
63
  if args:
64
    msg = msg % args
65
  logging.error(msg)
66
  raise errors.BlockDeviceError(msg)
67

    
68

    
69
class BlockDev(object):
70
  """Block device abstract class.
71

72
  A block device can be in the following states:
73
    - not existing on the system, and by `Create()` it goes into:
74
    - existing but not setup/not active, and by `Assemble()` goes into:
75
    - active read-write and by `Open()` it goes into
76
    - online (=used, or ready for use)
77

78
  A device can also be online but read-only, however we are not using
79
  the readonly state (LV has it, if needed in the future) and we are
80
  usually looking at this like at a stack, so it's easier to
81
  conceptualise the transition from not-existing to online and back
82
  like a linear one.
83

84
  The many different states of the device are due to the fact that we
85
  need to cover many device types:
86
    - logical volumes are created, lvchange -a y $lv, and used
87
    - drbd devices are attached to a local disk/remote peer and made primary
88

89
  A block device is identified by three items:
90
    - the /dev path of the device (dynamic)
91
    - a unique ID of the device (static)
92
    - it's major/minor pair (dynamic)
93

94
  Not all devices implement both the first two as distinct items. LVM
95
  logical volumes have their unique ID (the pair volume group, logical
96
  volume name) in a 1-to-1 relation to the dev path. For DRBD devices,
97
  the /dev path is again dynamic and the unique id is the pair (host1,
98
  dev1), (host2, dev2).
99

100
  You can get to a device in two ways:
101
    - creating the (real) device, which returns you
102
      an attached instance (lvcreate)
103
    - attaching of a python instance to an existing (real) device
104

105
  The second point, the attachement to a device, is different
106
  depending on whether the device is assembled or not. At init() time,
107
  we search for a device with the same unique_id as us. If found,
108
  good. It also means that the device is already assembled. If not,
109
  after assembly we'll have our correct major/minor.
110

111
  """
112
  def __init__(self, unique_id, children, size):
113
    self._children = children
114
    self.dev_path = None
115
    self.unique_id = unique_id
116
    self.major = None
117
    self.minor = None
118
    self.attached = False
119
    self.size = size
120

    
121
  def Assemble(self):
122
    """Assemble the device from its components.
123

124
    Implementations of this method by child classes must ensure that:
125
      - after the device has been assembled, it knows its major/minor
126
        numbers; this allows other devices (usually parents) to probe
127
        correctly for their children
128
      - calling this method on an existing, in-use device is safe
129
      - if the device is already configured (and in an OK state),
130
        this method is idempotent
131

132
    """
133
    pass
134

    
135
  def Attach(self):
136
    """Find a device which matches our config and attach to it.
137

138
    """
139
    raise NotImplementedError
140

    
141
  def Close(self):
142
    """Notifies that the device will no longer be used for I/O.
143

144
    """
145
    raise NotImplementedError
146

    
147
  @classmethod
148
  def Create(cls, unique_id, children, size):
149
    """Create the device.
150

151
    If the device cannot be created, it will return None
152
    instead. Error messages go to the logging system.
153

154
    Note that for some devices, the unique_id is used, and for other,
155
    the children. The idea is that these two, taken together, are
156
    enough for both creation and assembly (later).
157

158
    """
159
    raise NotImplementedError
160

    
161
  def Remove(self):
162
    """Remove this device.
163

164
    This makes sense only for some of the device types: LV and file
165
    storage. Also note that if the device can't attach, the removal
166
    can't be completed.
167

168
    """
169
    raise NotImplementedError
170

    
171
  def Rename(self, new_id):
172
    """Rename this device.
173

174
    This may or may not make sense for a given device type.
175

176
    """
177
    raise NotImplementedError
178

    
179
  def Open(self, force=False):
180
    """Make the device ready for use.
181

182
    This makes the device ready for I/O. For now, just the DRBD
183
    devices need this.
184

185
    The force parameter signifies that if the device has any kind of
186
    --force thing, it should be used, we know what we are doing.
187

188
    """
189
    raise NotImplementedError
190

    
191
  def Shutdown(self):
192
    """Shut down the device, freeing its children.
193

194
    This undoes the `Assemble()` work, except for the child
195
    assembling; as such, the children on the device are still
196
    assembled after this call.
197

198
    """
199
    raise NotImplementedError
200

    
201
  def SetSyncSpeed(self, speed):
202
    """Adjust the sync speed of the mirror.
203

204
    In case this is not a mirroring device, this is no-op.
205

206
    """
207
    result = True
208
    if self._children:
209
      for child in self._children:
210
        result = result and child.SetSyncSpeed(speed)
211
    return result
212

    
213
  def GetSyncStatus(self):
214
    """Returns the sync status of the device.
215

216
    If this device is a mirroring device, this function returns the
217
    status of the mirror.
218

219
    If sync_percent is None, it means the device is not syncing.
220

221
    If estimated_time is None, it means we can't estimate
222
    the time needed, otherwise it's the time left in seconds.
223

224
    If is_degraded is True, it means the device is missing
225
    redundancy. This is usually a sign that something went wrong in
226
    the device setup, if sync_percent is None.
227

228
    The ldisk parameter represents the degradation of the local
229
    data. This is only valid for some devices, the rest will always
230
    return False (not degraded).
231

232
    @rtype: objects.BlockDevStatus
233

234
    """
235
    return objects.BlockDevStatus(dev_path=self.dev_path,
236
                                  major=self.major,
237
                                  minor=self.minor,
238
                                  sync_percent=None,
239
                                  estimated_time=None,
240
                                  is_degraded=False,
241
                                  ldisk_status=constants.LDS_OKAY)
242

    
243
  def CombinedSyncStatus(self):
244
    """Calculate the mirror status recursively for our children.
245

246
    The return value is the same as for `GetSyncStatus()` except the
247
    minimum percent and maximum time are calculated across our
248
    children.
249

250
    @rtype: objects.BlockDevStatus
251

252
    """
253
    status = self.GetSyncStatus()
254

    
255
    min_percent = status.sync_percent
256
    max_time = status.estimated_time
257
    is_degraded = status.is_degraded
258
    ldisk_status = status.ldisk_status
259

    
260
    if self._children:
261
      for child in self._children:
262
        child_status = child.GetSyncStatus()
263

    
264
        if min_percent is None:
265
          min_percent = child_status.sync_percent
266
        elif child_status.sync_percent is not None:
267
          min_percent = min(min_percent, child_status.sync_percent)
268

    
269
        if max_time is None:
270
          max_time = child_status.estimated_time
271
        elif child_status.estimated_time is not None:
272
          max_time = max(max_time, child_status.estimated_time)
273

    
274
        is_degraded = is_degraded or child_status.is_degraded
275

    
276
        if ldisk_status is None:
277
          ldisk_status = child_status.ldisk_status
278
        elif child_status.ldisk_status is not None:
279
          ldisk_status = max(ldisk_status, child_status.ldisk_status)
280

    
281
    return objects.BlockDevStatus(dev_path=self.dev_path,
282
                                  major=self.major,
283
                                  minor=self.minor,
284
                                  sync_percent=min_percent,
285
                                  estimated_time=max_time,
286
                                  is_degraded=is_degraded,
287
                                  ldisk_status=ldisk_status)
288

    
289

    
290
  def SetInfo(self, text):
291
    """Update metadata with info text.
292

293
    Only supported for some device types.
294

295
    """
296
    for child in self._children:
297
      child.SetInfo(text)
298

    
299
  def Grow(self, amount):
300
    """Grow the block device.
301

302
    @param amount: the amount (in mebibytes) to grow with
303

304
    """
305
    raise NotImplementedError
306

    
307
  def __repr__(self):
308
    return ("<%s: unique_id: %s, children: %s, %s:%s, %s>" %
309
            (self.__class__, self.unique_id, self._children,
310
             self.major, self.minor, self.dev_path))
311

    
312

    
313
class LogicalVolume(BlockDev):
314
  """Logical Volume block device.
315

316
  """
317
  def __init__(self, unique_id, children, size):
318
    """Attaches to a LV device.
319

320
    The unique_id is a tuple (vg_name, lv_name)
321

322
    """
323
    super(LogicalVolume, self).__init__(unique_id, children, size)
324
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
325
      raise ValueError("Invalid configuration data %s" % str(unique_id))
326
    self._vg_name, self._lv_name = unique_id
327
    self.dev_path = "/dev/%s/%s" % (self._vg_name, self._lv_name)
328
    self._degraded = True
329
    self.major = self.minor = self.pe_size = self.stripe_count = None
330
    self.Attach()
331

    
332
  @classmethod
333
  def Create(cls, unique_id, children, size):
334
    """Create a new logical volume.
335

336
    """
337
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
338
      raise errors.ProgrammerError("Invalid configuration data %s" %
339
                                   str(unique_id))
340
    vg_name, lv_name = unique_id
341
    pvs_info = cls.GetPVInfo(vg_name)
342
    if not pvs_info:
343
      _ThrowError("Can't compute PV info for vg %s", vg_name)
344
    pvs_info.sort()
345
    pvs_info.reverse()
346

    
347
    pvlist = [ pv[1] for pv in pvs_info ]
348
    free_size = sum([ pv[0] for pv in pvs_info ])
349
    current_pvs = len(pvlist)
350
    stripes = min(current_pvs, constants.LVM_STRIPECOUNT)
351

    
352
    # The size constraint should have been checked from the master before
353
    # calling the create function.
354
    if free_size < size:
355
      _ThrowError("Not enough free space: required %s,"
356
                  " available %s", size, free_size)
357
    cmd = ["lvcreate", "-L%dm" % size, "-n%s" % lv_name]
358
    # If the free space is not well distributed, we won't be able to
359
    # create an optimally-striped volume; in that case, we want to try
360
    # with N, N-1, ..., 2, and finally 1 (non-stripped) number of
361
    # stripes
362
    for stripes_arg in range(stripes, 0, -1):
363
      result = utils.RunCmd(cmd + ["-i%d" % stripes_arg] + [vg_name] + pvlist)
364
      if not result.failed:
365
        break
366
    if result.failed:
367
      _ThrowError("LV create failed (%s): %s",
368
                  result.fail_reason, result.output)
369
    return LogicalVolume(unique_id, children, size)
370

    
371
  @staticmethod
372
  def GetPVInfo(vg_name):
373
    """Get the free space info for PVs in a volume group.
374

375
    @param vg_name: the volume group name
376

377
    @rtype: list
378
    @return: list of tuples (free_space, name) with free_space in mebibytes
379

380
    """
381
    command = ["pvs", "--noheadings", "--nosuffix", "--units=m",
382
               "-opv_name,vg_name,pv_free,pv_attr", "--unbuffered",
383
               "--separator=:"]
384
    result = utils.RunCmd(command)
385
    if result.failed:
386
      logging.error("Can't get the PV information: %s - %s",
387
                    result.fail_reason, result.output)
388
      return None
389
    data = []
390
    for line in result.stdout.splitlines():
391
      fields = line.strip().split(':')
392
      if len(fields) != 4:
393
        logging.error("Can't parse pvs output: line '%s'", line)
394
        return None
395
      # skip over pvs from another vg or ones which are not allocatable
396
      if fields[1] != vg_name or fields[3][0] != 'a':
397
        continue
398
      data.append((float(fields[2]), fields[0]))
399

    
400
    return data
401

    
402
  def Remove(self):
403
    """Remove this logical volume.
404

405
    """
406
    if not self.minor and not self.Attach():
407
      # the LV does not exist
408
      return
409
    result = utils.RunCmd(["lvremove", "-f", "%s/%s" %
410
                           (self._vg_name, self._lv_name)])
411
    if result.failed:
412
      _ThrowError("Can't lvremove: %s - %s", result.fail_reason, result.output)
413

    
414
  def Rename(self, new_id):
415
    """Rename this logical volume.
416

417
    """
418
    if not isinstance(new_id, (tuple, list)) or len(new_id) != 2:
419
      raise errors.ProgrammerError("Invalid new logical id '%s'" % new_id)
420
    new_vg, new_name = new_id
421
    if new_vg != self._vg_name:
422
      raise errors.ProgrammerError("Can't move a logical volume across"
423
                                   " volume groups (from %s to to %s)" %
424
                                   (self._vg_name, new_vg))
425
    result = utils.RunCmd(["lvrename", new_vg, self._lv_name, new_name])
426
    if result.failed:
427
      _ThrowError("Failed to rename the logical volume: %s", result.output)
428
    self._lv_name = new_name
429
    self.dev_path = "/dev/%s/%s" % (self._vg_name, self._lv_name)
430

    
431
  def Attach(self):
432
    """Attach to an existing LV.
433

434
    This method will try to see if an existing and active LV exists
435
    which matches our name. If so, its major/minor will be
436
    recorded.
437

438
    """
439
    self.attached = False
440
    result = utils.RunCmd(["lvs", "--noheadings", "--separator=,",
441
                           "--units=m", "--nosuffix",
442
                           "-olv_attr,lv_kernel_major,lv_kernel_minor,"
443
                           "vg_extent_size,stripes", self.dev_path])
444
    if result.failed:
445
      logging.error("Can't find LV %s: %s, %s",
446
                    self.dev_path, result.fail_reason, result.output)
447
      return False
448
    # the output can (and will) have multiple lines for multi-segment
449
    # LVs, as the 'stripes' parameter is a segment one, so we take
450
    # only the last entry, which is the one we're interested in; note
451
    # that with LVM2 anyway the 'stripes' value must be constant
452
    # across segments, so this is a no-op actually
453
    out = result.stdout.splitlines()
454
    if not out: # totally empty result? splitlines() returns at least
455
                # one line for any non-empty string
456
      logging.error("Can't parse LVS output, no lines? Got '%s'", str(out))
457
      return False
458
    out = out[-1].strip().rstrip(',')
459
    out = out.split(",")
460
    if len(out) != 5:
461
      logging.error("Can't parse LVS output, len(%s) != 5", str(out))
462
      return False
463

    
464
    status, major, minor, pe_size, stripes = out
465
    if len(status) != 6:
466
      logging.error("lvs lv_attr is not 6 characters (%s)", status)
467
      return False
468

    
469
    try:
470
      major = int(major)
471
      minor = int(minor)
472
    except ValueError, err:
473
      logging.error("lvs major/minor cannot be parsed: %s", str(err))
474

    
475
    try:
476
      pe_size = int(float(pe_size))
477
    except (TypeError, ValueError), err:
478
      logging.error("Can't parse vg extent size: %s", err)
479
      return False
480

    
481
    try:
482
      stripes = int(stripes)
483
    except (TypeError, ValueError), err:
484
      logging.error("Can't parse the number of stripes: %s", err)
485
      return False
486

    
487
    self.major = major
488
    self.minor = minor
489
    self.pe_size = pe_size
490
    self.stripe_count = stripes
491
    self._degraded = status[0] == 'v' # virtual volume, i.e. doesn't backing
492
                                      # storage
493
    self.attached = True
494
    return True
495

    
496
  def Assemble(self):
497
    """Assemble the device.
498

499
    We always run `lvchange -ay` on the LV to ensure it's active before
500
    use, as there were cases when xenvg was not active after boot
501
    (also possibly after disk issues).
502

503
    """
504
    result = utils.RunCmd(["lvchange", "-ay", self.dev_path])
505
    if result.failed:
506
      _ThrowError("Can't activate lv %s: %s", self.dev_path, result.output)
507

    
508
  def Shutdown(self):
509
    """Shutdown the device.
510

511
    This is a no-op for the LV device type, as we don't deactivate the
512
    volumes on shutdown.
513

514
    """
515
    pass
516

    
517
  def GetSyncStatus(self):
518
    """Returns the sync status of the device.
519

520
    If this device is a mirroring device, this function returns the
521
    status of the mirror.
522

523
    For logical volumes, sync_percent and estimated_time are always
524
    None (no recovery in progress, as we don't handle the mirrored LV
525
    case). The is_degraded parameter is the inverse of the ldisk
526
    parameter.
527

528
    For the ldisk parameter, we check if the logical volume has the
529
    'virtual' type, which means it's not backed by existing storage
530
    anymore (read from it return I/O error). This happens after a
531
    physical disk failure and subsequent 'vgreduce --removemissing' on
532
    the volume group.
533

534
    The status was already read in Attach, so we just return it.
535

536
    @rtype: objects.BlockDevStatus
537

538
    """
539
    if self._degraded:
540
      ldisk_status = constants.LDS_FAULTY
541
    else:
542
      ldisk_status = constants.LDS_OKAY
543

    
544
    return objects.BlockDevStatus(dev_path=self.dev_path,
545
                                  major=self.major,
546
                                  minor=self.minor,
547
                                  sync_percent=None,
548
                                  estimated_time=None,
549
                                  is_degraded=self._degraded,
550
                                  ldisk_status=ldisk_status)
551

    
552
  def Open(self, force=False):
553
    """Make the device ready for I/O.
554

555
    This is a no-op for the LV device type.
556

557
    """
558
    pass
559

    
560
  def Close(self):
561
    """Notifies that the device will no longer be used for I/O.
562

563
    This is a no-op for the LV device type.
564

565
    """
566
    pass
567

    
568
  def Snapshot(self, size):
569
    """Create a snapshot copy of an lvm block device.
570

571
    """
572
    snap_name = self._lv_name + ".snap"
573

    
574
    # remove existing snapshot if found
575
    snap = LogicalVolume((self._vg_name, snap_name), None, size)
576
    _IgnoreError(snap.Remove)
577

    
578
    pvs_info = self.GetPVInfo(self._vg_name)
579
    if not pvs_info:
580
      _ThrowError("Can't compute PV info for vg %s", self._vg_name)
581
    pvs_info.sort()
582
    pvs_info.reverse()
583
    free_size, pv_name = pvs_info[0]
584
    if free_size < size:
585
      _ThrowError("Not enough free space: required %s,"
586
                  " available %s", size, free_size)
587

    
588
    result = utils.RunCmd(["lvcreate", "-L%dm" % size, "-s",
589
                           "-n%s" % snap_name, self.dev_path])
590
    if result.failed:
591
      _ThrowError("command: %s error: %s - %s",
592
                  result.cmd, result.fail_reason, result.output)
593

    
594
    return snap_name
595

    
596
  def SetInfo(self, text):
597
    """Update metadata with info text.
598

599
    """
600
    BlockDev.SetInfo(self, text)
601

    
602
    # Replace invalid characters
603
    text = re.sub('^[^A-Za-z0-9_+.]', '_', text)
604
    text = re.sub('[^-A-Za-z0-9_+.]', '_', text)
605

    
606
    # Only up to 128 characters are allowed
607
    text = text[:128]
608

    
609
    result = utils.RunCmd(["lvchange", "--addtag", text,
610
                           self.dev_path])
611
    if result.failed:
612
      _ThrowError("Command: %s error: %s - %s", result.cmd, result.fail_reason,
613
                  result.output)
614

    
615
  def Grow(self, amount):
616
    """Grow the logical volume.
617

618
    """
619
    if self.pe_size is None or self.stripe_count is None:
620
      if not self.Attach():
621
        _ThrowError("Can't attach to LV during Grow()")
622
    full_stripe_size = self.pe_size * self.stripe_count
623
    rest = amount % full_stripe_size
624
    if rest != 0:
625
      amount += full_stripe_size - rest
626
    # we try multiple algorithms since the 'best' ones might not have
627
    # space available in the right place, but later ones might (since
628
    # they have less constraints); also note that only recent LVM
629
    # supports 'cling'
630
    for alloc_policy in "contiguous", "cling", "normal":
631
      result = utils.RunCmd(["lvextend", "--alloc", alloc_policy,
632
                             "-L", "+%dm" % amount, self.dev_path])
633
      if not result.failed:
634
        return
635
    _ThrowError("Can't grow LV %s: %s", self.dev_path, result.output)
636

    
637

    
638
class DRBD8Status(object):
639
  """A DRBD status representation class.
640

641
  Note that this doesn't support unconfigured devices (cs:Unconfigured).
642

643
  """
644
  UNCONF_RE = re.compile(r"\s*[0-9]+:\s*cs:Unconfigured$")
645
  LINE_RE = re.compile(r"\s*[0-9]+:\s*cs:(\S+)\s+(?:st|ro):([^/]+)/(\S+)"
646
                       "\s+ds:([^/]+)/(\S+)\s+.*$")
647
  SYNC_RE = re.compile(r"^.*\ssync'ed:\s*([0-9.]+)%.*"
648
                       "\sfinish: ([0-9]+):([0-9]+):([0-9]+)\s.*$")
649

    
650
  CS_UNCONFIGURED = "Unconfigured"
651
  CS_STANDALONE = "StandAlone"
652
  CS_WFCONNECTION = "WFConnection"
653
  CS_WFREPORTPARAMS = "WFReportParams"
654
  CS_CONNECTED = "Connected"
655
  CS_STARTINGSYNCS = "StartingSyncS"
656
  CS_STARTINGSYNCT = "StartingSyncT"
657
  CS_WFBITMAPS = "WFBitMapS"
658
  CS_WFBITMAPT = "WFBitMapT"
659
  CS_WFSYNCUUID = "WFSyncUUID"
660
  CS_SYNCSOURCE = "SyncSource"
661
  CS_SYNCTARGET = "SyncTarget"
662
  CS_PAUSEDSYNCS = "PausedSyncS"
663
  CS_PAUSEDSYNCT = "PausedSyncT"
664
  CSET_SYNC = frozenset([
665
    CS_WFREPORTPARAMS,
666
    CS_STARTINGSYNCS,
667
    CS_STARTINGSYNCT,
668
    CS_WFBITMAPS,
669
    CS_WFBITMAPT,
670
    CS_WFSYNCUUID,
671
    CS_SYNCSOURCE,
672
    CS_SYNCTARGET,
673
    CS_PAUSEDSYNCS,
674
    CS_PAUSEDSYNCT,
675
    ])
676

    
677
  DS_DISKLESS = "Diskless"
678
  DS_ATTACHING = "Attaching" # transient state
679
  DS_FAILED = "Failed" # transient state, next: diskless
680
  DS_NEGOTIATING = "Negotiating" # transient state
681
  DS_INCONSISTENT = "Inconsistent" # while syncing or after creation
682
  DS_OUTDATED = "Outdated"
683
  DS_DUNKNOWN = "DUnknown" # shown for peer disk when not connected
684
  DS_CONSISTENT = "Consistent"
685
  DS_UPTODATE = "UpToDate" # normal state
686

    
687
  RO_PRIMARY = "Primary"
688
  RO_SECONDARY = "Secondary"
689
  RO_UNKNOWN = "Unknown"
690

    
691
  def __init__(self, procline):
692
    u = self.UNCONF_RE.match(procline)
693
    if u:
694
      self.cstatus = self.CS_UNCONFIGURED
695
      self.lrole = self.rrole = self.ldisk = self.rdisk = None
696
    else:
697
      m = self.LINE_RE.match(procline)
698
      if not m:
699
        raise errors.BlockDeviceError("Can't parse input data '%s'" % procline)
700
      self.cstatus = m.group(1)
701
      self.lrole = m.group(2)
702
      self.rrole = m.group(3)
703
      self.ldisk = m.group(4)
704
      self.rdisk = m.group(5)
705

    
706
    # end reading of data from the LINE_RE or UNCONF_RE
707

    
708
    self.is_standalone = self.cstatus == self.CS_STANDALONE
709
    self.is_wfconn = self.cstatus == self.CS_WFCONNECTION
710
    self.is_connected = self.cstatus == self.CS_CONNECTED
711
    self.is_primary = self.lrole == self.RO_PRIMARY
712
    self.is_secondary = self.lrole == self.RO_SECONDARY
713
    self.peer_primary = self.rrole == self.RO_PRIMARY
714
    self.peer_secondary = self.rrole == self.RO_SECONDARY
715
    self.both_primary = self.is_primary and self.peer_primary
716
    self.both_secondary = self.is_secondary and self.peer_secondary
717

    
718
    self.is_diskless = self.ldisk == self.DS_DISKLESS
719
    self.is_disk_uptodate = self.ldisk == self.DS_UPTODATE
720

    
721
    self.is_in_resync = self.cstatus in self.CSET_SYNC
722
    self.is_in_use = self.cstatus != self.CS_UNCONFIGURED
723

    
724
    m = self.SYNC_RE.match(procline)
725
    if m:
726
      self.sync_percent = float(m.group(1))
727
      hours = int(m.group(2))
728
      minutes = int(m.group(3))
729
      seconds = int(m.group(4))
730
      self.est_time = hours * 3600 + minutes * 60 + seconds
731
    else:
732
      # we have (in this if branch) no percent information, but if
733
      # we're resyncing we need to 'fake' a sync percent information,
734
      # as this is how cmdlib determines if it makes sense to wait for
735
      # resyncing or not
736
      if self.is_in_resync:
737
        self.sync_percent = 0
738
      else:
739
        self.sync_percent = None
740
      self.est_time = None
741

    
742

    
743
class BaseDRBD(BlockDev):
744
  """Base DRBD class.
745

746
  This class contains a few bits of common functionality between the
747
  0.7 and 8.x versions of DRBD.
748

749
  """
750
  _VERSION_RE = re.compile(r"^version: (\d+)\.(\d+)\.(\d+)"
751
                           r" \(api:(\d+)/proto:(\d+)(?:-(\d+))?\)")
752

    
753
  _DRBD_MAJOR = 147
754
  _ST_UNCONFIGURED = "Unconfigured"
755
  _ST_WFCONNECTION = "WFConnection"
756
  _ST_CONNECTED = "Connected"
757

    
758
  _STATUS_FILE = "/proc/drbd"
759

    
760
  @staticmethod
761
  def _GetProcData(filename=_STATUS_FILE):
762
    """Return data from /proc/drbd.
763

764
    """
765
    try:
766
      stat = open(filename, "r")
767
      try:
768
        data = stat.read().splitlines()
769
      finally:
770
        stat.close()
771
    except EnvironmentError, err:
772
      if err.errno == errno.ENOENT:
773
        _ThrowError("The file %s cannot be opened, check if the module"
774
                    " is loaded (%s)", filename, str(err))
775
      else:
776
        _ThrowError("Can't read the DRBD proc file %s: %s", filename, str(err))
777
    if not data:
778
      _ThrowError("Can't read any data from %s", filename)
779
    return data
780

    
781
  @staticmethod
782
  def _MassageProcData(data):
783
    """Transform the output of _GetProdData into a nicer form.
784

785
    @return: a dictionary of minor: joined lines from /proc/drbd
786
        for that minor
787

788
    """
789
    lmatch = re.compile("^ *([0-9]+):.*$")
790
    results = {}
791
    old_minor = old_line = None
792
    for line in data:
793
      lresult = lmatch.match(line)
794
      if lresult is not None:
795
        if old_minor is not None:
796
          results[old_minor] = old_line
797
        old_minor = int(lresult.group(1))
798
        old_line = line
799
      else:
800
        if old_minor is not None:
801
          old_line += " " + line.strip()
802
    # add last line
803
    if old_minor is not None:
804
      results[old_minor] = old_line
805
    return results
806

    
807
  @classmethod
808
  def _GetVersion(cls):
809
    """Return the DRBD version.
810

811
    This will return a dict with keys:
812
      - k_major
813
      - k_minor
814
      - k_point
815
      - api
816
      - proto
817
      - proto2 (only on drbd > 8.2.X)
818

819
    """
820
    proc_data = cls._GetProcData()
821
    first_line = proc_data[0].strip()
822
    version = cls._VERSION_RE.match(first_line)
823
    if not version:
824
      raise errors.BlockDeviceError("Can't parse DRBD version from '%s'" %
825
                                    first_line)
826

    
827
    values = version.groups()
828
    retval = {'k_major': int(values[0]),
829
              'k_minor': int(values[1]),
830
              'k_point': int(values[2]),
831
              'api': int(values[3]),
832
              'proto': int(values[4]),
833
             }
834
    if values[5] is not None:
835
      retval['proto2'] = values[5]
836

    
837
    return retval
838

    
839
  @staticmethod
840
  def _DevPath(minor):
841
    """Return the path to a drbd device for a given minor.
842

843
    """
844
    return "/dev/drbd%d" % minor
845

    
846
  @classmethod
847
  def GetUsedDevs(cls):
848
    """Compute the list of used DRBD devices.
849

850
    """
851
    data = cls._GetProcData()
852

    
853
    used_devs = {}
854
    valid_line = re.compile("^ *([0-9]+): cs:([^ ]+).*$")
855
    for line in data:
856
      match = valid_line.match(line)
857
      if not match:
858
        continue
859
      minor = int(match.group(1))
860
      state = match.group(2)
861
      if state == cls._ST_UNCONFIGURED:
862
        continue
863
      used_devs[minor] = state, line
864

    
865
    return used_devs
866

    
867
  def _SetFromMinor(self, minor):
868
    """Set our parameters based on the given minor.
869

870
    This sets our minor variable and our dev_path.
871

872
    """
873
    if minor is None:
874
      self.minor = self.dev_path = None
875
      self.attached = False
876
    else:
877
      self.minor = minor
878
      self.dev_path = self._DevPath(minor)
879
      self.attached = True
880

    
881
  @staticmethod
882
  def _CheckMetaSize(meta_device):
883
    """Check if the given meta device looks like a valid one.
884

885
    This currently only check the size, which must be around
886
    128MiB.
887

888
    """
889
    result = utils.RunCmd(["blockdev", "--getsize", meta_device])
890
    if result.failed:
891
      _ThrowError("Failed to get device size: %s - %s",
892
                  result.fail_reason, result.output)
893
    try:
894
      sectors = int(result.stdout)
895
    except ValueError:
896
      _ThrowError("Invalid output from blockdev: '%s'", result.stdout)
897
    bytes = sectors * 512
898
    if bytes < 128 * 1024 * 1024: # less than 128MiB
899
      _ThrowError("Meta device too small (%.2fMib)", (bytes / 1024 / 1024))
900
    # the maximum *valid* size of the meta device when living on top
901
    # of LVM is hard to compute: it depends on the number of stripes
902
    # and the PE size; e.g. a 2-stripe, 64MB PE will result in a 128MB
903
    # (normal size), but an eight-stripe 128MB PE will result in a 1GB
904
    # size meta device; as such, we restrict it to 1GB (a little bit
905
    # too generous, but making assumptions about PE size is hard)
906
    if bytes > 1024 * 1024 * 1024:
907
      _ThrowError("Meta device too big (%.2fMiB)", (bytes / 1024 / 1024))
908

    
909
  def Rename(self, new_id):
910
    """Rename a device.
911

912
    This is not supported for drbd devices.
913

914
    """
915
    raise errors.ProgrammerError("Can't rename a drbd device")
916

    
917

    
918
class DRBD8(BaseDRBD):
919
  """DRBD v8.x block device.
920

921
  This implements the local host part of the DRBD device, i.e. it
922
  doesn't do anything to the supposed peer. If you need a fully
923
  connected DRBD pair, you need to use this class on both hosts.
924

925
  The unique_id for the drbd device is the (local_ip, local_port,
926
  remote_ip, remote_port) tuple, and it must have two children: the
927
  data device and the meta_device. The meta device is checked for
928
  valid size and is zeroed on create.
929

930
  """
931
  _MAX_MINORS = 255
932
  _PARSE_SHOW = None
933

    
934
  # timeout constants
935
  _NET_RECONFIG_TIMEOUT = 60
936

    
937
  def __init__(self, unique_id, children, size):
938
    if children and children.count(None) > 0:
939
      children = []
940
    super(DRBD8, self).__init__(unique_id, children, size)
941
    self.major = self._DRBD_MAJOR
942
    version = self._GetVersion()
943
    if version['k_major'] != 8 :
944
      _ThrowError("Mismatch in DRBD kernel version and requested ganeti"
945
                  " usage: kernel is %s.%s, ganeti wants 8.x",
946
                  version['k_major'], version['k_minor'])
947

    
948
    if len(children) not in (0, 2):
949
      raise ValueError("Invalid configuration data %s" % str(children))
950
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 6:
951
      raise ValueError("Invalid configuration data %s" % str(unique_id))
952
    (self._lhost, self._lport,
953
     self._rhost, self._rport,
954
     self._aminor, self._secret) = unique_id
955
    if (self._lhost is not None and self._lhost == self._rhost and
956
        self._lport == self._rport):
957
      raise ValueError("Invalid configuration data, same local/remote %s" %
958
                       (unique_id,))
959
    self.Attach()
960

    
961
  @classmethod
962
  def _InitMeta(cls, minor, dev_path):
963
    """Initialize a meta device.
964

965
    This will not work if the given minor is in use.
966

967
    """
968
    result = utils.RunCmd(["drbdmeta", "--force", cls._DevPath(minor),
969
                           "v08", dev_path, "0", "create-md"])
970
    if result.failed:
971
      _ThrowError("Can't initialize meta device: %s", result.output)
972

    
973
  @classmethod
974
  def _FindUnusedMinor(cls):
975
    """Find an unused DRBD device.
976

977
    This is specific to 8.x as the minors are allocated dynamically,
978
    so non-existing numbers up to a max minor count are actually free.
979

980
    """
981
    data = cls._GetProcData()
982

    
983
    unused_line = re.compile("^ *([0-9]+): cs:Unconfigured$")
984
    used_line = re.compile("^ *([0-9]+): cs:")
985
    highest = None
986
    for line in data:
987
      match = unused_line.match(line)
988
      if match:
989
        return int(match.group(1))
990
      match = used_line.match(line)
991
      if match:
992
        minor = int(match.group(1))
993
        highest = max(highest, minor)
994
    if highest is None: # there are no minors in use at all
995
      return 0
996
    if highest >= cls._MAX_MINORS:
997
      logging.error("Error: no free drbd minors!")
998
      raise errors.BlockDeviceError("Can't find a free DRBD minor")
999
    return highest + 1
1000

    
1001
  @classmethod
1002
  def _GetShowParser(cls):
1003
    """Return a parser for `drbd show` output.
1004

1005
    This will either create or return an already-create parser for the
1006
    output of the command `drbd show`.
1007

1008
    """
1009
    if cls._PARSE_SHOW is not None:
1010
      return cls._PARSE_SHOW
1011

    
1012
    # pyparsing setup
1013
    lbrace = pyp.Literal("{").suppress()
1014
    rbrace = pyp.Literal("}").suppress()
1015
    semi = pyp.Literal(";").suppress()
1016
    # this also converts the value to an int
1017
    number = pyp.Word(pyp.nums).setParseAction(lambda s, l, t: int(t[0]))
1018

    
1019
    comment = pyp.Literal ("#") + pyp.Optional(pyp.restOfLine)
1020
    defa = pyp.Literal("_is_default").suppress()
1021
    dbl_quote = pyp.Literal('"').suppress()
1022

    
1023
    keyword = pyp.Word(pyp.alphanums + '-')
1024

    
1025
    # value types
1026
    value = pyp.Word(pyp.alphanums + '_-/.:')
1027
    quoted = dbl_quote + pyp.CharsNotIn('"') + dbl_quote
1028
    addr_type = (pyp.Optional(pyp.Literal("ipv4")).suppress() +
1029
                 pyp.Optional(pyp.Literal("ipv6")).suppress())
1030
    addr_port = (addr_type + pyp.Word(pyp.nums + '.') +
1031
                 pyp.Literal(':').suppress() + number)
1032
    # meta device, extended syntax
1033
    meta_value = ((value ^ quoted) + pyp.Literal('[').suppress() +
1034
                  number + pyp.Word(']').suppress())
1035
    # device name, extended syntax
1036
    device_value = pyp.Literal("minor").suppress() + number
1037

    
1038
    # a statement
1039
    stmt = (~rbrace + keyword + ~lbrace +
1040
            pyp.Optional(addr_port ^ value ^ quoted ^ meta_value ^
1041
                         device_value) +
1042
            pyp.Optional(defa) + semi +
1043
            pyp.Optional(pyp.restOfLine).suppress())
1044

    
1045
    # an entire section
1046
    section_name = pyp.Word(pyp.alphas + '_')
1047
    section = section_name + lbrace + pyp.ZeroOrMore(pyp.Group(stmt)) + rbrace
1048

    
1049
    bnf = pyp.ZeroOrMore(pyp.Group(section ^ stmt))
1050
    bnf.ignore(comment)
1051

    
1052
    cls._PARSE_SHOW = bnf
1053

    
1054
    return bnf
1055

    
1056
  @classmethod
1057
  def _GetShowData(cls, minor):
1058
    """Return the `drbdsetup show` data for a minor.
1059

1060
    """
1061
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "show"])
1062
    if result.failed:
1063
      logging.error("Can't display the drbd config: %s - %s",
1064
                    result.fail_reason, result.output)
1065
      return None
1066
    return result.stdout
1067

    
1068
  @classmethod
1069
  def _GetDevInfo(cls, out):
1070
    """Parse details about a given DRBD minor.
1071

1072
    This return, if available, the local backing device (as a path)
1073
    and the local and remote (ip, port) information from a string
1074
    containing the output of the `drbdsetup show` command as returned
1075
    by _GetShowData.
1076

1077
    """
1078
    data = {}
1079
    if not out:
1080
      return data
1081

    
1082
    bnf = cls._GetShowParser()
1083
    # run pyparse
1084

    
1085
    try:
1086
      results = bnf.parseString(out)
1087
    except pyp.ParseException, err:
1088
      _ThrowError("Can't parse drbdsetup show output: %s", str(err))
1089

    
1090
    # and massage the results into our desired format
1091
    for section in results:
1092
      sname = section[0]
1093
      if sname == "_this_host":
1094
        for lst in section[1:]:
1095
          if lst[0] == "disk":
1096
            data["local_dev"] = lst[1]
1097
          elif lst[0] == "meta-disk":
1098
            data["meta_dev"] = lst[1]
1099
            data["meta_index"] = lst[2]
1100
          elif lst[0] == "address":
1101
            data["local_addr"] = tuple(lst[1:])
1102
      elif sname == "_remote_host":
1103
        for lst in section[1:]:
1104
          if lst[0] == "address":
1105
            data["remote_addr"] = tuple(lst[1:])
1106
    return data
1107

    
1108
  def _MatchesLocal(self, info):
1109
    """Test if our local config matches with an existing device.
1110

1111
    The parameter should be as returned from `_GetDevInfo()`. This
1112
    method tests if our local backing device is the same as the one in
1113
    the info parameter, in effect testing if we look like the given
1114
    device.
1115

1116
    """
1117
    if self._children:
1118
      backend, meta = self._children
1119
    else:
1120
      backend = meta = None
1121

    
1122
    if backend is not None:
1123
      retval = ("local_dev" in info and info["local_dev"] == backend.dev_path)
1124
    else:
1125
      retval = ("local_dev" not in info)
1126

    
1127
    if meta is not None:
1128
      retval = retval and ("meta_dev" in info and
1129
                           info["meta_dev"] == meta.dev_path)
1130
      retval = retval and ("meta_index" in info and
1131
                           info["meta_index"] == 0)
1132
    else:
1133
      retval = retval and ("meta_dev" not in info and
1134
                           "meta_index" not in info)
1135
    return retval
1136

    
1137
  def _MatchesNet(self, info):
1138
    """Test if our network config matches with an existing device.
1139

1140
    The parameter should be as returned from `_GetDevInfo()`. This
1141
    method tests if our network configuration is the same as the one
1142
    in the info parameter, in effect testing if we look like the given
1143
    device.
1144

1145
    """
1146
    if (((self._lhost is None and not ("local_addr" in info)) and
1147
         (self._rhost is None and not ("remote_addr" in info)))):
1148
      return True
1149

    
1150
    if self._lhost is None:
1151
      return False
1152

    
1153
    if not ("local_addr" in info and
1154
            "remote_addr" in info):
1155
      return False
1156

    
1157
    retval = (info["local_addr"] == (self._lhost, self._lport))
1158
    retval = (retval and
1159
              info["remote_addr"] == (self._rhost, self._rport))
1160
    return retval
1161

    
1162
  @classmethod
1163
  def _AssembleLocal(cls, minor, backend, meta, size):
1164
    """Configure the local part of a DRBD device.
1165

1166
    """
1167
    args = ["drbdsetup", cls._DevPath(minor), "disk",
1168
            backend, meta, "0",
1169
            "-d", "%sm" % size,
1170
            "-e", "detach",
1171
            "--create-device"]
1172
    result = utils.RunCmd(args)
1173
    if result.failed:
1174
      _ThrowError("drbd%d: can't attach local disk: %s", minor, result.output)
1175

    
1176
  @classmethod
1177
  def _AssembleNet(cls, minor, net_info, protocol,
1178
                   dual_pri=False, hmac=None, secret=None):
1179
    """Configure the network part of the device.
1180

1181
    """
1182
    lhost, lport, rhost, rport = net_info
1183
    if None in net_info:
1184
      # we don't want network connection and actually want to make
1185
      # sure its shutdown
1186
      cls._ShutdownNet(minor)
1187
      return
1188

    
1189
    # Workaround for a race condition. When DRBD is doing its dance to
1190
    # establish a connection with its peer, it also sends the
1191
    # synchronization speed over the wire. In some cases setting the
1192
    # sync speed only after setting up both sides can race with DRBD
1193
    # connecting, hence we set it here before telling DRBD anything
1194
    # about its peer.
1195
    cls._SetMinorSyncSpeed(minor, constants.SYNC_SPEED)
1196

    
1197
    args = ["drbdsetup", cls._DevPath(minor), "net",
1198
            "%s:%s" % (lhost, lport), "%s:%s" % (rhost, rport), protocol,
1199
            "-A", "discard-zero-changes",
1200
            "-B", "consensus",
1201
            "--create-device",
1202
            ]
1203
    if dual_pri:
1204
      args.append("-m")
1205
    if hmac and secret:
1206
      args.extend(["-a", hmac, "-x", secret])
1207
    result = utils.RunCmd(args)
1208
    if result.failed:
1209
      _ThrowError("drbd%d: can't setup network: %s - %s",
1210
                  minor, result.fail_reason, result.output)
1211

    
1212
    timeout = time.time() + 10
1213
    ok = False
1214
    while time.time() < timeout:
1215
      info = cls._GetDevInfo(cls._GetShowData(minor))
1216
      if not "local_addr" in info or not "remote_addr" in info:
1217
        time.sleep(1)
1218
        continue
1219
      if (info["local_addr"] != (lhost, lport) or
1220
          info["remote_addr"] != (rhost, rport)):
1221
        time.sleep(1)
1222
        continue
1223
      ok = True
1224
      break
1225
    if not ok:
1226
      _ThrowError("drbd%d: timeout while configuring network", minor)
1227

    
1228
  def AddChildren(self, devices):
1229
    """Add a disk to the DRBD device.
1230

1231
    """
1232
    if self.minor is None:
1233
      _ThrowError("drbd%d: can't attach to dbrd8 during AddChildren",
1234
                  self._aminor)
1235
    if len(devices) != 2:
1236
      _ThrowError("drbd%d: need two devices for AddChildren", self.minor)
1237
    info = self._GetDevInfo(self._GetShowData(self.minor))
1238
    if "local_dev" in info:
1239
      _ThrowError("drbd%d: already attached to a local disk", self.minor)
1240
    backend, meta = devices
1241
    if backend.dev_path is None or meta.dev_path is None:
1242
      _ThrowError("drbd%d: children not ready during AddChildren", self.minor)
1243
    backend.Open()
1244
    meta.Open()
1245
    self._CheckMetaSize(meta.dev_path)
1246
    self._InitMeta(self._FindUnusedMinor(), meta.dev_path)
1247

    
1248
    self._AssembleLocal(self.minor, backend.dev_path, meta.dev_path, self.size)
1249
    self._children = devices
1250

    
1251
  def RemoveChildren(self, devices):
1252
    """Detach the drbd device from local storage.
1253

1254
    """
1255
    if self.minor is None:
1256
      _ThrowError("drbd%d: can't attach to drbd8 during RemoveChildren",
1257
                  self._aminor)
1258
    # early return if we don't actually have backing storage
1259
    info = self._GetDevInfo(self._GetShowData(self.minor))
1260
    if "local_dev" not in info:
1261
      return
1262
    if len(self._children) != 2:
1263
      _ThrowError("drbd%d: we don't have two children: %s", self.minor,
1264
                  self._children)
1265
    if self._children.count(None) == 2: # we don't actually have children :)
1266
      logging.warning("drbd%d: requested detach while detached", self.minor)
1267
      return
1268
    if len(devices) != 2:
1269
      _ThrowError("drbd%d: we need two children in RemoveChildren", self.minor)
1270
    for child, dev in zip(self._children, devices):
1271
      if dev != child.dev_path:
1272
        _ThrowError("drbd%d: mismatch in local storage (%s != %s) in"
1273
                    " RemoveChildren", self.minor, dev, child.dev_path)
1274

    
1275
    self._ShutdownLocal(self.minor)
1276
    self._children = []
1277

    
1278
  @classmethod
1279
  def _SetMinorSyncSpeed(cls, minor, kbytes):
1280
    """Set the speed of the DRBD syncer.
1281

1282
    This is the low-level implementation.
1283

1284
    @type minor: int
1285
    @param minor: the drbd minor whose settings we change
1286
    @type kbytes: int
1287
    @param kbytes: the speed in kbytes/second
1288
    @rtype: boolean
1289
    @return: the success of the operation
1290

1291
    """
1292
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "syncer",
1293
                           "-r", "%d" % kbytes, "--create-device"])
1294
    if result.failed:
1295
      logging.error("Can't change syncer rate: %s - %s",
1296
                    result.fail_reason, result.output)
1297
    return not result.failed
1298

    
1299
  def SetSyncSpeed(self, kbytes):
1300
    """Set the speed of the DRBD syncer.
1301

1302
    @type kbytes: int
1303
    @param kbytes: the speed in kbytes/second
1304
    @rtype: boolean
1305
    @return: the success of the operation
1306

1307
    """
1308
    if self.minor is None:
1309
      logging.info("Not attached during SetSyncSpeed")
1310
      return False
1311
    children_result = super(DRBD8, self).SetSyncSpeed(kbytes)
1312
    return self._SetMinorSyncSpeed(self.minor, kbytes) and children_result
1313

    
1314
  def GetProcStatus(self):
1315
    """Return device data from /proc.
1316

1317
    """
1318
    if self.minor is None:
1319
      _ThrowError("drbd%d: GetStats() called while not attached", self._aminor)
1320
    proc_info = self._MassageProcData(self._GetProcData())
1321
    if self.minor not in proc_info:
1322
      _ThrowError("drbd%d: can't find myself in /proc", self.minor)
1323
    return DRBD8Status(proc_info[self.minor])
1324

    
1325
  def GetSyncStatus(self):
1326
    """Returns the sync status of the device.
1327

1328

1329
    If sync_percent is None, it means all is ok
1330
    If estimated_time is None, it means we can't estimate
1331
    the time needed, otherwise it's the time left in seconds.
1332

1333

1334
    We set the is_degraded parameter to True on two conditions:
1335
    network not connected or local disk missing.
1336

1337
    We compute the ldisk parameter based on whether we have a local
1338
    disk or not.
1339

1340
    @rtype: objects.BlockDevStatus
1341

1342
    """
1343
    if self.minor is None and not self.Attach():
1344
      _ThrowError("drbd%d: can't Attach() in GetSyncStatus", self._aminor)
1345

    
1346
    stats = self.GetProcStatus()
1347
    is_degraded = not stats.is_connected or not stats.is_disk_uptodate
1348

    
1349
    if stats.is_disk_uptodate:
1350
      ldisk_status = constants.LDS_OKAY
1351
    elif stats.is_diskless:
1352
      ldisk_status = constants.LDS_FAULTY
1353
    else:
1354
      ldisk_status = constants.LDS_UNKNOWN
1355

    
1356
    return objects.BlockDevStatus(dev_path=self.dev_path,
1357
                                  major=self.major,
1358
                                  minor=self.minor,
1359
                                  sync_percent=stats.sync_percent,
1360
                                  estimated_time=stats.est_time,
1361
                                  is_degraded=is_degraded,
1362
                                  ldisk_status=ldisk_status)
1363

    
1364
  def Open(self, force=False):
1365
    """Make the local state primary.
1366

1367
    If the 'force' parameter is given, the '-o' option is passed to
1368
    drbdsetup. Since this is a potentially dangerous operation, the
1369
    force flag should be only given after creation, when it actually
1370
    is mandatory.
1371

1372
    """
1373
    if self.minor is None and not self.Attach():
1374
      logging.error("DRBD cannot attach to a device during open")
1375
      return False
1376
    cmd = ["drbdsetup", self.dev_path, "primary"]
1377
    if force:
1378
      cmd.append("-o")
1379
    result = utils.RunCmd(cmd)
1380
    if result.failed:
1381
      _ThrowError("drbd%d: can't make drbd device primary: %s", self.minor,
1382
                  result.output)
1383

    
1384
  def Close(self):
1385
    """Make the local state secondary.
1386

1387
    This will, of course, fail if the device is in use.
1388

1389
    """
1390
    if self.minor is None and not self.Attach():
1391
      _ThrowError("drbd%d: can't Attach() in Close()", self._aminor)
1392
    result = utils.RunCmd(["drbdsetup", self.dev_path, "secondary"])
1393
    if result.failed:
1394
      _ThrowError("drbd%d: can't switch drbd device to secondary: %s",
1395
                  self.minor, result.output)
1396

    
1397
  def DisconnectNet(self):
1398
    """Removes network configuration.
1399

1400
    This method shutdowns the network side of the device.
1401

1402
    The method will wait up to a hardcoded timeout for the device to
1403
    go into standalone after the 'disconnect' command before
1404
    re-configuring it, as sometimes it takes a while for the
1405
    disconnect to actually propagate and thus we might issue a 'net'
1406
    command while the device is still connected. If the device will
1407
    still be attached to the network and we time out, we raise an
1408
    exception.
1409

1410
    """
1411
    if self.minor is None:
1412
      _ThrowError("drbd%d: disk not attached in re-attach net", self._aminor)
1413

    
1414
    if None in (self._lhost, self._lport, self._rhost, self._rport):
1415
      _ThrowError("drbd%d: DRBD disk missing network info in"
1416
                  " DisconnectNet()", self.minor)
1417

    
1418
    ever_disconnected = _IgnoreError(self._ShutdownNet, self.minor)
1419
    timeout_limit = time.time() + self._NET_RECONFIG_TIMEOUT
1420
    sleep_time = 0.100 # we start the retry time at 100 milliseconds
1421
    while time.time() < timeout_limit:
1422
      status = self.GetProcStatus()
1423
      if status.is_standalone:
1424
        break
1425
      # retry the disconnect, it seems possible that due to a
1426
      # well-time disconnect on the peer, my disconnect command might
1427
      # be ignored and forgotten
1428
      ever_disconnected = _IgnoreError(self._ShutdownNet, self.minor) or \
1429
                          ever_disconnected
1430
      time.sleep(sleep_time)
1431
      sleep_time = min(2, sleep_time * 1.5)
1432

    
1433
    if not status.is_standalone:
1434
      if ever_disconnected:
1435
        msg = ("drbd%d: device did not react to the"
1436
               " 'disconnect' command in a timely manner")
1437
      else:
1438
        msg = "drbd%d: can't shutdown network, even after multiple retries"
1439
      _ThrowError(msg, self.minor)
1440

    
1441
    reconfig_time = time.time() - timeout_limit + self._NET_RECONFIG_TIMEOUT
1442
    if reconfig_time > 15: # hardcoded alert limit
1443
      logging.info("drbd%d: DisconnectNet: detach took %.3f seconds",
1444
                   self.minor, reconfig_time)
1445

    
1446
  def AttachNet(self, multimaster):
1447
    """Reconnects the network.
1448

1449
    This method connects the network side of the device with a
1450
    specified multi-master flag. The device needs to be 'Standalone'
1451
    but have valid network configuration data.
1452

1453
    Args:
1454
      - multimaster: init the network in dual-primary mode
1455

1456
    """
1457
    if self.minor is None:
1458
      _ThrowError("drbd%d: device not attached in AttachNet", self._aminor)
1459

    
1460
    if None in (self._lhost, self._lport, self._rhost, self._rport):
1461
      _ThrowError("drbd%d: missing network info in AttachNet()", self.minor)
1462

    
1463
    status = self.GetProcStatus()
1464

    
1465
    if not status.is_standalone:
1466
      _ThrowError("drbd%d: device is not standalone in AttachNet", self.minor)
1467

    
1468
    self._AssembleNet(self.minor,
1469
                      (self._lhost, self._lport, self._rhost, self._rport),
1470
                      constants.DRBD_NET_PROTOCOL, dual_pri=multimaster,
1471
                      hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1472

    
1473
  def Attach(self):
1474
    """Check if our minor is configured.
1475

1476
    This doesn't do any device configurations - it only checks if the
1477
    minor is in a state different from Unconfigured.
1478

1479
    Note that this function will not change the state of the system in
1480
    any way (except in case of side-effects caused by reading from
1481
    /proc).
1482

1483
    """
1484
    used_devs = self.GetUsedDevs()
1485
    if self._aminor in used_devs:
1486
      minor = self._aminor
1487
    else:
1488
      minor = None
1489

    
1490
    self._SetFromMinor(minor)
1491
    return minor is not None
1492

    
1493
  def Assemble(self):
1494
    """Assemble the drbd.
1495

1496
    Method:
1497
      - if we have a configured device, we try to ensure that it matches
1498
        our config
1499
      - if not, we create it from zero
1500

1501
    """
1502
    super(DRBD8, self).Assemble()
1503

    
1504
    self.Attach()
1505
    if self.minor is None:
1506
      # local device completely unconfigured
1507
      self._FastAssemble()
1508
    else:
1509
      # we have to recheck the local and network status and try to fix
1510
      # the device
1511
      self._SlowAssemble()
1512

    
1513
  def _SlowAssemble(self):
1514
    """Assembles the DRBD device from a (partially) configured device.
1515

1516
    In case of partially attached (local device matches but no network
1517
    setup), we perform the network attach. If successful, we re-test
1518
    the attach if can return success.
1519

1520
    """
1521
    net_data = (self._lhost, self._lport, self._rhost, self._rport)
1522
    for minor in (self._aminor,):
1523
      info = self._GetDevInfo(self._GetShowData(minor))
1524
      match_l = self._MatchesLocal(info)
1525
      match_r = self._MatchesNet(info)
1526

    
1527
      if match_l and match_r:
1528
        # everything matches
1529
        break
1530

    
1531
      if match_l and not match_r and "local_addr" not in info:
1532
        # disk matches, but not attached to network, attach and recheck
1533
        self._AssembleNet(minor, net_data, constants.DRBD_NET_PROTOCOL,
1534
                          hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1535
        if self._MatchesNet(self._GetDevInfo(self._GetShowData(minor))):
1536
          break
1537
        else:
1538
          _ThrowError("drbd%d: network attach successful, but 'drbdsetup"
1539
                      " show' disagrees", minor)
1540

    
1541
      if match_r and "local_dev" not in info:
1542
        # no local disk, but network attached and it matches
1543
        self._AssembleLocal(minor, self._children[0].dev_path,
1544
                            self._children[1].dev_path, self.size)
1545
        if self._MatchesNet(self._GetDevInfo(self._GetShowData(minor))):
1546
          break
1547
        else:
1548
          _ThrowError("drbd%d: disk attach successful, but 'drbdsetup"
1549
                      " show' disagrees", minor)
1550

    
1551
      # this case must be considered only if we actually have local
1552
      # storage, i.e. not in diskless mode, because all diskless
1553
      # devices are equal from the point of view of local
1554
      # configuration
1555
      if (match_l and "local_dev" in info and
1556
          not match_r and "local_addr" in info):
1557
        # strange case - the device network part points to somewhere
1558
        # else, even though its local storage is ours; as we own the
1559
        # drbd space, we try to disconnect from the remote peer and
1560
        # reconnect to our correct one
1561
        try:
1562
          self._ShutdownNet(minor)
1563
        except errors.BlockDeviceError, err:
1564
          _ThrowError("drbd%d: device has correct local storage, wrong"
1565
                      " remote peer and is unable to disconnect in order"
1566
                      " to attach to the correct peer: %s", minor, str(err))
1567
        # note: _AssembleNet also handles the case when we don't want
1568
        # local storage (i.e. one or more of the _[lr](host|port) is
1569
        # None)
1570
        self._AssembleNet(minor, net_data, constants.DRBD_NET_PROTOCOL,
1571
                          hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1572
        if self._MatchesNet(self._GetDevInfo(self._GetShowData(minor))):
1573
          break
1574
        else:
1575
          _ThrowError("drbd%d: network attach successful, but 'drbdsetup"
1576
                      " show' disagrees", minor)
1577

    
1578
    else:
1579
      minor = None
1580

    
1581
    self._SetFromMinor(minor)
1582
    if minor is None:
1583
      _ThrowError("drbd%d: cannot activate, unknown or unhandled reason",
1584
                  self._aminor)
1585

    
1586
  def _FastAssemble(self):
1587
    """Assemble the drbd device from zero.
1588

1589
    This is run when in Assemble we detect our minor is unused.
1590

1591
    """
1592
    minor = self._aminor
1593
    if self._children and self._children[0] and self._children[1]:
1594
      self._AssembleLocal(minor, self._children[0].dev_path,
1595
                          self._children[1].dev_path, self.size)
1596
    if self._lhost and self._lport and self._rhost and self._rport:
1597
      self._AssembleNet(minor,
1598
                        (self._lhost, self._lport, self._rhost, self._rport),
1599
                        constants.DRBD_NET_PROTOCOL,
1600
                        hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1601
    self._SetFromMinor(minor)
1602

    
1603
  @classmethod
1604
  def _ShutdownLocal(cls, minor):
1605
    """Detach from the local device.
1606

1607
    I/Os will continue to be served from the remote device. If we
1608
    don't have a remote device, this operation will fail.
1609

1610
    """
1611
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "detach"])
1612
    if result.failed:
1613
      _ThrowError("drbd%d: can't detach local disk: %s", minor, result.output)
1614

    
1615
  @classmethod
1616
  def _ShutdownNet(cls, minor):
1617
    """Disconnect from the remote peer.
1618

1619
    This fails if we don't have a local device.
1620

1621
    """
1622
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "disconnect"])
1623
    if result.failed:
1624
      _ThrowError("drbd%d: can't shutdown network: %s", minor, result.output)
1625

    
1626
  @classmethod
1627
  def _ShutdownAll(cls, minor):
1628
    """Deactivate the device.
1629

1630
    This will, of course, fail if the device is in use.
1631

1632
    """
1633
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "down"])
1634
    if result.failed:
1635
      _ThrowError("drbd%d: can't shutdown drbd device: %s",
1636
                  minor, result.output)
1637

    
1638
  def Shutdown(self):
1639
    """Shutdown the DRBD device.
1640

1641
    """
1642
    if self.minor is None and not self.Attach():
1643
      logging.info("drbd%d: not attached during Shutdown()", self._aminor)
1644
      return
1645
    minor = self.minor
1646
    self.minor = None
1647
    self.dev_path = None
1648
    self._ShutdownAll(minor)
1649

    
1650
  def Remove(self):
1651
    """Stub remove for DRBD devices.
1652

1653
    """
1654
    self.Shutdown()
1655

    
1656
  @classmethod
1657
  def Create(cls, unique_id, children, size):
1658
    """Create a new DRBD8 device.
1659

1660
    Since DRBD devices are not created per se, just assembled, this
1661
    function only initializes the metadata.
1662

1663
    """
1664
    if len(children) != 2:
1665
      raise errors.ProgrammerError("Invalid setup for the drbd device")
1666
    # check that the minor is unused
1667
    aminor = unique_id[4]
1668
    proc_info = cls._MassageProcData(cls._GetProcData())
1669
    if aminor in proc_info:
1670
      status = DRBD8Status(proc_info[aminor])
1671
      in_use = status.is_in_use
1672
    else:
1673
      in_use = False
1674
    if in_use:
1675
      _ThrowError("drbd%d: minor is already in use at Create() time", aminor)
1676
    meta = children[1]
1677
    meta.Assemble()
1678
    if not meta.Attach():
1679
      _ThrowError("drbd%d: can't attach to meta device '%s'",
1680
                  aminor, meta)
1681
    cls._CheckMetaSize(meta.dev_path)
1682
    cls._InitMeta(aminor, meta.dev_path)
1683
    return cls(unique_id, children, size)
1684

    
1685
  def Grow(self, amount):
1686
    """Resize the DRBD device and its backing storage.
1687

1688
    """
1689
    if self.minor is None:
1690
      _ThrowError("drbd%d: Grow called while not attached", self._aminor)
1691
    if len(self._children) != 2 or None in self._children:
1692
      _ThrowError("drbd%d: cannot grow diskless device", self.minor)
1693
    self._children[0].Grow(amount)
1694
    result = utils.RunCmd(["drbdsetup", self.dev_path, "resize", "-s",
1695
                           "%dm" % (self.size + amount)])
1696
    if result.failed:
1697
      _ThrowError("drbd%d: resize failed: %s", self.minor, result.output)
1698

    
1699

    
1700
class FileStorage(BlockDev):
1701
  """File device.
1702

1703
  This class represents the a file storage backend device.
1704

1705
  The unique_id for the file device is a (file_driver, file_path) tuple.
1706

1707
  """
1708
  def __init__(self, unique_id, children, size):
1709
    """Initalizes a file device backend.
1710

1711
    """
1712
    if children:
1713
      raise errors.BlockDeviceError("Invalid setup for file device")
1714
    super(FileStorage, self).__init__(unique_id, children, size)
1715
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
1716
      raise ValueError("Invalid configuration data %s" % str(unique_id))
1717
    self.driver = unique_id[0]
1718
    self.dev_path = unique_id[1]
1719
    self.Attach()
1720

    
1721
  def Assemble(self):
1722
    """Assemble the device.
1723

1724
    Checks whether the file device exists, raises BlockDeviceError otherwise.
1725

1726
    """
1727
    if not os.path.exists(self.dev_path):
1728
      _ThrowError("File device '%s' does not exist" % self.dev_path)
1729

    
1730
  def Shutdown(self):
1731
    """Shutdown the device.
1732

1733
    This is a no-op for the file type, as we don't deactivate
1734
    the file on shutdown.
1735

1736
    """
1737
    pass
1738

    
1739
  def Open(self, force=False):
1740
    """Make the device ready for I/O.
1741

1742
    This is a no-op for the file type.
1743

1744
    """
1745
    pass
1746

    
1747
  def Close(self):
1748
    """Notifies that the device will no longer be used for I/O.
1749

1750
    This is a no-op for the file type.
1751

1752
    """
1753
    pass
1754

    
1755
  def Remove(self):
1756
    """Remove the file backing the block device.
1757

1758
    @rtype: boolean
1759
    @return: True if the removal was successful
1760

1761
    """
1762
    try:
1763
      os.remove(self.dev_path)
1764
    except OSError, err:
1765
      if err.errno != errno.ENOENT:
1766
        _ThrowError("Can't remove file '%s': %s", self.dev_path, err)
1767

    
1768
  def Attach(self):
1769
    """Attach to an existing file.
1770

1771
    Check if this file already exists.
1772

1773
    @rtype: boolean
1774
    @return: True if file exists
1775

1776
    """
1777
    self.attached = os.path.exists(self.dev_path)
1778
    return self.attached
1779

    
1780
  @classmethod
1781
  def Create(cls, unique_id, children, size):
1782
    """Create a new file.
1783

1784
    @param size: the size of file in MiB
1785

1786
    @rtype: L{bdev.FileStorage}
1787
    @return: an instance of FileStorage
1788

1789
    """
1790
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
1791
      raise ValueError("Invalid configuration data %s" % str(unique_id))
1792
    dev_path = unique_id[1]
1793
    if os.path.exists(dev_path):
1794
      _ThrowError("File already existing: %s", dev_path)
1795
    try:
1796
      f = open(dev_path, 'w')
1797
      f.truncate(size * 1024 * 1024)
1798
      f.close()
1799
    except IOError, err:
1800
      _ThrowError("Error in file creation: %", str(err))
1801

    
1802
    return FileStorage(unique_id, children, size)
1803

    
1804

    
1805
DEV_MAP = {
1806
  constants.LD_LV: LogicalVolume,
1807
  constants.LD_DRBD8: DRBD8,
1808
  constants.LD_FILE: FileStorage,
1809
  }
1810

    
1811

    
1812
def FindDevice(dev_type, unique_id, children, size):
1813
  """Search for an existing, assembled device.
1814

1815
  This will succeed only if the device exists and is assembled, but it
1816
  does not do any actions in order to activate the device.
1817

1818
  """
1819
  if dev_type not in DEV_MAP:
1820
    raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
1821
  device = DEV_MAP[dev_type](unique_id, children, size)
1822
  if not device.attached:
1823
    return None
1824
  return device
1825

    
1826

    
1827
def Assemble(dev_type, unique_id, children, size):
1828
  """Try to attach or assemble an existing device.
1829

1830
  This will attach to assemble the device, as needed, to bring it
1831
  fully up. It must be safe to run on already-assembled devices.
1832

1833
  """
1834
  if dev_type not in DEV_MAP:
1835
    raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
1836
  device = DEV_MAP[dev_type](unique_id, children, size)
1837
  device.Assemble()
1838
  return device
1839

    
1840

    
1841
def Create(dev_type, unique_id, children, size):
1842
  """Create a device.
1843

1844
  """
1845
  if dev_type not in DEV_MAP:
1846
    raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
1847
  device = DEV_MAP[dev_type].Create(unique_id, children, size)
1848
  return device