Statistics
| Branch: | Tag: | Revision:

root / lib / bdev.py @ 714ea7ca

History | View | Annotate | Download (62.2 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
# Size of reads in _CanReadDevice
38
_DEVICE_READ_SIZE = 128 * 1024
39

    
40

    
41
def _IgnoreError(fn, *args, **kwargs):
42
  """Executes the given function, ignoring BlockDeviceErrors.
43

44
  This is used in order to simplify the execution of cleanup or
45
  rollback functions.
46

47
  @rtype: boolean
48
  @return: True when fn didn't raise an exception, False otherwise
49

50
  """
51
  try:
52
    fn(*args, **kwargs)
53
    return True
54
  except errors.BlockDeviceError, err:
55
    logging.warning("Caught BlockDeviceError but ignoring: %s", str(err))
56
    return False
57

    
58

    
59
def _ThrowError(msg, *args):
60
  """Log an error to the node daemon and the raise an exception.
61

62
  @type msg: string
63
  @param msg: the text of the exception
64
  @raise errors.BlockDeviceError
65

66
  """
67
  if args:
68
    msg = msg % args
69
  logging.error(msg)
70
  raise errors.BlockDeviceError(msg)
71

    
72

    
73
def _CanReadDevice(path):
74
  """Check if we can read from the given device.
75

76
  This tries to read the first 128k of the device.
77

78
  """
79
  try:
80
    utils.ReadFile(path, size=_DEVICE_READ_SIZE)
81
    return True
82
  except EnvironmentError:
83
    logging.warning("Can't read from device %s", path, exc_info=True)
84
    return False
85

    
86

    
87
class BlockDev(object):
88
  """Block device abstract class.
89

90
  A block device can be in the following states:
91
    - not existing on the system, and by `Create()` it goes into:
92
    - existing but not setup/not active, and by `Assemble()` goes into:
93
    - active read-write and by `Open()` it goes into
94
    - online (=used, or ready for use)
95

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

102
  The many different states of the device are due to the fact that we
103
  need to cover many device types:
104
    - logical volumes are created, lvchange -a y $lv, and used
105
    - drbd devices are attached to a local disk/remote peer and made primary
106

107
  A block device is identified by three items:
108
    - the /dev path of the device (dynamic)
109
    - a unique ID of the device (static)
110
    - it's major/minor pair (dynamic)
111

112
  Not all devices implement both the first two as distinct items. LVM
113
  logical volumes have their unique ID (the pair volume group, logical
114
  volume name) in a 1-to-1 relation to the dev path. For DRBD devices,
115
  the /dev path is again dynamic and the unique id is the pair (host1,
116
  dev1), (host2, dev2).
117

118
  You can get to a device in two ways:
119
    - creating the (real) device, which returns you
120
      an attached instance (lvcreate)
121
    - attaching of a python instance to an existing (real) device
122

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

129
  """
130
  def __init__(self, unique_id, children, size):
131
    self._children = children
132
    self.dev_path = None
133
    self.unique_id = unique_id
134
    self.major = None
135
    self.minor = None
136
    self.attached = False
137
    self.size = size
138

    
139
  def Assemble(self):
140
    """Assemble the device from its components.
141

142
    Implementations of this method by child classes must ensure that:
143
      - after the device has been assembled, it knows its major/minor
144
        numbers; this allows other devices (usually parents) to probe
145
        correctly for their children
146
      - calling this method on an existing, in-use device is safe
147
      - if the device is already configured (and in an OK state),
148
        this method is idempotent
149

150
    """
151
    pass
152

    
153
  def Attach(self):
154
    """Find a device which matches our config and attach to it.
155

156
    """
157
    raise NotImplementedError
158

    
159
  def Close(self):
160
    """Notifies that the device will no longer be used for I/O.
161

162
    """
163
    raise NotImplementedError
164

    
165
  @classmethod
166
  def Create(cls, unique_id, children, size):
167
    """Create the device.
168

169
    If the device cannot be created, it will return None
170
    instead. Error messages go to the logging system.
171

172
    Note that for some devices, the unique_id is used, and for other,
173
    the children. The idea is that these two, taken together, are
174
    enough for both creation and assembly (later).
175

176
    """
177
    raise NotImplementedError
178

    
179
  def Remove(self):
180
    """Remove this device.
181

182
    This makes sense only for some of the device types: LV and file
183
    storage. Also note that if the device can't attach, the removal
184
    can't be completed.
185

186
    """
187
    raise NotImplementedError
188

    
189
  def Rename(self, new_id):
190
    """Rename this device.
191

192
    This may or may not make sense for a given device type.
193

194
    """
195
    raise NotImplementedError
196

    
197
  def Open(self, force=False):
198
    """Make the device ready for use.
199

200
    This makes the device ready for I/O. For now, just the DRBD
201
    devices need this.
202

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

206
    """
207
    raise NotImplementedError
208

    
209
  def Shutdown(self):
210
    """Shut down the device, freeing its children.
211

212
    This undoes the `Assemble()` work, except for the child
213
    assembling; as such, the children on the device are still
214
    assembled after this call.
215

216
    """
217
    raise NotImplementedError
218

    
219
  def SetSyncSpeed(self, speed):
220
    """Adjust the sync speed of the mirror.
221

222
    In case this is not a mirroring device, this is no-op.
223

224
    """
225
    result = True
226
    if self._children:
227
      for child in self._children:
228
        result = result and child.SetSyncSpeed(speed)
229
    return result
230

    
231
  def GetSyncStatus(self):
232
    """Returns the sync status of the device.
233

234
    If this device is a mirroring device, this function returns the
235
    status of the mirror.
236

237
    If sync_percent is None, it means the device is not syncing.
238

239
    If estimated_time is None, it means we can't estimate
240
    the time needed, otherwise it's the time left in seconds.
241

242
    If is_degraded is True, it means the device is missing
243
    redundancy. This is usually a sign that something went wrong in
244
    the device setup, if sync_percent is None.
245

246
    The ldisk parameter represents the degradation of the local
247
    data. This is only valid for some devices, the rest will always
248
    return False (not degraded).
249

250
    @rtype: objects.BlockDevStatus
251

252
    """
253
    return objects.BlockDevStatus(dev_path=self.dev_path,
254
                                  major=self.major,
255
                                  minor=self.minor,
256
                                  sync_percent=None,
257
                                  estimated_time=None,
258
                                  is_degraded=False,
259
                                  ldisk_status=constants.LDS_OKAY)
260

    
261
  def CombinedSyncStatus(self):
262
    """Calculate the mirror status recursively for our children.
263

264
    The return value is the same as for `GetSyncStatus()` except the
265
    minimum percent and maximum time are calculated across our
266
    children.
267

268
    @rtype: objects.BlockDevStatus
269

270
    """
271
    status = self.GetSyncStatus()
272

    
273
    min_percent = status.sync_percent
274
    max_time = status.estimated_time
275
    is_degraded = status.is_degraded
276
    ldisk_status = status.ldisk_status
277

    
278
    if self._children:
279
      for child in self._children:
280
        child_status = child.GetSyncStatus()
281

    
282
        if min_percent is None:
283
          min_percent = child_status.sync_percent
284
        elif child_status.sync_percent is not None:
285
          min_percent = min(min_percent, child_status.sync_percent)
286

    
287
        if max_time is None:
288
          max_time = child_status.estimated_time
289
        elif child_status.estimated_time is not None:
290
          max_time = max(max_time, child_status.estimated_time)
291

    
292
        is_degraded = is_degraded or child_status.is_degraded
293

    
294
        if ldisk_status is None:
295
          ldisk_status = child_status.ldisk_status
296
        elif child_status.ldisk_status is not None:
297
          ldisk_status = max(ldisk_status, child_status.ldisk_status)
298

    
299
    return objects.BlockDevStatus(dev_path=self.dev_path,
300
                                  major=self.major,
301
                                  minor=self.minor,
302
                                  sync_percent=min_percent,
303
                                  estimated_time=max_time,
304
                                  is_degraded=is_degraded,
305
                                  ldisk_status=ldisk_status)
306

    
307

    
308
  def SetInfo(self, text):
309
    """Update metadata with info text.
310

311
    Only supported for some device types.
312

313
    """
314
    for child in self._children:
315
      child.SetInfo(text)
316

    
317
  def Grow(self, amount):
318
    """Grow the block device.
319

320
    @param amount: the amount (in mebibytes) to grow with
321

322
    """
323
    raise NotImplementedError
324

    
325
  def GetActualSize(self):
326
    """Return the actual disk size.
327

328
    @note: the device needs to be active when this is called
329

330
    """
331
    assert self.attached, "BlockDevice not attached in GetActualSize()"
332
    result = utils.RunCmd(["blockdev", "--getsize64", self.dev_path])
333
    if result.failed:
334
      _ThrowError("blockdev failed (%s): %s",
335
                  result.fail_reason, result.output)
336
    try:
337
      sz = int(result.output.strip())
338
    except (ValueError, TypeError), err:
339
      _ThrowError("Failed to parse blockdev output: %s", str(err))
340
    return sz
341

    
342
  def __repr__(self):
343
    return ("<%s: unique_id: %s, children: %s, %s:%s, %s>" %
344
            (self.__class__, self.unique_id, self._children,
345
             self.major, self.minor, self.dev_path))
346

    
347

    
348
class LogicalVolume(BlockDev):
349
  """Logical Volume block device.
350

351
  """
352
  def __init__(self, unique_id, children, size):
353
    """Attaches to a LV device.
354

355
    The unique_id is a tuple (vg_name, lv_name)
356

357
    """
358
    super(LogicalVolume, self).__init__(unique_id, children, size)
359
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
360
      raise ValueError("Invalid configuration data %s" % str(unique_id))
361
    self._vg_name, self._lv_name = unique_id
362
    self.dev_path = "/dev/%s/%s" % (self._vg_name, self._lv_name)
363
    self._degraded = True
364
    self.major = self.minor = self.pe_size = self.stripe_count = None
365
    self.Attach()
366

    
367
  @classmethod
368
  def Create(cls, unique_id, children, size):
369
    """Create a new logical volume.
370

371
    """
372
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
373
      raise errors.ProgrammerError("Invalid configuration data %s" %
374
                                   str(unique_id))
375
    vg_name, lv_name = unique_id
376
    pvs_info = cls.GetPVInfo([vg_name])
377
    if not pvs_info:
378
      _ThrowError("Can't compute PV info for vg %s", vg_name)
379
    pvs_info.sort()
380
    pvs_info.reverse()
381

    
382
    pvlist = [ pv[1] for pv in pvs_info ]
383
    if utils.any(pvlist, lambda v: ":" in v):
384
      _ThrowError("Some of your PVs have invalid character ':'"
385
                  " in their name")
386
    free_size = sum([ pv[0] for pv in pvs_info ])
387
    current_pvs = len(pvlist)
388
    stripes = min(current_pvs, constants.LVM_STRIPECOUNT)
389

    
390
    # The size constraint should have been checked from the master before
391
    # calling the create function.
392
    if free_size < size:
393
      _ThrowError("Not enough free space: required %s,"
394
                  " available %s", size, free_size)
395
    cmd = ["lvcreate", "-L%dm" % size, "-n%s" % lv_name]
396
    # If the free space is not well distributed, we won't be able to
397
    # create an optimally-striped volume; in that case, we want to try
398
    # with N, N-1, ..., 2, and finally 1 (non-stripped) number of
399
    # stripes
400
    for stripes_arg in range(stripes, 0, -1):
401
      result = utils.RunCmd(cmd + ["-i%d" % stripes_arg] + [vg_name] + pvlist)
402
      if not result.failed:
403
        break
404
    if result.failed:
405
      _ThrowError("LV create failed (%s): %s",
406
                  result.fail_reason, result.output)
407
    return LogicalVolume(unique_id, children, size)
408

    
409
  @staticmethod
410
  def GetPVInfo(vg_names, filter_allocatable=True):
411
    """Get the free space info for PVs in a volume group.
412

413
    @param vg_names: list of volume group names, if empty all will be returned
414
    @param filter_allocatable: whether to skip over unallocatable PVs
415

416
    @rtype: list
417
    @return: list of tuples (free_space, name) with free_space in mebibytes
418

419
    """
420
    sep = "|"
421
    command = ["pvs", "--noheadings", "--nosuffix", "--units=m",
422
               "-opv_name,vg_name,pv_free,pv_attr", "--unbuffered",
423
               "--separator=%s" % sep ]
424
    result = utils.RunCmd(command)
425
    if result.failed:
426
      logging.error("Can't get the PV information: %s - %s",
427
                    result.fail_reason, result.output)
428
      return None
429
    data = []
430
    for line in result.stdout.splitlines():
431
      fields = line.strip().split(sep)
432
      if len(fields) != 4:
433
        logging.error("Can't parse pvs output: line '%s'", line)
434
        return None
435
      # (possibly) skip over pvs which are not allocatable
436
      if filter_allocatable and fields[3][0] != 'a':
437
        continue
438
      # (possibly) skip over pvs which are not in the right volume group(s)
439
      if vg_names and fields[1] not in vg_names:
440
        continue
441
      data.append((float(fields[2]), fields[0], fields[1]))
442

    
443
    return data
444

    
445
  def Remove(self):
446
    """Remove this logical volume.
447

448
    """
449
    if not self.minor and not self.Attach():
450
      # the LV does not exist
451
      return
452
    result = utils.RunCmd(["lvremove", "-f", "%s/%s" %
453
                           (self._vg_name, self._lv_name)])
454
    if result.failed:
455
      _ThrowError("Can't lvremove: %s - %s", result.fail_reason, result.output)
456

    
457
  def Rename(self, new_id):
458
    """Rename this logical volume.
459

460
    """
461
    if not isinstance(new_id, (tuple, list)) or len(new_id) != 2:
462
      raise errors.ProgrammerError("Invalid new logical id '%s'" % new_id)
463
    new_vg, new_name = new_id
464
    if new_vg != self._vg_name:
465
      raise errors.ProgrammerError("Can't move a logical volume across"
466
                                   " volume groups (from %s to to %s)" %
467
                                   (self._vg_name, new_vg))
468
    result = utils.RunCmd(["lvrename", new_vg, self._lv_name, new_name])
469
    if result.failed:
470
      _ThrowError("Failed to rename the logical volume: %s", result.output)
471
    self._lv_name = new_name
472
    self.dev_path = "/dev/%s/%s" % (self._vg_name, self._lv_name)
473

    
474
  def Attach(self):
475
    """Attach to an existing LV.
476

477
    This method will try to see if an existing and active LV exists
478
    which matches our name. If so, its major/minor will be
479
    recorded.
480

481
    """
482
    self.attached = False
483
    result = utils.RunCmd(["lvs", "--noheadings", "--separator=,",
484
                           "--units=m", "--nosuffix",
485
                           "-olv_attr,lv_kernel_major,lv_kernel_minor,"
486
                           "vg_extent_size,stripes", self.dev_path])
487
    if result.failed:
488
      logging.error("Can't find LV %s: %s, %s",
489
                    self.dev_path, result.fail_reason, result.output)
490
      return False
491
    # the output can (and will) have multiple lines for multi-segment
492
    # LVs, as the 'stripes' parameter is a segment one, so we take
493
    # only the last entry, which is the one we're interested in; note
494
    # that with LVM2 anyway the 'stripes' value must be constant
495
    # across segments, so this is a no-op actually
496
    out = result.stdout.splitlines()
497
    if not out: # totally empty result? splitlines() returns at least
498
                # one line for any non-empty string
499
      logging.error("Can't parse LVS output, no lines? Got '%s'", str(out))
500
      return False
501
    out = out[-1].strip().rstrip(',')
502
    out = out.split(",")
503
    if len(out) != 5:
504
      logging.error("Can't parse LVS output, len(%s) != 5", str(out))
505
      return False
506

    
507
    status, major, minor, pe_size, stripes = out
508
    if len(status) != 6:
509
      logging.error("lvs lv_attr is not 6 characters (%s)", status)
510
      return False
511

    
512
    try:
513
      major = int(major)
514
      minor = int(minor)
515
    except (TypeError, ValueError), err:
516
      logging.error("lvs major/minor cannot be parsed: %s", str(err))
517

    
518
    try:
519
      pe_size = int(float(pe_size))
520
    except (TypeError, ValueError), err:
521
      logging.error("Can't parse vg extent size: %s", err)
522
      return False
523

    
524
    try:
525
      stripes = int(stripes)
526
    except (TypeError, ValueError), err:
527
      logging.error("Can't parse the number of stripes: %s", err)
528
      return False
529

    
530
    self.major = major
531
    self.minor = minor
532
    self.pe_size = pe_size
533
    self.stripe_count = stripes
534
    self._degraded = status[0] == 'v' # virtual volume, i.e. doesn't backing
535
                                      # storage
536
    self.attached = True
537
    return True
538

    
539
  def Assemble(self):
540
    """Assemble the device.
541

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

546
    """
547
    result = utils.RunCmd(["lvchange", "-ay", self.dev_path])
548
    if result.failed:
549
      _ThrowError("Can't activate lv %s: %s", self.dev_path, result.output)
550

    
551
  def Shutdown(self):
552
    """Shutdown the device.
553

554
    This is a no-op for the LV device type, as we don't deactivate the
555
    volumes on shutdown.
556

557
    """
558
    pass
559

    
560
  def GetSyncStatus(self):
561
    """Returns the sync status of the device.
562

563
    If this device is a mirroring device, this function returns the
564
    status of the mirror.
565

566
    For logical volumes, sync_percent and estimated_time are always
567
    None (no recovery in progress, as we don't handle the mirrored LV
568
    case). The is_degraded parameter is the inverse of the ldisk
569
    parameter.
570

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

577
    The status was already read in Attach, so we just return it.
578

579
    @rtype: objects.BlockDevStatus
580

581
    """
582
    if self._degraded:
583
      ldisk_status = constants.LDS_FAULTY
584
    else:
585
      ldisk_status = constants.LDS_OKAY
586

    
587
    return objects.BlockDevStatus(dev_path=self.dev_path,
588
                                  major=self.major,
589
                                  minor=self.minor,
590
                                  sync_percent=None,
591
                                  estimated_time=None,
592
                                  is_degraded=self._degraded,
593
                                  ldisk_status=ldisk_status)
594

    
595
  def Open(self, force=False):
596
    """Make the device ready for I/O.
597

598
    This is a no-op for the LV device type.
599

600
    """
601
    pass
602

    
603
  def Close(self):
604
    """Notifies that the device will no longer be used for I/O.
605

606
    This is a no-op for the LV device type.
607

608
    """
609
    pass
610

    
611
  def Snapshot(self, size):
612
    """Create a snapshot copy of an lvm block device.
613

614
    """
615
    snap_name = self._lv_name + ".snap"
616

    
617
    # remove existing snapshot if found
618
    snap = LogicalVolume((self._vg_name, snap_name), None, size)
619
    _IgnoreError(snap.Remove)
620

    
621
    pvs_info = self.GetPVInfo([self._vg_name])
622
    if not pvs_info:
623
      _ThrowError("Can't compute PV info for vg %s", self._vg_name)
624
    pvs_info.sort()
625
    pvs_info.reverse()
626
    free_size, _, _ = pvs_info[0]
627
    if free_size < size:
628
      _ThrowError("Not enough free space: required %s,"
629
                  " available %s", size, free_size)
630

    
631
    result = utils.RunCmd(["lvcreate", "-L%dm" % size, "-s",
632
                           "-n%s" % snap_name, self.dev_path])
633
    if result.failed:
634
      _ThrowError("command: %s error: %s - %s",
635
                  result.cmd, result.fail_reason, result.output)
636

    
637
    return snap_name
638

    
639
  def SetInfo(self, text):
640
    """Update metadata with info text.
641

642
    """
643
    BlockDev.SetInfo(self, text)
644

    
645
    # Replace invalid characters
646
    text = re.sub('^[^A-Za-z0-9_+.]', '_', text)
647
    text = re.sub('[^-A-Za-z0-9_+.]', '_', text)
648

    
649
    # Only up to 128 characters are allowed
650
    text = text[:128]
651

    
652
    result = utils.RunCmd(["lvchange", "--addtag", text,
653
                           self.dev_path])
654
    if result.failed:
655
      _ThrowError("Command: %s error: %s - %s", result.cmd, result.fail_reason,
656
                  result.output)
657

    
658
  def Grow(self, amount):
659
    """Grow the logical volume.
660

661
    """
662
    if self.pe_size is None or self.stripe_count is None:
663
      if not self.Attach():
664
        _ThrowError("Can't attach to LV during Grow()")
665
    full_stripe_size = self.pe_size * self.stripe_count
666
    rest = amount % full_stripe_size
667
    if rest != 0:
668
      amount += full_stripe_size - rest
669
    # we try multiple algorithms since the 'best' ones might not have
670
    # space available in the right place, but later ones might (since
671
    # they have less constraints); also note that only recent LVM
672
    # supports 'cling'
673
    for alloc_policy in "contiguous", "cling", "normal":
674
      result = utils.RunCmd(["lvextend", "--alloc", alloc_policy,
675
                             "-L", "+%dm" % amount, self.dev_path])
676
      if not result.failed:
677
        return
678
    _ThrowError("Can't grow LV %s: %s", self.dev_path, result.output)
679

    
680

    
681
class DRBD8Status(object):
682
  """A DRBD status representation class.
683

684
  Note that this doesn't support unconfigured devices (cs:Unconfigured).
685

686
  """
687
  UNCONF_RE = re.compile(r"\s*[0-9]+:\s*cs:Unconfigured$")
688
  LINE_RE = re.compile(r"\s*[0-9]+:\s*cs:(\S+)\s+(?:st|ro):([^/]+)/(\S+)"
689
                       "\s+ds:([^/]+)/(\S+)\s+.*$")
690
  SYNC_RE = re.compile(r"^.*\ssync'ed:\s*([0-9.]+)%.*"
691
                       "\sfinish: ([0-9]+):([0-9]+):([0-9]+)\s.*$")
692

    
693
  CS_UNCONFIGURED = "Unconfigured"
694
  CS_STANDALONE = "StandAlone"
695
  CS_WFCONNECTION = "WFConnection"
696
  CS_WFREPORTPARAMS = "WFReportParams"
697
  CS_CONNECTED = "Connected"
698
  CS_STARTINGSYNCS = "StartingSyncS"
699
  CS_STARTINGSYNCT = "StartingSyncT"
700
  CS_WFBITMAPS = "WFBitMapS"
701
  CS_WFBITMAPT = "WFBitMapT"
702
  CS_WFSYNCUUID = "WFSyncUUID"
703
  CS_SYNCSOURCE = "SyncSource"
704
  CS_SYNCTARGET = "SyncTarget"
705
  CS_PAUSEDSYNCS = "PausedSyncS"
706
  CS_PAUSEDSYNCT = "PausedSyncT"
707
  CSET_SYNC = frozenset([
708
    CS_WFREPORTPARAMS,
709
    CS_STARTINGSYNCS,
710
    CS_STARTINGSYNCT,
711
    CS_WFBITMAPS,
712
    CS_WFBITMAPT,
713
    CS_WFSYNCUUID,
714
    CS_SYNCSOURCE,
715
    CS_SYNCTARGET,
716
    CS_PAUSEDSYNCS,
717
    CS_PAUSEDSYNCT,
718
    ])
719

    
720
  DS_DISKLESS = "Diskless"
721
  DS_ATTACHING = "Attaching" # transient state
722
  DS_FAILED = "Failed" # transient state, next: diskless
723
  DS_NEGOTIATING = "Negotiating" # transient state
724
  DS_INCONSISTENT = "Inconsistent" # while syncing or after creation
725
  DS_OUTDATED = "Outdated"
726
  DS_DUNKNOWN = "DUnknown" # shown for peer disk when not connected
727
  DS_CONSISTENT = "Consistent"
728
  DS_UPTODATE = "UpToDate" # normal state
729

    
730
  RO_PRIMARY = "Primary"
731
  RO_SECONDARY = "Secondary"
732
  RO_UNKNOWN = "Unknown"
733

    
734
  def __init__(self, procline):
735
    u = self.UNCONF_RE.match(procline)
736
    if u:
737
      self.cstatus = self.CS_UNCONFIGURED
738
      self.lrole = self.rrole = self.ldisk = self.rdisk = None
739
    else:
740
      m = self.LINE_RE.match(procline)
741
      if not m:
742
        raise errors.BlockDeviceError("Can't parse input data '%s'" % procline)
743
      self.cstatus = m.group(1)
744
      self.lrole = m.group(2)
745
      self.rrole = m.group(3)
746
      self.ldisk = m.group(4)
747
      self.rdisk = m.group(5)
748

    
749
    # end reading of data from the LINE_RE or UNCONF_RE
750

    
751
    self.is_standalone = self.cstatus == self.CS_STANDALONE
752
    self.is_wfconn = self.cstatus == self.CS_WFCONNECTION
753
    self.is_connected = self.cstatus == self.CS_CONNECTED
754
    self.is_primary = self.lrole == self.RO_PRIMARY
755
    self.is_secondary = self.lrole == self.RO_SECONDARY
756
    self.peer_primary = self.rrole == self.RO_PRIMARY
757
    self.peer_secondary = self.rrole == self.RO_SECONDARY
758
    self.both_primary = self.is_primary and self.peer_primary
759
    self.both_secondary = self.is_secondary and self.peer_secondary
760

    
761
    self.is_diskless = self.ldisk == self.DS_DISKLESS
762
    self.is_disk_uptodate = self.ldisk == self.DS_UPTODATE
763

    
764
    self.is_in_resync = self.cstatus in self.CSET_SYNC
765
    self.is_in_use = self.cstatus != self.CS_UNCONFIGURED
766

    
767
    m = self.SYNC_RE.match(procline)
768
    if m:
769
      self.sync_percent = float(m.group(1))
770
      hours = int(m.group(2))
771
      minutes = int(m.group(3))
772
      seconds = int(m.group(4))
773
      self.est_time = hours * 3600 + minutes * 60 + seconds
774
    else:
775
      # we have (in this if branch) no percent information, but if
776
      # we're resyncing we need to 'fake' a sync percent information,
777
      # as this is how cmdlib determines if it makes sense to wait for
778
      # resyncing or not
779
      if self.is_in_resync:
780
        self.sync_percent = 0
781
      else:
782
        self.sync_percent = None
783
      self.est_time = None
784

    
785

    
786
class BaseDRBD(BlockDev): # pylint: disable-msg=W0223
787
  """Base DRBD class.
788

789
  This class contains a few bits of common functionality between the
790
  0.7 and 8.x versions of DRBD.
791

792
  """
793
  _VERSION_RE = re.compile(r"^version: (\d+)\.(\d+)\.(\d+)"
794
                           r" \(api:(\d+)/proto:(\d+)(?:-(\d+))?\)")
795
  _VALID_LINE_RE = re.compile("^ *([0-9]+): cs:([^ ]+).*$")
796
  _UNUSED_LINE_RE = re.compile("^ *([0-9]+): cs:Unconfigured$")
797

    
798
  _DRBD_MAJOR = 147
799
  _ST_UNCONFIGURED = "Unconfigured"
800
  _ST_WFCONNECTION = "WFConnection"
801
  _ST_CONNECTED = "Connected"
802

    
803
  _STATUS_FILE = "/proc/drbd"
804

    
805
  @staticmethod
806
  def _GetProcData(filename=_STATUS_FILE):
807
    """Return data from /proc/drbd.
808

809
    """
810
    try:
811
      data = utils.ReadFile(filename).splitlines()
812
    except EnvironmentError, err:
813
      if err.errno == errno.ENOENT:
814
        _ThrowError("The file %s cannot be opened, check if the module"
815
                    " is loaded (%s)", filename, str(err))
816
      else:
817
        _ThrowError("Can't read the DRBD proc file %s: %s", filename, str(err))
818
    if not data:
819
      _ThrowError("Can't read any data from %s", filename)
820
    return data
821

    
822
  @classmethod
823
  def _MassageProcData(cls, data):
824
    """Transform the output of _GetProdData into a nicer form.
825

826
    @return: a dictionary of minor: joined lines from /proc/drbd
827
        for that minor
828

829
    """
830
    results = {}
831
    old_minor = old_line = None
832
    for line in data:
833
      if not line: # completely empty lines, as can be returned by drbd8.0+
834
        continue
835
      lresult = cls._VALID_LINE_RE.match(line)
836
      if lresult is not None:
837
        if old_minor is not None:
838
          results[old_minor] = old_line
839
        old_minor = int(lresult.group(1))
840
        old_line = line
841
      else:
842
        if old_minor is not None:
843
          old_line += " " + line.strip()
844
    # add last line
845
    if old_minor is not None:
846
      results[old_minor] = old_line
847
    return results
848

    
849
  @classmethod
850
  def _GetVersion(cls):
851
    """Return the DRBD version.
852

853
    This will return a dict with keys:
854
      - k_major
855
      - k_minor
856
      - k_point
857
      - api
858
      - proto
859
      - proto2 (only on drbd > 8.2.X)
860

861
    """
862
    proc_data = cls._GetProcData()
863
    first_line = proc_data[0].strip()
864
    version = cls._VERSION_RE.match(first_line)
865
    if not version:
866
      raise errors.BlockDeviceError("Can't parse DRBD version from '%s'" %
867
                                    first_line)
868

    
869
    values = version.groups()
870
    retval = {'k_major': int(values[0]),
871
              'k_minor': int(values[1]),
872
              'k_point': int(values[2]),
873
              'api': int(values[3]),
874
              'proto': int(values[4]),
875
             }
876
    if values[5] is not None:
877
      retval['proto2'] = values[5]
878

    
879
    return retval
880

    
881
  @staticmethod
882
  def _DevPath(minor):
883
    """Return the path to a drbd device for a given minor.
884

885
    """
886
    return "/dev/drbd%d" % minor
887

    
888
  @classmethod
889
  def GetUsedDevs(cls):
890
    """Compute the list of used DRBD devices.
891

892
    """
893
    data = cls._GetProcData()
894

    
895
    used_devs = {}
896
    for line in data:
897
      match = cls._VALID_LINE_RE.match(line)
898
      if not match:
899
        continue
900
      minor = int(match.group(1))
901
      state = match.group(2)
902
      if state == cls._ST_UNCONFIGURED:
903
        continue
904
      used_devs[minor] = state, line
905

    
906
    return used_devs
907

    
908
  def _SetFromMinor(self, minor):
909
    """Set our parameters based on the given minor.
910

911
    This sets our minor variable and our dev_path.
912

913
    """
914
    if minor is None:
915
      self.minor = self.dev_path = None
916
      self.attached = False
917
    else:
918
      self.minor = minor
919
      self.dev_path = self._DevPath(minor)
920
      self.attached = True
921

    
922
  @staticmethod
923
  def _CheckMetaSize(meta_device):
924
    """Check if the given meta device looks like a valid one.
925

926
    This currently only check the size, which must be around
927
    128MiB.
928

929
    """
930
    result = utils.RunCmd(["blockdev", "--getsize", meta_device])
931
    if result.failed:
932
      _ThrowError("Failed to get device size: %s - %s",
933
                  result.fail_reason, result.output)
934
    try:
935
      sectors = int(result.stdout)
936
    except (TypeError, ValueError):
937
      _ThrowError("Invalid output from blockdev: '%s'", result.stdout)
938
    bytes = sectors * 512
939
    if bytes < 128 * 1024 * 1024: # less than 128MiB
940
      _ThrowError("Meta device too small (%.2fMib)", (bytes / 1024 / 1024))
941
    # the maximum *valid* size of the meta device when living on top
942
    # of LVM is hard to compute: it depends on the number of stripes
943
    # and the PE size; e.g. a 2-stripe, 64MB PE will result in a 128MB
944
    # (normal size), but an eight-stripe 128MB PE will result in a 1GB
945
    # size meta device; as such, we restrict it to 1GB (a little bit
946
    # too generous, but making assumptions about PE size is hard)
947
    if bytes > 1024 * 1024 * 1024:
948
      _ThrowError("Meta device too big (%.2fMiB)", (bytes / 1024 / 1024))
949

    
950
  def Rename(self, new_id):
951
    """Rename a device.
952

953
    This is not supported for drbd devices.
954

955
    """
956
    raise errors.ProgrammerError("Can't rename a drbd device")
957

    
958

    
959
class DRBD8(BaseDRBD):
960
  """DRBD v8.x block device.
961

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

966
  The unique_id for the drbd device is the (local_ip, local_port,
967
  remote_ip, remote_port) tuple, and it must have two children: the
968
  data device and the meta_device. The meta device is checked for
969
  valid size and is zeroed on create.
970

971
  """
972
  _MAX_MINORS = 255
973
  _PARSE_SHOW = None
974

    
975
  # timeout constants
976
  _NET_RECONFIG_TIMEOUT = 60
977

    
978
  def __init__(self, unique_id, children, size):
979
    if children and children.count(None) > 0:
980
      children = []
981
    if len(children) not in (0, 2):
982
      raise ValueError("Invalid configuration data %s" % str(children))
983
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 6:
984
      raise ValueError("Invalid configuration data %s" % str(unique_id))
985
    (self._lhost, self._lport,
986
     self._rhost, self._rport,
987
     self._aminor, self._secret) = unique_id
988
    if children:
989
      if not _CanReadDevice(children[1].dev_path):
990
        logging.info("drbd%s: Ignoring unreadable meta device", self._aminor)
991
        children = []
992
    super(DRBD8, self).__init__(unique_id, children, size)
993
    self.major = self._DRBD_MAJOR
994
    version = self._GetVersion()
995
    if version['k_major'] != 8 :
996
      _ThrowError("Mismatch in DRBD kernel version and requested ganeti"
997
                  " usage: kernel is %s.%s, ganeti wants 8.x",
998
                  version['k_major'], version['k_minor'])
999

    
1000
    if (self._lhost is not None and self._lhost == self._rhost and
1001
        self._lport == self._rport):
1002
      raise ValueError("Invalid configuration data, same local/remote %s" %
1003
                       (unique_id,))
1004
    self.Attach()
1005

    
1006
  @classmethod
1007
  def _InitMeta(cls, minor, dev_path):
1008
    """Initialize a meta device.
1009

1010
    This will not work if the given minor is in use.
1011

1012
    """
1013
    result = utils.RunCmd(["drbdmeta", "--force", cls._DevPath(minor),
1014
                           "v08", dev_path, "0", "create-md"])
1015
    if result.failed:
1016
      _ThrowError("Can't initialize meta device: %s", result.output)
1017

    
1018
  @classmethod
1019
  def _FindUnusedMinor(cls):
1020
    """Find an unused DRBD device.
1021

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

1025
    """
1026
    data = cls._GetProcData()
1027

    
1028
    highest = None
1029
    for line in data:
1030
      match = cls._UNUSED_LINE_RE.match(line)
1031
      if match:
1032
        return int(match.group(1))
1033
      match = cls._VALID_LINE_RE.match(line)
1034
      if match:
1035
        minor = int(match.group(1))
1036
        highest = max(highest, minor)
1037
    if highest is None: # there are no minors in use at all
1038
      return 0
1039
    if highest >= cls._MAX_MINORS:
1040
      logging.error("Error: no free drbd minors!")
1041
      raise errors.BlockDeviceError("Can't find a free DRBD minor")
1042
    return highest + 1
1043

    
1044
  @classmethod
1045
  def _GetShowParser(cls):
1046
    """Return a parser for `drbd show` output.
1047

1048
    This will either create or return an already-create parser for the
1049
    output of the command `drbd show`.
1050

1051
    """
1052
    if cls._PARSE_SHOW is not None:
1053
      return cls._PARSE_SHOW
1054

    
1055
    # pyparsing setup
1056
    lbrace = pyp.Literal("{").suppress()
1057
    rbrace = pyp.Literal("}").suppress()
1058
    semi = pyp.Literal(";").suppress()
1059
    # this also converts the value to an int
1060
    number = pyp.Word(pyp.nums).setParseAction(lambda s, l, t: int(t[0]))
1061

    
1062
    comment = pyp.Literal ("#") + pyp.Optional(pyp.restOfLine)
1063
    defa = pyp.Literal("_is_default").suppress()
1064
    dbl_quote = pyp.Literal('"').suppress()
1065

    
1066
    keyword = pyp.Word(pyp.alphanums + '-')
1067

    
1068
    # value types
1069
    value = pyp.Word(pyp.alphanums + '_-/.:')
1070
    quoted = dbl_quote + pyp.CharsNotIn('"') + dbl_quote
1071
    addr_type = (pyp.Optional(pyp.Literal("ipv4")).suppress() +
1072
                 pyp.Optional(pyp.Literal("ipv6")).suppress())
1073
    addr_port = (addr_type + pyp.Word(pyp.nums + '.') +
1074
                 pyp.Literal(':').suppress() + number)
1075
    # meta device, extended syntax
1076
    meta_value = ((value ^ quoted) + pyp.Literal('[').suppress() +
1077
                  number + pyp.Word(']').suppress())
1078
    # device name, extended syntax
1079
    device_value = pyp.Literal("minor").suppress() + number
1080

    
1081
    # a statement
1082
    stmt = (~rbrace + keyword + ~lbrace +
1083
            pyp.Optional(addr_port ^ value ^ quoted ^ meta_value ^
1084
                         device_value) +
1085
            pyp.Optional(defa) + semi +
1086
            pyp.Optional(pyp.restOfLine).suppress())
1087

    
1088
    # an entire section
1089
    section_name = pyp.Word(pyp.alphas + '_')
1090
    section = section_name + lbrace + pyp.ZeroOrMore(pyp.Group(stmt)) + rbrace
1091

    
1092
    bnf = pyp.ZeroOrMore(pyp.Group(section ^ stmt))
1093
    bnf.ignore(comment)
1094

    
1095
    cls._PARSE_SHOW = bnf
1096

    
1097
    return bnf
1098

    
1099
  @classmethod
1100
  def _GetShowData(cls, minor):
1101
    """Return the `drbdsetup show` data for a minor.
1102

1103
    """
1104
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "show"])
1105
    if result.failed:
