Statistics
| Branch: | Tag: | Revision:

root / lib / bdev.py @ 1122eb25

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

    
796
  _DRBD_MAJOR = 147
797
  _ST_UNCONFIGURED = "Unconfigured"
798
  _ST_WFCONNECTION = "WFConnection"
799
  _ST_CONNECTED = "Connected"
800

    
801
  _STATUS_FILE = "/proc/drbd"
802

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

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

    
820
  @staticmethod
821
  def _MassageProcData(data):
822
    """Transform the output of _GetProdData into a nicer form.
823

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

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

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

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

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

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

    
878
    return retval
879

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

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

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

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

    
894
    used_devs = {}
895
    valid_line = re.compile("^ *([0-9]+): cs:([^ ]+).*$")
896
    for line in data:
897
      match = valid_line.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 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
    unused_line = re.compile("^ *([0-9]+): cs:Unconfigured$")
1029
    used_line = re.compile("^ *([0-9]+): cs:")
1030
    highest = None
1031
    for line in data:
1032
      match = unused_line.match(line)
1033
      if match:
1034
        return int(match.group(1))
1035
      match = used_line.match(line)
1036
      if match:
1037
        minor = int(match.group(1))
1038
        highest = max(highest, minor)
1039
    if highest is None: # there are no minors in use at all
1040
      return 0
1041
    if highest >= cls._MAX_MINORS:
1042
      logging.error("Error: no free drbd minors!")
1043
      raise errors.BlockDeviceError("Can't find a free DRBD minor")
1044
    return highest + 1
1045

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

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

1053
    """
1054
    if cls._PARSE_SHOW is not None:
1055
      return cls._PARSE_SHOW
1056

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

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

    
1068
    keyword = pyp.Word(pyp.alphanums + '-')
1069

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

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

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

    
1094
    bnf = pyp.ZeroOrMore(pyp.Group(section ^ stmt))
1095
    bnf.ignore(comment)
1096

    
1097
    cls._PARSE_SHOW = bnf
1098

    
1099
    return bnf
1100

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

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

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

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

1122
    """
1123
    data = {}
1124
    if not out:
1125
      return data
1126

    
1127
    bnf = cls._GetShowParser()
1128
    # run pyparse
1129

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

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

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

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

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

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

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

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

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

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

    
1195
    if self._lhost is None:
1196
      return False
1197

    
1198
    if not ("local_addr" in info and
1199
            "remote_addr" in info):
1200
      return False
1201

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

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

1211
    """
1212
    args = ["drbdsetup", cls._DevPath(minor), "disk",
1213
            backend, meta, "0",
1214
            "-e", "detach",
1215
            "--create-device"]
1216
    if size:
1217
      args.extend(["-d", "%sm" % size])
1218
    result = utils.RunCmd(args)
1219
    if result.failed:
1220
      _ThrowError("drbd%d: can't attach local disk: %s", minor, result.output)
1221

    
1222
  @classmethod
1223
  def _AssembleNet(cls, minor, net_info, protocol,
1224
                   dual_pri=False, hmac=None, secret=None):
1225
    """Configure the network part of the device.
1226

1227
    """
1228
    lhost, lport, rhost, rport = net_info
1229
    if None in net_info:
1230
      # we don't want network connection and actually want to make
1231
      # sure its shutdown
1232
      cls._ShutdownNet(minor)
1233
      return
1234

    
1235
    # Workaround for a race condition. When DRBD is doing its dance to
1236
    # establish a connection with its peer, it also sends the
1237
    # synchronization speed over the wire. In some cases setting the
1238
    # sync speed only after setting up both sides can race with DRBD
1239
    # connecting, hence we set it here before telling DRBD anything
1240
    # about its peer.
1241
    cls._SetMinorSyncSpeed(minor, constants.SYNC_SPEED)
1242

    
1243
    args = ["drbdsetup", cls._DevPath(minor), "net",
1244
            "%s:%s" % (lhost, lport), "%s:%s" % (rhost, rport), protocol,
1245
            "-A", "discard-zero-changes",
1246
            "-B", "consensus",
1247
            "--create-device",
1248
            ]
1249
    if dual_pri:
1250
      args.append("-m")
1251
    if hmac and secret:
1252
      args.extend(["-a", hmac, "-x", secret])
1253
    result = utils.RunCmd(args)
1254
    if result.failed:
1255
      _ThrowError("drbd%d: can't setup network: %s - %s",
1256
                  minor, result.fail_reason, result.output)
1257

    
1258
    def _CheckNetworkConfig():
1259
      info = cls._GetDevInfo(cls._GetShowData(minor))
1260
      if not "local_addr" in info or not "remote_addr" in info:
1261
        raise utils.RetryAgain()
1262

    
1263
      if (info["local_addr"] != (lhost, lport) or
1264
          info["remote_addr"] != (rhost, rport)):
1265
        raise utils.RetryAgain()
1266

    
1267
    try:
1268
      utils.Retry(_CheckNetworkConfig, 1.0, 10.0)
1269
    except utils.RetryTimeout:
1270
      _ThrowError("drbd%d: timeout while configuring network", minor)
1271

    
1272
  def AddChildren(self, devices):
1273
    """Add a disk to the DRBD device.
1274

1275
    """
1276
    if self.minor is None:
1277
      _ThrowError("drbd%d: can't attach to dbrd8 during AddChildren",
1278
                  self._aminor)
1279
    if len(devices) != 2:
1280
      _ThrowError("drbd%d: need two devices for AddChildren", self.minor)
1281
    info = self._GetDevInfo(self._GetShowData(self.minor))
1282
    if "local_dev" in info:
1283
      _ThrowError("drbd%d: already attached to a local disk", self.minor)
1284
    backend, meta = devices
1285
    if backend.dev_path is None or meta.dev_path is None:
1286
      _ThrowError("drbd%d: children not ready during AddChildren", self.minor)
1287
    backend.Open()
1288
    meta.Open()
1289
    self._CheckMetaSize(meta.dev_path)
1290
    self._InitMeta(self._FindUnusedMinor(), meta.dev_path)
1291

    
1292
    self._AssembleLocal(self.minor, backend.dev_path, meta.dev_path, self.size)
1293
    self._children = devices
1294

    
1295
  def RemoveChildren(self, devices):
1296
    """Detach the drbd device from local storage.
1297

1298
    """
1299
    if self.minor is None:
1300
      _ThrowError("drbd%d: can't attach to drbd8 during RemoveChildren",
1301
                  self._aminor)
1302
    # early return if we don't actually have backing storage
1303
    info = self._GetDevInfo(self._GetShowData(self.minor))
1304
    if "local_dev" not in info:
1305
      return
1306
    if len(self._children) != 2:
1307
      _ThrowError("drbd%d: we don't have two children: %s", self.minor,
1308
                  self._children)
1309
    if self._children.count(None) == 2: # we don't actually have children :)
1310
      logging.warning("drbd%d: requested detach while detached", self.minor)
1311
      return
1312
    if len(devices) != 2:
1313
      _ThrowError("drbd%d: we need two children in RemoveChildren", self.minor)
1314
    for child, dev in zip(self._children, devices):
1315
      if dev != child.dev_path:
1316
        _ThrowError("drbd%d: mismatch in local storage (%s != %s) in"
1317
                    " RemoveChildren", self.minor, dev, child.dev_path)
1318

    
1319
    self._ShutdownLocal(self.minor)
1320
    self._children = []
1321

    
1322
  @classmethod
1323
  def _SetMinorSyncSpeed(cls, minor, kbytes):
1324
    """Set the speed of the DRBD syncer.
1325

1326
    This is the low-level implementation.
1327

1328
    @type minor: int
1329
    @param minor: the drbd minor whose settings we change
1330
    @type kbytes: int
1331
    @param kbytes: the speed in kbytes/second
1332
    @rtype: boolean
1333
    @return: the success of the operation
1334

1335
    """
1336
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "syncer",
1337
                           "-r", "%d" % kbytes, "--create-device"])
1338
    if result.failed:
1339
      logging.error("Can't change syncer rate: %s - %s",
1340
                    result.fail_reason, result.output)
1341
    return not result.failed
1342

    
1343
  def SetSyncSpeed(self, kbytes):