1106
      logging.error("Can't display the drbd config: %s - %s",
1107
                    result.fail_reason, result.output)
1108
      return None
1109
    return result.stdout
1110

    
1111
  @classmethod
1112
  def _GetDevInfo(cls, out):
1113
    """Parse details about a given DRBD minor.
1114

1115
    This return, if available, the local backing device (as a path)
1116
    and the local and remote (ip, port) information from a string
1117
    containing the output of the `drbdsetup show` command as returned
1118
    by _GetShowData.
1119

1120
    """
1121
    data = {}
1122
    if not out:
1123
      return data
1124

    
1125
    bnf = cls._GetShowParser()
1126
    # run pyparse
1127

    
1128
    try:
1129
      results = bnf.parseString(out)
1130
    except pyp.ParseException, err:
1131
      _ThrowError("Can't parse drbdsetup show output: %s", str(err))
1132

    
1133
    # and massage the results into our desired format
1134
    for section in results:
1135
      sname = section[0]
1136
      if sname == "_this_host":
1137
        for lst in section[1:]:
1138
          if lst[0] == "disk":
1139
            data["local_dev"] = lst[1]
1140
          elif lst[0] == "meta-disk":
1141
            data["meta_dev"] = lst[1]
1142
            data["meta_index"] = lst[2]