1344
    """Set the speed of the DRBD syncer.
1345

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
    if self.minor is None:
1353
      logging.info("Not attached during SetSyncSpeed")
1354
      return False
1355
    children_result = super(DRBD8, self).SetSyncSpeed(kbytes)
1356
    return self._SetMinorSyncSpeed(self.minor, kbytes) and children_result
1357

    
1358
  def GetProcStatus(self):
1359
    """Return device data from /proc.
1360

1361
    """
1362
    if self.minor is None:
1363
      _ThrowError("drbd%d: GetStats() called while not attached", self._aminor)
1364
    proc_info = self._MassageProcData(self._GetProcData())
1365
    if self.minor not in proc_info:
1366
      _ThrowError("drbd%d: can't find myself in /proc", self.minor)
1367
    return DRBD8Status(proc_info[self.minor])
1368

    
1369
  def GetSyncStatus(self):
1370
    """Returns the sync status of the device.
1371

1372

1373
    If sync_percent is None, it means all is ok
1374
    If estimated_time is None, it means we can't estimate
1375
    the time needed, otherwise it's the time left in seconds.
1376

1377

1378
    We set the is_degraded parameter to True on two conditions:
1379
    network not connected or local disk missing.
1380

1381
    We compute the ldisk parameter based on whether we have a local
1382
    disk or not.
1383

1384
    @rtype: objects.BlockDevStatus
1385

1386
    """
1387
    if self.minor is None and not self.Attach():
1388
      _ThrowError("drbd%d: can't Attach() in GetSyncStatus", self._aminor)
1389

    
1390
    stats = self.GetProcStatus()
1391
    is_degraded = not stats.is_connected or not stats.is_disk_uptodate
1392

    
1393
    if stats.is_disk_uptodate:
1394
      ldisk_status = constants.LDS_OKAY
1395
    elif stats.is_diskless:
1396
      ldisk_status = constants.LDS_FAULTY
1397
    else:
1398
      ldisk_status = constants.LDS_UNKNOWN
1399

    
1400
    return objects.BlockDevStatus(dev_path=self.dev_path,
1401
                                  major=self.major,
1402
                                  minor=self.minor,
1403
                                  sync_percent=stats.sync_percent,
1404
                                  estimated_time=stats.est_time,
1405
                                  is_degraded=is_degraded,
1406
                                  ldisk_status=ldisk_status)
1407

    
1408
  def Open(self, force=False):
1409
    """Make the local state primary.
1410

1411
    If the 'force' parameter is given, the '-o' option is passed to
1412
    drbdsetup. Since this is a potentially dangerous operation, the
1413
    force flag should be only given after creation, when it actually
1414
    is mandatory.
1415

1416
    """
1417
    if self.minor is None and not self.Attach():
1418
      logging.error("DRBD cannot attach to a device during open")
1419
      return False
1420
    cmd = ["drbdsetup", self.dev_path, "primary"]
1421
    if force:
1422
      cmd.append("-o")
1423
    result = utils.RunCmd(cmd)
1424
    if result.failed:
1425
      _ThrowError("drbd%d: can't make drbd device primary: %s", self.minor,
1426
                  result.output)
1427

    
1428
  def Close(self):
1429
    """Make the local state secondary.
1430

1431
    This will, of course, fail if the device is in use.
1432

1433
    """
1434
    if self.minor is None and not self.Attach():
1435
      _ThrowError("drbd%d: can't Attach() in Close()", self._aminor)
1436
    result = utils.RunCmd(["drbdsetup", self.dev_path, "secondary"])
1437
    if result.failed:
1438
      _ThrowError("drbd%d: can't switch drbd device to secondary: %s",
1439
                  self.minor, result.output)
1440

    
1441
  def DisconnectNet(self):
1442
    """Removes network configuration.
1443

1444
    This method shutdowns the network side of the device.
1445

1446
    The method will wait up to a hardcoded timeout for the device to
1447
    go into standalone after the 'disconnect' command before
1448
    re-configuring it, as sometimes it takes a while for the
1449
    disconnect to actually propagate and thus we might issue a 'net'
1450
    command while the device is still connected. If the device will
1451
    still be attached to the network and we time out, we raise an
1452
    exception.
1453