1143
          elif lst[0] == "address":
1144
            data["local_addr"] = tuple(lst[1:])
1145
      elif sname == "_remote_host":
1146
        for lst in section[1:]:
1147
          if lst[0] == "address":
1148
            data["remote_addr"] = tuple(lst[1:])
1149
    return data
1150

    
1151
  def _MatchesLocal(self, info):
1152
    """Test if our local config matches with an existing device.
1153

1154
    The parameter should be as returned from `_GetDevInfo()`. This
1155
    method tests if our local backing device is the same as the one in
1156
    the info parameter, in effect testing if we look like the given
1157
    device.
1158

1159
    """
1160
    if self._children:
1161
      backend, meta = self._children
1162
    else:
1163
      backend = meta = None
1164

    
1165
    if backend is not None:
1166
      retval = ("local_dev" in info and info["local_dev"] == backend.dev_path)
1167
    else:
1168
      retval = ("local_dev" not in info)
1169

    
1170
    if meta is not None:
1171
      retval = retval and ("meta_dev" in info and
1172
                           info["meta_dev"] == meta.dev_path)
1173
      retval = retval and ("meta_index" in info and
1174
                           info["meta_index"] == 0)
1175
    else:
1176
      retval = retval and ("meta_dev" not in info and
1177
                           "meta_index" not in info)
1178
    return retval
1179

    
1180
  def _MatchesNet(self, info):
1181
    """Test if our network config matches with an existing device.
1182

1183
    The parameter should be as returned from `_GetDevInfo()`. This
1184
    method tests if our network configuration is the same as the one
1185
    in the info parameter, in effect testing if we look like the given
1186
    device.
1187

1188
    """
1189
    if (((self._lhost is None and not ("local_addr" in info)) and
1190
         (self._rhost is None and not ("remote_addr" in info)))):
1191
      return True
1192

    
1193
    if self._lhost is None:
1194
      return False
1195

    
1196
    if not ("local_addr" in info and
1197
            "remote_addr" in info):
1198
      return False
1199

    
1200
    retval = (info["local_addr"] == (self._lhost, self._lport))
1201
    retval = (retval and
1202
              info["remote_addr"] == (self._rhost, self._rport))
1203
    return retval
1204

    
1205
  @classmethod
1206
  def _AssembleLocal(cls, minor, backend, meta, size):
1207
    """Configure the local part of a DRBD device.
1208

1209
    """
1210
    args = ["drbdsetup", cls._DevPath(minor), "disk",
1211
            backend, meta, "0",
1212
            "-e", "detach",
1213
            "--create-device"]
1214
    if size:
1215
      args.extend(["-d", "%sm" % size])