1454
    """
1455
    if self.minor is None:
1456
      _ThrowError("drbd%d: disk not attached in re-attach net", self._aminor)
1457

    
1458
    if None in (self._lhost, self._lport, self._rhost, self._rport):
1459
      _ThrowError("drbd%d: DRBD disk missing network info in"
1460
                  " DisconnectNet()", self.minor)
1461

    
1462
    class _DisconnectStatus:
1463
      def __init__(self, ever_disconnected):
1464
        self.ever_disconnected = ever_disconnected
1465

    
1466
    dstatus = _DisconnectStatus(_IgnoreError(self._ShutdownNet, self.minor))
1467

    
1468
    def _WaitForDisconnect():
1469
      if self.GetProcStatus().is_standalone:
1470
        return
1471

    
1472
      # retry the disconnect, it seems possible that due to a well-time
1473
      # disconnect on the peer, my disconnect command might be ignored and
1474
      # forgotten
1475
      dstatus.ever_disconnected = \
1476
        _IgnoreError(self._ShutdownNet, self.minor) or dstatus.ever_disconnected
1477

    
1478
      raise utils.RetryAgain()
1479

    
1480
    # Keep start time
1481
    start_time = time.time()
1482

    
1483
    try:
1484
      # Start delay at 100 milliseconds and grow up to 2 seconds
1485
      utils.Retry(_WaitForDisconnect, (0.1, 1.5, 2.0),
1486
                  self._NET_RECONFIG_TIMEOUT)
1487
    except utils.RetryTimeout:
1488
      if dstatus.ever_disconnected:
1489
        msg = ("drbd%d: device did not react to the"
1490
               " 'disconnect' command in a timely manner")
1491
      else:
1492
        msg = "drbd%d: can't shutdown network, even after multiple retries"
1493

    
1494
      _ThrowError(msg, self.minor)
1495

    
1496
    reconfig_time = time.time() - start_time
1497
    if reconfig_time > (self._NET_RECONFIG_TIMEOUT * 0.25):
1498
      logging.info("drbd%d: DisconnectNet: detach took %.3f seconds",
1499
                   self.minor, reconfig_time)
1500

    
1501
  def AttachNet(self, multimaster):
1502
    """Reconnects the network.
1503

1504
    This method connects the network side of the device with a
1505
    specified multi-master flag. The device needs to be 'Standalone'
1506
    but have valid network configuration data.
1507

1508
    Args:
1509
      - multimaster: init the network in dual-primary mode
1510

1511
    """
1512
    if self.minor is None:
1513
      _ThrowError("drbd%d: device not attached in AttachNet", self._aminor)
1514

    
1515
    if None in (self._lhost, self._lport, self._rhost, self._rport):
1516
      _ThrowError("drbd%d: missing network info in AttachNet()", self.minor)
1517

    
1518
    status = self.GetProcStatus()
1519

    
1520
    if not status.is_standalone:
1521
      _ThrowError("drbd%d: device is not standalone in AttachNet", self.minor)
1522

    
1523
    self._AssembleNet(self.minor,
1524
                      (self._lhost, self._lport, self._rhost, self._rport),
1525
                      constants.DRBD_NET_PROTOCOL, dual_pri=multimaster,
1526
                      hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1527

    
1528
  def Attach(self):
1529
    """Check if our minor is configured.
1530

1531
    This doesn't do any device configurations - it only checks if the
1532
    minor is in a state different from Unconfigured.
1533

1534
    Note that this function will not change the state of the system in
1535
    any way (except in case of side-effects caused by reading from
1536
    /proc).
1537

1538
    """
1539
    used_devs = self.GetUsedDevs()
1540
    if self._aminor in used_devs:
1541
      minor = self._aminor
1542
    else:
1543
      minor = None
1544

    
1545
    self._SetFromMinor(minor)
1546
    return minor is not None
1547

    
1548
  def Assemble(self):
1549
    """Assemble the drbd.
1550

1551
    Method:
1552
      - if we have a configured device, we try to ensure that it matches
1553
        our config
1554
      - if not, we create it from zero
1555