1216
    if not constants.DRBD_BARRIERS: # disable barriers, if configured so
1217
      version = cls._GetVersion()
1218
      # various DRBD versions support different disk barrier options;
1219
      # what we aim here is to revert back to the 'drain' method of
1220
      # disk flushes and to disable metadata barriers, in effect going
1221
      # back to pre-8.0.7 behaviour
1222
      vmaj = version['k_major']
1223
      vmin = version['k_minor']
1224
      vrel = version['k_point']
1225
      assert vmaj == 8
1226
      if vmin == 0: # 8.0.x
1227
        if vrel >= 12:
1228
          args.extend(['-i', '-m'])
1229
      elif vmin == 2: # 8.2.x
1230
        if vrel >= 7:
1231
          args.extend(['-i', '-m'])
1232
      elif vmaj >= 3: # 8.3.x or newer
1233
        args.extend(['-i', '-a', 'm'])
1234
    result = utils.RunCmd(args)
1235
    if result.failed:
1236
      _ThrowError("drbd%d: can't attach local disk: %s", minor, result.output)
1237

    
1238
  @classmethod
1239
  def _AssembleNet(cls, minor, net_info, protocol,
1240
                   dual_pri=False, hmac=None, secret=None):
1241
    """Configure the network part of the device.
1242

1243
    """
1244
    lhost, lport, rhost, rport = net_info
1245
    if None in net_info:
1246
      # we don't want network connection and actually want to make