1556
    """
1557
    super(DRBD8, self).Assemble()
1558

    
1559
    self.Attach()
1560
    if self.minor is None:
1561
      # local device completely unconfigured
1562
      self._FastAssemble()
1563
    else:
1564
      # we have to recheck the local and network status and try to fix
1565
      # the device
1566
      self._SlowAssemble()
1567

    
1568
  def _SlowAssemble(self):
1569
    """Assembles the DRBD device from a (partially) configured device.
1570

1571
    In case of partially attached (local device matches but no network
1572
    setup), we perform the network attach. If successful, we re-test
1573
    the attach if can return success.
1574

1575
    """
1576
    net_data = (self._lhost, self._lport, self._rhost, self._rport)
1577
    for minor in (self._aminor,):
1578
      info = self._GetDevInfo(self._GetShowData(minor))
1579
      match_l = self._MatchesLocal(info)
1580
      match_r = self._MatchesNet(info)
1581

    
1582
      if match_l and match_r:
1583
        # everything matches
1584
        break
1585

    
1586
      if match_l and not match_r and "local_addr" not in info:
1587
        # disk matches, but not attached to network, attach and recheck
1588
        self._AssembleNet(minor, net_data, constants.DRBD_NET_PROTOCOL,
1589
                          hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1590
        if self._MatchesNet(self._GetDevInfo(self._GetShowData(minor))):
1591
          break
1592
        else:
1593
          _ThrowError("drbd%d: network attach successful, but 'drbdsetup"
1594
                      " show' disagrees", minor)
1595

    
1596
      if match_r and "local_dev" not in info:
1597
        # no local disk, but network attached and it matches
1598
        self._AssembleLocal(minor, self._children[0].dev_path,
1599
                            self._children[1].dev_path, self.size)
1600
        if self._MatchesNet(self._GetDevInfo(self._GetShowData(minor))):
1601
          break
1602
        else:
1603
          _ThrowError("drbd%d: disk attach successful, but 'drbdsetup"
1604
                      " show' disagrees", minor)
1605

    
1606
      # this case must be considered only if we actually have local
1607
      # storage, i.e. not in diskless mode, because all diskless
1608
      # devices are equal from the point of view of local
1609
      # configuration
1610
      if (match_l and "local_dev" in info and
1611
          not match_r and "local_addr" in info):
1612
        # strange case - the device network part points to somewhere
1613
        # else, even though its local storage is ours; as we own the
1614
        # drbd space, we try to disconnect from the remote peer and
1615
        # reconnect to our correct one
1616
        try:
1617
          self._ShutdownNet(minor)
1618
        except errors.BlockDeviceError, err:
1619
          _ThrowError("drbd%d: device has correct local storage, wrong"
1620
                      " remote peer and is unable to disconnect in order"
1621
                      " to attach to the correct peer: %s", minor, str(err))
1622
        # note: _AssembleNet also handles the case when we don't want
1623
        # local storage (i.e. one or more of the _[lr](host|port) is
1624
        # None)
1625
        self._AssembleNet(minor, net_data, constants.DRBD_NET_PROTOCOL,
1626
                          hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1627
        if self._MatchesNet(self._GetDevInfo(self._GetShowData(minor))):
1628
          break
1629
        else:
1630
          _ThrowError("drbd%d: network attach successful, but 'drbdsetup"
1631
                      " show' disagrees", minor)
1632

    
1633
    else:
1634
      minor = None
1635

    
1636
    self._SetFromMinor(minor)
1637
    if minor is None:
1638
      _ThrowError("drbd%d: cannot activate, unknown or unhandled reason",
1639
                  self._aminor)
1640

    
1641
  def _FastAssemble(self):
1642
    """Assemble the drbd device from zero.
1643

1644
    This is run when in Assemble we detect our minor is unused.
1645

1646
    """
1647
    minor = self._aminor
1648
    if self._children and self._children[0] and self._children[1]:
1649
      self._AssembleLocal(minor, self._children[0].dev_path,
1650
                          self._children[1].dev_path, self.size)
1651
    if self._lhost and self._lport and self._rhost and self._rport:
1652
      self._AssembleNet(minor,
1653
                        (self._lhost, self._lport, self._rhost, self._rport),
1654
                        constants.DRBD_NET_PROTOCOL,
1655
                        hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1656
    self._SetFromMinor(minor)
1657

    
1658
  @classmethod
1659
  def _ShutdownLocal(cls, minor):
1660
    """Detach from the local device.