1247
      # sure its shutdown
1248
      cls._ShutdownNet(minor)
1249
      return
1250

    
1251
    # Workaround for a race condition. When DRBD is doing its dance to
1252
    # establish a connection with its peer, it also sends the
1253
    # synchronization speed over the wire. In some cases setting the
1254
    # sync speed only after setting up both sides can race with DRBD
1255
    # connecting, hence we set it here before telling DRBD anything
1256
    # about its peer.
1257
    cls._SetMinorSyncSpeed(minor, constants.SYNC_SPEED)
1258

    
1259
    args = ["drbdsetup", cls._DevPath(minor), "net",
1260
            "%s:%s" % (lhost, lport), "%s:%s" % (rhost, rport), protocol,
1261
            "-A", "discard-zero-changes",
1262
            "-B", "consensus",
1263
            "--create-device",
1264
            ]
1265
    if dual_pri:
1266
      args.append("-m")
1267
    if hmac and secret:
1268
      args.extend(["-a", hmac, "-x", secret])
1269
    result = utils.RunCmd(args)
1270
    if result.failed:
1271
      _ThrowError("drbd%d: can't setup network: %s - %s",
1272
                  minor, result.fail_reason, result.output)
1273

    
1274
    def _CheckNetworkConfig():
1275
      info = cls._GetDevInfo(cls._GetShowData(minor))
1276
      if not "local_addr" in info or not "remote_addr" in info:
1277
        raise utils.RetryAgain()
1278

    
1279
      if (info["local_addr"] != (lhost, lport) or
1280
          info["remote_addr"] != (rhost, rport)):
1281
        raise utils.RetryAgain()
1282

    
1283
    try:
1284
      utils.Retry(_CheckNetworkConfig, 1.0, 10.0)
1285
    except utils.RetryTimeout:
1286
      _ThrowError("drbd%d: timeout while configuring network", minor)
1287

    
1288
  def AddChildren(self, devices):
1289
    """Add a disk to the DRBD device.
1290

1291
    """
1292
    if self.minor is None:
1293
      _ThrowError("drbd%d: can't attach to dbrd8 during AddChildren",
1294
                  self._aminor)
1295
    if len(devices) != 2:
1296
      _ThrowError("drbd%d: need two devices for AddChildren", self.minor)
1297
    info = self._GetDevInfo(self._GetShowData(self.minor))
1298
    if "local_dev" in info:
1299
      _ThrowError("drbd%d: already attached to a local disk", self.minor)
1300
    backend, meta = devices
1301
    if backend.dev_path is None or meta.dev_path is None:
1302
      _ThrowError("drbd%d: children not ready during AddChildren", self.minor)
1303
    backend.Open()
1304
    meta.Open()
1305
    self._CheckMetaSize(meta.dev_path)
1306
    self._InitMeta(self._FindUnusedMinor(), meta.dev_path)
1307

    
1308
    self._AssembleLocal(self.minor, backend.dev_path, meta.dev_path, self.size)
1309
    self._children = devices
1310

    
1311
  def RemoveChildren(self, devices):
1312
    """Detach the drbd device from local storage.
1313

1314
    """
1315
    if self.minor is None:
1316
      _ThrowError("drbd%d: can't attach to drbd8 during RemoveChildren",
1317
                  self._aminor)
1318
    # early return if we don't actually have backing storage
1319
    info = self._GetDevInfo(self._GetShowData(self.minor))
1320
    if "local_dev" not in info:
1321
      return
1322
    if len(self._children) != 2:
1323
      _ThrowError("drbd%d: we don't have two children: %s", self.minor,
1324
                  self._children)
1325
    if self._children.count(None) == 2: # we don't actually have children :)
1326
      logging.warning("drbd%d: requested detach while detached", self.minor)
1327
      return
1328
    if len(devices) != 2:
1329
      _ThrowError("drbd%d: we need two children in RemoveChildren", self.minor)
1330
    for child, dev in zip(self._children, devices):
1331
      if dev != child.dev_path:
1332
        _ThrowError("drbd%d: mismatch in local storage (%s != %s) in"
1333
                    " RemoveChildren", self.minor, dev, child.dev_path)
1334

    
1335
    self._ShutdownLocal(self.minor)
1336
    self._children = []
1337

    
1338
  @classmethod
1339
  def _SetMinorSyncSpeed(cls, minor, kbytes):
1340
    """Set the speed of the DRBD syncer.
1341

1342
    This is the low-level implementation.
1343

1344
    @type minor: int
1345
    @param minor: the drbd minor whose settings we change
1346
    @type kbytes: int
1347
    @param kbytes: the speed in kbytes/second
1348
    @rtype: boolean
1349
    @return: the success of the operation
1350

1351
    """
1352
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "syncer",
1353
                           "-r", "%d" % kbytes, "--create-device"])
1354
    if result.failed:
1355
      logging.error("Can't change syncer rate: %s - %s",
1356
                    result.fail_reason, result.output)
1357
    return not result.failed
1358

    
1359
  def SetSyncSpeed(self, kbytes):
1360
    """Set the speed of the DRBD syncer.
1361

1362
    @type kbytes: int
1363
    @param kbytes: the speed in kbytes/second
1364
    @rtype: boolean
1365
    @return: the success of the operation
1366

1367
    """
1368
    if self.minor is None:
1369
      logging.info("Not attached during SetSyncSpeed")
1370
      return False
1371
    children_result = super(DRBD8, self).SetSyncSpeed(kbytes)
1372
    return self._SetMinorSyncSpeed(self.minor, kbytes) and children_result
1373

    
1374
  def GetProcStatus(self):
1375
    """Return device data from /proc.
1376

1377
    """
1378
    if self.minor is None:
1379
      _ThrowError("drbd%d: GetStats() called while not attached", self._aminor)
1380
    proc_info = self._MassageProcData(self._GetProcData())
1381
    if self.minor not in proc_info:
1382
      _ThrowError("drbd%d: can't find myself in /proc", self.minor)
1383
    return DRBD8Status(proc_info[self.minor])
1384

    
1385
  def GetSyncStatus(self):
1386
    """Returns the sync status of the device.
1387

1388

1389
    If sync_percent is None, it means all is ok
1390
    If estimated_time is None, it means we can't estimate
1391
    the time needed, otherwise it's the time left in seconds.
1392

1393

1394
    We set the is_degraded parameter to True on two conditions:
1395
    network not connected or local disk missing.
1396

1397
    We compute the ldisk parameter based on whether we have a local
1398
    disk or not.
1399

1400
    @rtype: objects.BlockDevStatus
1401

1402
    """
1403
    if self.minor is None and not self.Attach():
1404
      _ThrowError("drbd%d: can't Attach() in GetSyncStatus", self._aminor)
1405

    
1406
    stats = self.GetProcStatus()
1407
    is_degraded = not stats.is_connected or not stats.is_disk_uptodate
1408

    
1409
    if stats.is_disk_uptodate:
1410
      ldisk_status = constants.LDS_OKAY
1411
    elif stats.is_diskless:
1412
      ldisk_status = constants.LDS_FAULTY
1413
    else:
1414
      ldisk_status = constants.LDS_UNKNOWN
1415

    
1416
    return objects.BlockDevStatus(dev_path=self.dev_path,
1417
                                  major=self.major,
1418
                                  minor=self.minor,
1419
                                  sync_percent=stats.sync_percent,
1420
                                  estimated_time=stats.est_time,
1421
                                  is_degraded=is_degraded,
1422
                                  ldisk_status=ldisk_status)
1423

    
1424
  def Open(self, force=False):
1425
    """Make the local state primary.
1426

1427
    If the 'force' parameter is given, the '-o' option is passed to
1428
    drbdsetup. Since this is a potentially dangerous operation, the
1429
    force flag should be only given after creation, when it actually
1430
    is mandatory.
1431

1432
    """
1433
    if self.minor is None and not self.Attach():
1434
      logging.error("DRBD cannot attach to a device during open")
1435
      return False
1436
    cmd = ["drbdsetup", self.dev_path, "primary"]
1437
    if force:
1438
      cmd.append("-o")
1439
    result = utils.RunCmd(cmd)
1440
    if result.failed:
1441
      _ThrowError("drbd%d: can't make drbd device primary: %s", self.minor,
1442
                  result.output)
1443

    
1444
  def Close(self):
1445
    """Make the local state secondary.
1446

1447
    This will, of course, fail if the device is in use.
1448

1449
    """
1450
    if self.minor is None and not self.Attach():
1451
      _ThrowError("drbd%d: can't Attach() in Close()", self._aminor)
1452
    result = utils.RunCmd(["drbdsetup", self.dev_path, "secondary"])
1453
    if result.failed:
1454
      _ThrowError("drbd%d: can't switch drbd device to secondary: %s",
1455
                  self.minor, result.output)
1456

    
1457
  def DisconnectNet(self):