1661

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

1665
    """
1666
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "detach"])
1667
    if result.failed:
1668
      _ThrowError("drbd%d: can't detach local disk: %s", minor, result.output)
1669

    
1670
  @classmethod
1671
  def _ShutdownNet(cls, minor):
1672
    """Disconnect from the remote peer.
1673

1674
    This fails if we don't have a local device.
1675

1676
    """
1677
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "disconnect"])
1678
    if result.failed:
1679
      _ThrowError("drbd%d: can't shutdown network: %s", minor, result.output)
1680

    
1681
  @classmethod
1682
  def _ShutdownAll(cls, minor):
1683
    """Deactivate the device.
1684

1685
    This will, of course, fail if the device is in use.
1686

1687
    """
1688
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "down"])
1689
    if result.failed:
1690
      _ThrowError("drbd%d: can't shutdown drbd device: %s",
1691
                  minor, result.output)
1692

    
1693
  def Shutdown(self):
1694
    """Shutdown the DRBD device.
1695

1696
    """
1697
    if self.minor is None and not self.Attach():
1698
      logging.info("drbd%d: not attached during Shutdown()", self._aminor)
1699
      return
1700
    minor = self.minor
1701
    self.minor = None
1702
    self.dev_path = None
1703
    self._ShutdownAll(minor)
1704

    
1705
  def Remove(self):
1706
    """Stub remove for DRBD devices.
1707

1708
    """
1709
    self.Shutdown()
1710

    
1711
  @classmethod
1712
  def Create(cls, unique_id, children, size):
1713
    """Create a new DRBD8 device.
1714

1715
    Since DRBD devices are not created per se, just assembled, this
1716
    function only initializes the metadata.
1717

1718
    """
1719
    if len(children) != 2:
1720
      raise errors.ProgrammerError("Invalid setup for the drbd device")
1721
    # check that the minor is unused
1722
    aminor = unique_id[4]
1723
    proc_info = cls._MassageProcData(cls._GetProcData())
1724
    if aminor in proc_info:
1725
      status = DRBD8Status(proc_info[aminor])
1726
      in_use = status.is_in_use
1727
    else:
1728
      in_use = False
1729
    if in_use:
1730
      _ThrowError("drbd%d: minor is already in use at Create() time", aminor)
1731
    meta = children[1]
1732
    meta.Assemble()
1733
    if not meta.Attach():
1734
      _ThrowError("drbd%d: can't attach to meta device '%s'",
1735
                  aminor, meta)
1736
    cls._CheckMetaSize(meta.dev_path)
1737
    cls._InitMeta(aminor, meta.dev_path)
1738
    return cls(unique_id, children, size)
1739

    
1740
  def Grow(self, amount):
1741
    """Resize the DRBD device and its backing storage.
1742

1743
    """
1744
    if self.minor is None:
1745
      _ThrowError("drbd%d: Grow called while not attached", self._aminor)
1746
    if len(self._children) != 2 or None in self._children:
1747
      _ThrowError("drbd%d: cannot grow diskless device", self.minor)
1748
    self._children[0].Grow(amount)
1749
    result = utils.RunCmd(["drbdsetup", self.dev_path, "resize", "-s",
1750
                           "%dm" % (self.size + amount)])
1751
    if result.failed:
1752
      _ThrowError("drbd%d: resize failed: %s", self.minor, result.output)
1753

    
1754

    
1755
class FileStorage(BlockDev):
1756
  """File device.
1757

1758
  This class represents the a file storage backend device.
1759

1760
  The unique_id for the file device is a (file_driver, file_path) tuple.
1761

1762
  """
1763
  def __init__(self, unique_id, children, size):
1764
    """Initalizes a file device backend.
1765

1766
    """
1767
    if children:
1768
      raise errors.BlockDeviceError("Invalid setup for file device")
1769
    super(FileStorage, self).__init__(unique_id, children, size)
1770
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
1771
      raise ValueError("Invalid configuration data %s" % str(unique_id))
1772
    self.driver = unique_id[0]
1773
    self.dev_path = unique_id[1]
1774
    self.Attach()
1775

    
1776
  def Assemble(self):
1777
    """Assemble the device.