1458
    """Removes network configuration.
1459

1460
    This method shutdowns the network side of the device.
1461

1462
    The method will wait up to a hardcoded timeout for the device to
1463
    go into standalone after the 'disconnect' command before
1464
    re-configuring it, as sometimes it takes a while for the
1465
    disconnect to actually propagate and thus we might issue a 'net'
1466
    command while the device is still connected. If the device will
1467
    still be attached to the network and we time out, we raise an
1468
    exception.
1469

1470
    """
1471
    if self.minor is None:
1472
      _ThrowError("drbd%d: disk not attached in re-attach net", self._aminor)
1473

    
1474
    if None in (self._lhost, self._lport, self._rhost, self._rport):
1475
      _ThrowError("drbd%d: DRBD disk missing network info in"
1476
                  " DisconnectNet()", self.minor)
1477

    
1478
    class _DisconnectStatus:
1479
      def __init__(self, ever_disconnected):
1480
        self.ever_disconnected = ever_disconnected
1481

    
1482
    dstatus = _DisconnectStatus(_IgnoreError(self._ShutdownNet, self.minor))
1483

    
1484
    def _WaitForDisconnect():
1485
      if self.GetProcStatus().is_standalone:
1486
        return
1487

    
1488
      # retry the disconnect, it seems possible that due to a well-time
1489
      # disconnect on the peer, my disconnect command might be ignored and
1490
      # forgotten
1491
      dstatus.ever_disconnected = \
1492
        _IgnoreError(self._ShutdownNet, self.minor) or dstatus.ever_disconnected
1493

    
1494
      raise utils.RetryAgain()
1495

    
1496
    # Keep start time
1497
    start_time = time.time()
1498

    
1499
    try:
1500
      # Start delay at 100 milliseconds and grow up to 2 seconds
1501
      utils.Retry(_WaitForDisconnect, (0.1, 1.5, 2.0),
1502
                  self._NET_RECONFIG_TIMEOUT)
1503
    except utils.RetryTimeout:
1504
      if dstatus.ever_disconnected:
1505
        msg = ("drbd%d: device did not react to the"
1506
               " 'disconnect' command in a timely manner")
1507
      else:
1508
        msg = "drbd%d: can't shutdown network, even after multiple retries"
1509

    
1510
      _ThrowError(msg, self.minor)
1511

    
1512
    reconfig_time = time.time() - start_time
1513
    if reconfig_time > (self._NET_RECONFIG_TIMEOUT * 0.25):
1514
      logging.info("drbd%d: DisconnectNet: detach took %.3f seconds",
1515
                   self.minor, reconfig_time)
1516

    
1517
  def AttachNet(self, multimaster):
1518
    """Reconnects the network.
1519

1520
    This method connects the network side of the device with a
1521
    specified multi-master flag. The device needs to be 'Standalone'
1522
    but have valid network configuration data.
1523

1524
    Args:
1525
      - multimaster: init the network in dual-primary mode
1526

1527
    """
1528
    if self.minor is None:
1529
      _ThrowError("drbd%d: device not attached in AttachNet", self._aminor)
1530

    
1531
    if None in (self._lhost, self._lport, self._rhost, self._rport):
1532
      _ThrowError("drbd%d: missing network info in AttachNet()", self.minor)
1533

    
1534
    status = self.GetProcStatus()
1535

    
1536
    if not status.is_standalone:
1537
      _ThrowError("drbd%d: device is not standalone in AttachNet", self.minor)
1538

    
1539
    self._AssembleNet(self.minor,
1540
                      (self._lhost, self._lport, self._rhost, self._rport),
1541
                      constants.DRBD_NET_PROTOCOL, dual_pri=multimaster,
1542
                      hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1543

    
1544
  def Attach(self):
1545
    """Check if our minor is configured.
1546

1547
    This doesn't do any device configurations - it only checks if the
1548
    minor is in a state different from Unconfigured.
1549

1550
    Note that this function will not change the state of the system in
1551
    any way (except in case of side-effects caused by reading from
1552
    /proc).
1553

1554
    """
1555
    used_devs = self.GetUsedDevs()
1556
    if self._aminor in used_devs:
1557
      minor = self._aminor
1558
    else:
1559
      minor = None
1560

    
1561
    self._SetFromMinor(minor)
1562
    return minor is not None
1563

    
1564
  def Assemble(self):
1565
    """Assemble the drbd.
1566

1567
    Method:
1568
      - if we have a configured device, we try to ensure that it matches
1569
        our config
1570
      - if not, we create it from zero
1571

1572
    """
1573
    super(DRBD8, self).Assemble()
1574

    
1575
    self.Attach()
1576
    if self.minor is None:
1577
      # local device completely unconfigured
1578
      self._FastAssemble()
1579
    else:
1580
      # we have to recheck the local and network status and try to fix
1581
      # the device
1582
      self._SlowAssemble()
1583

    
1584
  def _SlowAssemble(self):
1585
    """Assembles the DRBD device from a (partially) configured device.
1586

1587
    In case of partially attached (local device matches but no network
1588
    setup), we perform the network attach. If successful, we re-test
1589
    the attach if can return success.
1590

1591
    """
1592
    # TODO: Rewrite to not use a for loop just because there is 'break'
1593
    # pylint: disable-msg=W0631
1594
    net_data = (self._lhost, self._lport, self._rhost, self._rport)
1595
    for minor in (self._aminor,):
1596
      info = self._GetDevInfo(self._GetShowData(minor))
1597
      match_l = self._MatchesLocal(info)
1598
      match_r = self._MatchesNet(info)
1599

    
1600
      if match_l and match_r:
1601
        # everything matches
1602
        break
1603

    
1604
      if match_l and not match_r and "local_addr" not in info:
1605
        # disk matches, but not attached to network, attach and recheck
1606
        self._AssembleNet(minor, net_data, constants.DRBD_NET_PROTOCOL,
1607
                          hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1608
        if self._MatchesNet(self._GetDevInfo(self._GetShowData(minor))):
1609
          break
1610
        else:
1611
          _ThrowError("drbd%d: network attach successful, but 'drbdsetup"
1612
                      " show' disagrees", minor)
1613

    
1614
      if match_r and "local_dev" not in info:
1615
        # no local disk, but network attached and it matches
1616
        self._AssembleLocal(minor, self._children[0].dev_path,
1617
                            self._children[1].dev_path, self.size)
1618
        if self._MatchesNet(self._GetDevInfo(self._GetShowData(minor))):
1619
          break
1620
        else:
1621
          _ThrowError("drbd%d: disk attach successful, but 'drbdsetup"
1622
                      " show' disagrees", minor)
1623

    
1624
      # this case must be considered only if we actually have local
1625
      # storage, i.e. not in diskless mode, because all diskless
1626
      # devices are equal from the point of view of local
1627
      # configuration
1628
      if (match_l and "local_dev" in info and
1629
          not match_r and "local_addr" in info):
1630
        # strange case - the device network part points to somewhere
1631
        # else, even though its local storage is ours; as we own the
1632
        # drbd space, we try to disconnect from the remote peer and
1633
        # reconnect to our correct one
1634
        try:
1635
          self._ShutdownNet(minor)
1636
        except errors.BlockDeviceError, err:
1637
          _ThrowError("drbd%d: device has correct local storage, wrong"
1638
                      " remote peer and is unable to disconnect in order"
1639
                      " to attach to the correct peer: %s", minor, str(err))
1640
        # note: _AssembleNet also handles the case when we don't want
1641
        # local storage (i.e. one or more of the _[lr](host|port) is
1642
        # None)
1643
        self._AssembleNet(minor, net_data, constants.DRBD_NET_PROTOCOL,
1644
                          hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1645
        if self._MatchesNet(self._GetDevInfo(self._GetShowData(minor))):
1646
          break
1647
        else:
1648
          _ThrowError("drbd%d: network attach successful, but 'drbdsetup"
1649
                      " show' disagrees", minor)
1650

    
1651
    else:
1652
      minor = None
1653

    
1654
    self._SetFromMinor(minor)
1655
    if minor is None:
1656
      _ThrowError("drbd%d: cannot activate, unknown or unhandled reason",
1657
                  self._aminor)
1658

    
1659
  def _FastAssemble(self):
1660
    """Assemble the drbd device from zero.
1661

1662
    This is run when in Assemble we detect our minor is unused.
1663

1664
    """
1665
    minor = self._aminor
1666
    if self._children and self._children[0] and self._children[1]:
1667
      self._AssembleLocal(minor, self._children[0].dev_path,
1668
                          self._children[1].dev_path, self.size)
1669
    if self._lhost and self._lport and self._rhost and self._rport:
1670
      self._AssembleNet(minor,
1671
                        (self._lhost, self._lport, self._rhost, self._rport),
1672
                        constants.DRBD_NET_PROTOCOL,
1673
                        hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1674
    self._SetFromMinor(minor)
1675

    
1676
  @classmethod
1677
  def _ShutdownLocal(cls, minor):
1678
    """Detach from the local device.
1679

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

1683
    """
1684
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "detach"])
1685
    if result.failed:
1686
      _ThrowError("drbd%d: can't detach local disk: %s", minor, result.output)
1687

    
1688
  @classmethod
1689
  def _ShutdownNet(cls, minor):
1690
    """Disconnect from the remote peer.
1691

1692
    This fails if we don't have a local device.
1693

1694
    """
1695
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "disconnect"])
1696
    if result.failed:
1697
      _ThrowError("drbd%d: can't shutdown network: %s", minor, result.output)
1698

    
1699
  @classmethod
1700
  def _ShutdownAll(cls, minor):
1701
    """Deactivate the device.
1702

1703
    This will, of course, fail if the device is in use.
1704

1705
    """
1706
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "down"])
1707
    if result.failed:
1708
      _ThrowError("drbd%d: can't shutdown drbd device: %s",
1709
                  minor, result.output)
1710

    
1711
  def Shutdown(self):
1712
    """Shutdown the DRBD device.
1713

1714
    """
1715
    if self.minor is None and not self.Attach():
1716
      logging.info("drbd%d: not attached during Shutdown()", self._aminor)
1717
      return
1718
    minor = self.minor
1719
    self.minor = None
1720
    self.dev_path = None
1721
    self._ShutdownAll(minor)
1722

    
1723
  def Remove(self):
1724
    """Stub remove for DRBD devices.
1725

1726
    """
1727
    self.Shutdown()
1728

    
1729
  @classmethod
1730
  def Create(cls, unique_id, children, size):
1731
    """Create a new DRBD8 device.
1732

1733
    Since DRBD devices are not created per se, just assembled, this
1734
    function only initializes the metadata.
1735

1736
    """
1737
    if len(children) != 2:
1738
      raise errors.ProgrammerError("Invalid setup for the drbd device")
1739
    # check that the minor is unused
1740
    aminor = unique_id[4]
1741
    proc_info = cls._MassageProcData(cls._GetProcData())
1742
    if aminor in proc_info:
1743
      status = DRBD8Status(proc_info[aminor])
1744
      in_use = status.is_in_use
1745
    else:
1746
      in_use = False
1747
    if in_use:
1748
      _ThrowError("drbd%d: minor is already in use at Create() time", aminor)
1749
    meta = children[1]
1750
    meta.Assemble()
1751
    if not meta.Attach():
1752
      _ThrowError("drbd%d: can't attach to meta device '%s'",
1753
                  aminor, meta)
1754
    cls._CheckMetaSize(meta.dev_path)
1755
    cls._InitMeta(aminor, meta.dev_path)
1756
    return cls(unique_id, children, size)
1757

    
1758
  def Grow(self, amount):
1759
    """Resize the DRBD device and its backing storage.
1760

1761
    """
1762
    if self.minor is None:
1763
      _ThrowError("drbd%d: Grow called while not attached", self._aminor)
1764
    if len(self._children) != 2 or None in self._children:
1765
      _ThrowError("drbd%d: cannot grow diskless device", self.minor)
1766
    self._children[0].Grow(amount)
1767
    result = utils.RunCmd(["drbdsetup", self.dev_path, "resize", "-s",
1768
                           "%dm" % (self.size + amount)])
1769
    if result.failed:
1770
      _ThrowError("drbd%d: resize failed: %s", self.minor, result.output)
1771

    
1772

    
1773
class FileStorage(BlockDev):
1774
  """File device.
1775

1776
  This class represents the a file storage backend device.
1777

1778
  The unique_id for the file device is a (file_driver, file_path) tuple.
1779

1780
  """
1781
  def __init__(self, unique_id, children, size):
1782
    """Initalizes a file device backend.
1783

1784
    """
1785
    if children:
1786
      raise errors.BlockDeviceError("Invalid setup for file device")
1787
    super(FileStorage, self).__init__(unique_id, children, size)
1788
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
1789
      raise ValueError("Invalid configuration data %s" % str(unique_id))
1790
    self.driver = unique_id[0]
1791
    self.dev_path = unique_id[1]
1792
    self.Attach()
1793

    
1794
  def Assemble(self):
1795
    """Assemble the device.
1796

1797
    Checks whether the file device exists, raises BlockDeviceError otherwise.
1798

1799
    """
1800
    if not os.path.exists(self.dev_path):
1801
      _ThrowError("File device '%s' does not exist" % self.dev_path)
1802

    
1803
  def Shutdown(self):
1804
    """Shutdown the device.
1805

1806
    This is a no-op for the file type, as we don't deactivate
1807
    the file on shutdown.
1808

1809
    """
1810
    pass
1811

    
1812
  def Open(self, force=False):
1813
    """Make the device ready for I/O.
1814

1815
    This is a no-op for the file type.
1816

1817
    """
1818
    pass
1819

    
1820
  def Close(self):
1821
    """Notifies that the device will no longer be used for I/O.
1822

1823
    This is a no-op for the file type.
1824

1825
    """
1826
    pass
1827

    
1828
  def Remove(self):
1829
    """Remove the file backing the block device.
1830

1831
    @rtype: boolean
1832
    @return: True if the removal was successful
1833

1834
    """
1835
    try:
1836
      os.remove(self.dev_path)
1837
    except OSError, err:
1838
      if err.errno != errno.ENOENT:
1839
        _ThrowError("Can't remove file '%s': %s", self.dev_path, err)
1840

    
1841
  def Rename(self, new_id):
1842
    """Renames the file.
1843

1844
    """
1845
    # TODO: implement rename for file-based storage
1846
    _ThrowError("Rename is not supported for file-based storage")
1847

    
1848
  def Grow(self, amount):
1849
    """Grow the file
1850

1851
    @param amount: the amount (in mebibytes) to grow with
1852

1853
    """
1854
    # TODO: implement grow for file-based storage
1855
    _ThrowError("Grow not supported for file-based storage")
1856

    
1857
  def Attach(self):
1858
    """Attach to an existing file.
1859

1860
    Check if this file already exists.
1861

1862
    @rtype: boolean
1863
    @return: True if file exists
1864

1865
    """
1866
    self.attached = os.path.exists(self.dev_path)
1867
    return self.attached
1868

    
1869
  def GetActualSize(self):
1870
    """Return the actual disk size.
1871

1872
    @note: the device needs to be active when this is called
1873

1874
    """
1875
    assert self.attached, "BlockDevice not attached in GetActualSize()"
1876
    try:
1877
      st = os.stat(self.dev_path)
1878
      return st.st_size
1879
    except OSError, err:
1880
      _ThrowError("Can't stat %s: %s", self.dev_path, err)
1881

    
1882
  @classmethod
1883
  def Create(cls, unique_id, children, size):
1884
    """Create a new file.
1885

1886
    @param size: the size of file in MiB
1887

1888
    @rtype: L{bdev.FileStorage}
1889
    @return: an instance of FileStorage
1890

1891
    """
1892
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
1893
      raise ValueError("Invalid configuration data %s" % str(unique_id))
1894
    dev_path = unique_id[1]
1895
    if os.path.exists(dev_path):
1896
      _ThrowError("File already existing: %s", dev_path)
1897
    try:
1898
      f = open(dev_path, 'w')
1899
      f.truncate(size * 1024 * 1024)
1900
      f.close()
1901
    except IOError, err:
1902
      _ThrowError("Error in file creation: %", str(err))
1903

    
1904
    return FileStorage(unique_id, children, size)
1905

    
1906

    
1907
DEV_MAP = {
1908
  constants.LD_LV: LogicalVolume,
1909
  constants.LD_DRBD8: DRBD8,
1910
  constants.LD_FILE: FileStorage,
1911
  }
1912

    
1913

    
1914
def FindDevice(dev_type, unique_id, children, size):
1915
  """Search for an existing, assembled device.
1916

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

1920
  """
1921
  if dev_type not in DEV_MAP:
1922
    raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
1923
  device = DEV_MAP[dev_type](unique_id, children, size)
1924
  if not device.attached:
1925
    return None
1926
  return device
1927

    
1928

    
1929
def Assemble(dev_type, unique_id, children, size):
1930
  """Try to attach or assemble an existing device.
1931

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

1935
  """
1936
  if dev_type not in DEV_MAP:
1937
    raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
1938
  device = DEV_MAP[dev_type](unique_id, children, size)
1939
  device.Assemble()
1940
  return device
1941

    
1942

    
1943
def Create(dev_type, unique_id, children, size):
1944
  """Create a device.
1945

1946
  """
1947
  if dev_type not in DEV_MAP:
1948
    raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
1949
  device = DEV_MAP[dev_type].Create(unique_id, children, size)
1950
  return device