1778

1779
    Checks whether the file device exists, raises BlockDeviceError otherwise.
1780

1781
    """
1782
    if not os.path.exists(self.dev_path):
1783
      _ThrowError("File device '%s' does not exist" % self.dev_path)
1784

    
1785
  def Shutdown(self):
1786
    """Shutdown the device.
1787

1788
    This is a no-op for the file type, as we don't deactivate
1789
    the file on shutdown.
1790

1791
    """
1792
    pass
1793

    
1794
  def Open(self, force=False):
1795
    """Make the device ready for I/O.
1796

1797
    This is a no-op for the file type.
1798

1799
    """
1800
    pass
1801

    
1802
  def Close(self):
1803
    """Notifies that the device will no longer be used for I/O.
1804

1805
    This is a no-op for the file type.
1806

1807
    """
1808
    pass
1809

    
1810
  def Remove(self):
1811
    """Remove the file backing the block device.
1812

1813
    @rtype: boolean
1814
    @return: True if the removal was successful
1815

1816
    """
1817
    try:
1818
      os.remove(self.dev_path)
1819
    except OSError, err:
1820
      if err.errno != errno.ENOENT:
1821
        _ThrowError("Can't remove file '%s': %s", self.dev_path, err)
1822

    
1823
  def Attach(self):
1824
    """Attach to an existing file.
1825

1826
    Check if this file already exists.
1827

1828
    @rtype: boolean
1829
    @return: True if file exists
1830

1831
    """
1832
    self.attached = os.path.exists(self.dev_path)
1833
    return self.attached
1834

    
1835
  def GetActualSize(self):
1836
    """Return the actual disk size.
1837

1838
    @note: the device needs to be active when this is called
1839

1840
    """
1841
    assert self.attached, "BlockDevice not attached in GetActualSize()"
1842
    try:
1843
      st = os.stat(self.dev_path)
1844
      return st.st_size
1845
    except OSError, err:
1846
      _ThrowError("Can't stat %s: %s", self.dev_path, err)
1847

    
1848
  @classmethod
1849
  def Create(cls, unique_id, children, size):
1850
    """Create a new file.
1851

1852
    @param size: the size of file in MiB
1853

1854
    @rtype: L{bdev.FileStorage}
1855
    @return: an instance of FileStorage
1856

1857
    """
1858
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
1859
      raise ValueError("Invalid configuration data %s" % str(unique_id))
1860
    dev_path = unique_id[1]
1861
    if os.path.exists(dev_path):
1862
      _ThrowError("File already existing: %s", dev_path)
1863
    try:
1864
      f = open(dev_path, 'w')
1865
      f.truncate(size * 1024 * 1024)
1866
      f.close()
1867
    except IOError, err:
1868
      _ThrowError("Error in file creation: %", str(err))
1869

    
1870
    return FileStorage(unique_id, children, size)
1871

    
1872

    
1873
DEV_MAP = {
1874
  constants.LD_LV: LogicalVolume,
1875
  constants.LD_DRBD8: DRBD8,
1876
  constants.LD_FILE: FileStorage,
1877
  }
1878

    
1879

    
1880
def FindDevice(dev_type, unique_id, children, size):
1881
  """Search for an existing, assembled device.
1882

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

1886
  """
1887
  if dev_type not in DEV_MAP:
1888
    raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
1889
  device = DEV_MAP[dev_type](unique_id, children, size)
1890
  if not device.attached:
1891
    return None
1892
  return device
1893

    
1894

    
1895
def Assemble(dev_type, unique_id, children, size):
1896
  """Try to attach or assemble an existing device.
1897

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

1901
  """
1902
  if dev_type not in DEV_MAP:
1903
    raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
1904
  device = DEV_MAP[dev_type](unique_id, children, size)
1905
  device.Assemble()
1906
  return device
1907

    
1908

    
1909
def Create(dev_type, unique_id, children, size):
1910
  """Create a device.
1911

1912
  """
1913
  if dev_type not in DEV_MAP:
1914
    raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
1915
  device = DEV_MAP[dev_type].Create(unique_id, children, size)
1916
  return device