Statistics
| Branch: | Tag: | Revision:

root / lib / bdev.py @ 44caf5a8

History | View | Annotate | Download (63 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
  _VALID_NAME_RE = re.compile("^[a-zA-Z0-9+_.-]*$")
353
  _INVALID_NAMES = frozenset([".", "..", "snapshot", "pvmove"])
354
  _INVALID_SUBSTRINGS = frozenset(["_mlog", "_mimage"])
355

    
356
  def __init__(self, unique_id, children, size):
357
    """Attaches to a LV device.
358

359
    The unique_id is a tuple (vg_name, lv_name)
360

361
    """
362
    super(LogicalVolume, self).__init__(unique_id, children, size)
363
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
364
      raise ValueError("Invalid configuration data %s" % str(unique_id))
365
    self._vg_name, self._lv_name = unique_id
366
    self._ValidateName(self._vg_name)
367
    self._ValidateName(self._lv_name)
368
    self.dev_path = utils.PathJoin("/dev", self._vg_name, self._lv_name)
369
    self._degraded = True
370
    self.major = self.minor = self.pe_size = self.stripe_count = None
371
    self.Attach()
372

    
373
  @classmethod
374
  def Create(cls, unique_id, children, size):
375
    """Create a new logical volume.
376

377
    """
378
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
379
      raise errors.ProgrammerError("Invalid configuration data %s" %
380
                                   str(unique_id))
381
    vg_name, lv_name = unique_id
382
    cls._ValidateName(vg_name)
383
    cls._ValidateName(lv_name)
384
    pvs_info = cls.GetPVInfo([vg_name])
385
    if not pvs_info:
386
      _ThrowError("Can't compute PV info for vg %s", vg_name)
387
    pvs_info.sort()
388
    pvs_info.reverse()
389

    
390
    pvlist = [ pv[1] for pv in pvs_info ]
391
    if utils.any(pvlist, lambda v: ":" in v):
392
      _ThrowError("Some of your PVs have invalid character ':'"
393
                  " in their name")
394
    free_size = sum([ pv[0] for pv in pvs_info ])
395
    current_pvs = len(pvlist)
396
    stripes = min(current_pvs, constants.LVM_STRIPECOUNT)
397

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

    
417
  @staticmethod
418
  def GetPVInfo(vg_names, filter_allocatable=True):
419
    """Get the free space info for PVs in a volume group.
420

421
    @param vg_names: list of volume group names, if empty all will be returned
422
    @param filter_allocatable: whether to skip over unallocatable PVs
423

424
    @rtype: list
425
    @return: list of tuples (free_space, name) with free_space in mebibytes
426

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

    
451
    return data
452

    
453
  @classmethod
454
  def _ValidateName(cls, name):
455
    """Validates that a given name is valid as VG or LV name.
456

457
    The list of valid characters and restricted names is taken out of
458
    the lvm(8) manpage, with the simplification that we enforce both
459
    VG and LV restrictions on the names.
460

461
    """
462
    if (not cls._VALID_NAME_RE.match(name) or
463
        name in cls._INVALID_NAMES or
464
        utils.any(cls._INVALID_SUBSTRINGS, lambda x: x in name)):
465
      _ThrowError("Invalid LVM name '%s'", name)
466

    
467
  def Remove(self):
468
    """Remove this logical volume.
469

470
    """
471
    if not self.minor and not self.Attach():
472
      # the LV does not exist
473
      return
474
    result = utils.RunCmd(["lvremove", "-f", "%s/%s" %
475
                           (self._vg_name, self._lv_name)])
476
    if result.failed:
477
      _ThrowError("Can't lvremove: %s - %s", result.fail_reason, result.output)
478

    
479
  def Rename(self, new_id):
480
    """Rename this logical volume.
481

482
    """
483
    if not isinstance(new_id, (tuple, list)) or len(new_id) != 2:
484
      raise errors.ProgrammerError("Invalid new logical id '%s'" % new_id)
485
    new_vg, new_name = new_id
486
    if new_vg != self._vg_name:
487
      raise errors.ProgrammerError("Can't move a logical volume across"
488
                                   " volume groups (from %s to to %s)" %
489
                                   (self._vg_name, new_vg))
490
    result = utils.RunCmd(["lvrename", new_vg, self._lv_name, new_name])
491
    if result.failed:
492
      _ThrowError("Failed to rename the logical volume: %s", result.output)
493
    self._lv_name = new_name
494
    self.dev_path = utils.PathJoin("/dev", self._vg_name, self._lv_name)
495

    
496
  def Attach(self):
497
    """Attach to an existing LV.
498

499
    This method will try to see if an existing and active LV exists
500
    which matches our name. If so, its major/minor will be
501
    recorded.
502

503
    """
504
    self.attached = False
505
    result = utils.RunCmd(["lvs", "--noheadings", "--separator=,",
506
                           "--units=m", "--nosuffix",
507
                           "-olv_attr,lv_kernel_major,lv_kernel_minor,"
508
                           "vg_extent_size,stripes", self.dev_path])
509
    if result.failed:
510
      logging.error("Can't find LV %s: %s, %s",
511
                    self.dev_path, result.fail_reason, result.output)
512
      return False
513
    # the output can (and will) have multiple lines for multi-segment
514
    # LVs, as the 'stripes' parameter is a segment one, so we take
515
    # only the last entry, which is the one we're interested in; note
516
    # that with LVM2 anyway the 'stripes' value must be constant
517
    # across segments, so this is a no-op actually
518
    out = result.stdout.splitlines()
519
    if not out: # totally empty result? splitlines() returns at least
520
                # one line for any non-empty string
521
      logging.error("Can't parse LVS output, no lines? Got '%s'", str(out))
522
      return False
523
    out = out[-1].strip().rstrip(',')
524
    out = out.split(",")
525
    if len(out) != 5:
526
      logging.error("Can't parse LVS output, len(%s) != 5", str(out))
527
      return False
528

    
529
    status, major, minor, pe_size, stripes = out
530
    if len(status) != 6:
531
      logging.error("lvs lv_attr is not 6 characters (%s)", status)
532
      return False
533

    
534
    try:
535
      major = int(major)
536
      minor = int(minor)
537
    except (TypeError, ValueError), err:
538
      logging.error("lvs major/minor cannot be parsed: %s", str(err))
539

    
540
    try:
541
      pe_size = int(float(pe_size))
542
    except (TypeError, ValueError), err:
543
      logging.error("Can't parse vg extent size: %s", err)
544
      return False
545

    
546
    try:
547
      stripes = int(stripes)
548
    except (TypeError, ValueError), err:
549
      logging.error("Can't parse the number of stripes: %s", err)
550
      return False
551

    
552
    self.major = major
553
    self.minor = minor
554
    self.pe_size = pe_size
555
    self.stripe_count = stripes
556
    self._degraded = status[0] == 'v' # virtual volume, i.e. doesn't backing
557
                                      # storage
558
    self.attached = True
559
    return True
560

    
561
  def Assemble(self):
562
    """Assemble the device.
563

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

568
    """
569
    result = utils.RunCmd(["lvchange", "-ay", self.dev_path])
570
    if result.failed:
571
      _ThrowError("Can't activate lv %s: %s", self.dev_path, result.output)
572

    
573
  def Shutdown(self):
574
    """Shutdown the device.
575

576
    This is a no-op for the LV device type, as we don't deactivate the
577
    volumes on shutdown.
578

579
    """
580
    pass
581

    
582
  def GetSyncStatus(self):
583
    """Returns the sync status of the device.
584

585
    If this device is a mirroring device, this function returns the
586
    status of the mirror.
587

588
    For logical volumes, sync_percent and estimated_time are always
589
    None (no recovery in progress, as we don't handle the mirrored LV
590
    case). The is_degraded parameter is the inverse of the ldisk
591
    parameter.
592

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

599
    The status was already read in Attach, so we just return it.
600

601
    @rtype: objects.BlockDevStatus
602

603
    """
604
    if self._degraded:
605
      ldisk_status = constants.LDS_FAULTY
606
    else:
607
      ldisk_status = constants.LDS_OKAY
608

    
609
    return objects.BlockDevStatus(dev_path=self.dev_path,
610
                                  major=self.major,
611
                                  minor=self.minor,
612
                                  sync_percent=None,
613
                                  estimated_time=None,
614
                                  is_degraded=self._degraded,
615
                                  ldisk_status=ldisk_status)
616

    
617
  def Open(self, force=False):
618
    """Make the device ready for I/O.
619

620
    This is a no-op for the LV device type.
621

622
    """
623
    pass
624

    
625
  def Close(self):
626
    """Notifies that the device will no longer be used for I/O.
627

628
    This is a no-op for the LV device type.
629

630
    """
631
    pass
632

    
633
  def Snapshot(self, size):
634
    """Create a snapshot copy of an lvm block device.
635

636
    """
637
    snap_name = self._lv_name + ".snap"
638

    
639
    # remove existing snapshot if found
640
    snap = LogicalVolume((self._vg_name, snap_name), None, size)
641
    _IgnoreError(snap.Remove)
642

    
643
    pvs_info = self.GetPVInfo([self._vg_name])
644
    if not pvs_info:
645
      _ThrowError("Can't compute PV info for vg %s", self._vg_name)
646
    pvs_info.sort()
647
    pvs_info.reverse()
648
    free_size, _, _ = pvs_info[0]
649
    if free_size < size:
650
      _ThrowError("Not enough free space: required %s,"
651
                  " available %s", size, free_size)
652

    
653
    result = utils.RunCmd(["lvcreate", "-L%dm" % size, "-s",
654
                           "-n%s" % snap_name, self.dev_path])
655
    if result.failed:
656
      _ThrowError("command: %s error: %s - %s",
657
                  result.cmd, result.fail_reason, result.output)
658

    
659
    return snap_name
660

    
661
  def SetInfo(self, text):
662
    """Update metadata with info text.
663

664
    """
665
    BlockDev.SetInfo(self, text)
666

    
667
    # Replace invalid characters
668
    text = re.sub('^[^A-Za-z0-9_+.]', '_', text)
669
    text = re.sub('[^-A-Za-z0-9_+.]', '_', text)
670

    
671
    # Only up to 128 characters are allowed
672
    text = text[:128]
673

    
674
    result = utils.RunCmd(["lvchange", "--addtag", text,
675
                           self.dev_path])
676
    if result.failed:
677
      _ThrowError("Command: %s error: %s - %s", result.cmd, result.fail_reason,
678
                  result.output)
679

    
680
  def Grow(self, amount):
681
    """Grow the logical volume.
682

683
    """
684
    if self.pe_size is None or self.stripe_count is None:
685
      if not self.Attach():
686
        _ThrowError("Can't attach to LV during Grow()")
687
    full_stripe_size = self.pe_size * self.stripe_count
688
    rest = amount % full_stripe_size
689
    if rest != 0:
690
      amount += full_stripe_size - rest
691
    # we try multiple algorithms since the 'best' ones might not have
692
    # space available in the right place, but later ones might (since
693
    # they have less constraints); also note that only recent LVM
694
    # supports 'cling'
695
    for alloc_policy in "contiguous", "cling", "normal":
696
      result = utils.RunCmd(["lvextend", "--alloc", alloc_policy,
697
                             "-L", "+%dm" % amount, self.dev_path])
698
      if not result.failed:
699
        return
700
    _ThrowError("Can't grow LV %s: %s", self.dev_path, result.output)
701

    
702

    
703
class DRBD8Status(object):
704
  """A DRBD status representation class.
705

706
  Note that this doesn't support unconfigured devices (cs:Unconfigured).
707

708
  """
709
  UNCONF_RE = re.compile(r"\s*[0-9]+:\s*cs:Unconfigured$")
710
  LINE_RE = re.compile(r"\s*[0-9]+:\s*cs:(\S+)\s+(?:st|ro):([^/]+)/(\S+)"
711
                       "\s+ds:([^/]+)/(\S+)\s+.*$")
712
  SYNC_RE = re.compile(r"^.*\ssync'ed:\s*([0-9.]+)%.*"
713
                       "\sfinish: ([0-9]+):([0-9]+):([0-9]+)\s.*$")
714

    
715
  CS_UNCONFIGURED = "Unconfigured"
716
  CS_STANDALONE = "StandAlone"
717
  CS_WFCONNECTION = "WFConnection"
718
  CS_WFREPORTPARAMS = "WFReportParams"
719
  CS_CONNECTED = "Connected"
720
  CS_STARTINGSYNCS = "StartingSyncS"
721
  CS_STARTINGSYNCT = "StartingSyncT"
722
  CS_WFBITMAPS = "WFBitMapS"
723
  CS_WFBITMAPT = "WFBitMapT"
724
  CS_WFSYNCUUID = "WFSyncUUID"
725
  CS_SYNCSOURCE = "SyncSource"
726
  CS_SYNCTARGET = "SyncTarget"
727
  CS_PAUSEDSYNCS = "PausedSyncS"
728
  CS_PAUSEDSYNCT = "PausedSyncT"
729
  CSET_SYNC = frozenset([
730
    CS_WFREPORTPARAMS,
731
    CS_STARTINGSYNCS,
732
    CS_STARTINGSYNCT,
733
    CS_WFBITMAPS,
734
    CS_WFBITMAPT,
735
    CS_WFSYNCUUID,
736
    CS_SYNCSOURCE,
737
    CS_SYNCTARGET,
738
    CS_PAUSEDSYNCS,
739
    CS_PAUSEDSYNCT,
740
    ])
741

    
742
  DS_DISKLESS = "Diskless"
743
  DS_ATTACHING = "Attaching" # transient state
744
  DS_FAILED = "Failed" # transient state, next: diskless
745
  DS_NEGOTIATING = "Negotiating" # transient state
746
  DS_INCONSISTENT = "Inconsistent" # while syncing or after creation
747
  DS_OUTDATED = "Outdated"
748
  DS_DUNKNOWN = "DUnknown" # shown for peer disk when not connected
749
  DS_CONSISTENT = "Consistent"
750
  DS_UPTODATE = "UpToDate" # normal state
751

    
752
  RO_PRIMARY = "Primary"
753
  RO_SECONDARY = "Secondary"
754
  RO_UNKNOWN = "Unknown"
755

    
756
  def __init__(self, procline):
757
    u = self.UNCONF_RE.match(procline)
758
    if u:
759
      self.cstatus = self.CS_UNCONFIGURED
760
      self.lrole = self.rrole = self.ldisk = self.rdisk = None
761
    else:
762
      m = self.LINE_RE.match(procline)
763
      if not m:
764
        raise errors.BlockDeviceError("Can't parse input data '%s'" % procline)
765
      self.cstatus = m.group(1)
766
      self.lrole = m.group(2)
767
      self.rrole = m.group(3)
768
      self.ldisk = m.group(4)
769
      self.rdisk = m.group(5)
770

    
771
    # end reading of data from the LINE_RE or UNCONF_RE
772

    
773
    self.is_standalone = self.cstatus == self.CS_STANDALONE
774
    self.is_wfconn = self.cstatus == self.CS_WFCONNECTION
775
    self.is_connected = self.cstatus == self.CS_CONNECTED
776
    self.is_primary = self.lrole == self.RO_PRIMARY
777
    self.is_secondary = self.lrole == self.RO_SECONDARY
778
    self.peer_primary = self.rrole == self.RO_PRIMARY
779
    self.peer_secondary = self.rrole == self.RO_SECONDARY
780
    self.both_primary = self.is_primary and self.peer_primary
781
    self.both_secondary = self.is_secondary and self.peer_secondary
782

    
783
    self.is_diskless = self.ldisk == self.DS_DISKLESS
784
    self.is_disk_uptodate = self.ldisk == self.DS_UPTODATE
785

    
786
    self.is_in_resync = self.cstatus in self.CSET_SYNC
787
    self.is_in_use = self.cstatus != self.CS_UNCONFIGURED
788

    
789
    m = self.SYNC_RE.match(procline)
790
    if m:
791
      self.sync_percent = float(m.group(1))
792
      hours = int(m.group(2))
793
      minutes = int(m.group(3))
794
      seconds = int(m.group(4))
795
      self.est_time = hours * 3600 + minutes * 60 + seconds
796
    else:
797
      # we have (in this if branch) no percent information, but if
798
      # we're resyncing we need to 'fake' a sync percent information,
799
      # as this is how cmdlib determines if it makes sense to wait for
800
      # resyncing or not
801
      if self.is_in_resync:
802
        self.sync_percent = 0
803
      else:
804
        self.sync_percent = None
805
      self.est_time = None
806

    
807

    
808
class BaseDRBD(BlockDev): # pylint: disable-msg=W0223
809
  """Base DRBD class.
810

811
  This class contains a few bits of common functionality between the
812
  0.7 and 8.x versions of DRBD.
813

814
  """
815
  _VERSION_RE = re.compile(r"^version: (\d+)\.(\d+)\.(\d+)"
816
                           r" \(api:(\d+)/proto:(\d+)(?:-(\d+))?\)")
817
  _VALID_LINE_RE = re.compile("^ *([0-9]+): cs:([^ ]+).*$")
818
  _UNUSED_LINE_RE = re.compile("^ *([0-9]+): cs:Unconfigured$")
819

    
820
  _DRBD_MAJOR = 147
821
  _ST_UNCONFIGURED = "Unconfigured"
822
  _ST_WFCONNECTION = "WFConnection"
823
  _ST_CONNECTED = "Connected"
824

    
825
  _STATUS_FILE = "/proc/drbd"
826

    
827
  @staticmethod
828
  def _GetProcData(filename=_STATUS_FILE):
829
    """Return data from /proc/drbd.
830

831
    """
832
    try:
833
      data = utils.ReadFile(filename).splitlines()
834
    except EnvironmentError, err:
835
      if err.errno == errno.ENOENT:
836
        _ThrowError("The file %s cannot be opened, check if the module"
837
                    " is loaded (%s)", filename, str(err))
838
      else:
839
        _ThrowError("Can't read the DRBD proc file %s: %s", filename, str(err))
840
    if not data:
841
      _ThrowError("Can't read any data from %s", filename)
842
    return data
843

    
844
  @classmethod
845
  def _MassageProcData(cls, data):
846
    """Transform the output of _GetProdData into a nicer form.
847

848
    @return: a dictionary of minor: joined lines from /proc/drbd
849
        for that minor
850

851
    """
852
    results = {}
853
    old_minor = old_line = None
854
    for line in data:
855
      if not line: # completely empty lines, as can be returned by drbd8.0+
856
        continue
857
      lresult = cls._VALID_LINE_RE.match(line)
858
      if lresult is not None:
859
        if old_minor is not None:
860
          results[old_minor] = old_line
861
        old_minor = int(lresult.group(1))
862
        old_line = line
863
      else:
864
        if old_minor is not None:
865
          old_line += " " + line.strip()
866
    # add last line
867
    if old_minor is not None:
868
      results[old_minor] = old_line
869
    return results
870

    
871
  @classmethod
872
  def _GetVersion(cls):
873
    """Return the DRBD version.
874

875
    This will return a dict with keys:
876
      - k_major
877
      - k_minor
878
      - k_point
879
      - api
880
      - proto
881
      - proto2 (only on drbd > 8.2.X)
882

883
    """
884
    proc_data = cls._GetProcData()
885
    first_line = proc_data[0].strip()
886
    version = cls._VERSION_RE.match(first_line)
887
    if not version:
888
      raise errors.BlockDeviceError("Can't parse DRBD version from '%s'" %
889
                                    first_line)
890

    
891
    values = version.groups()
892
    retval = {'k_major': int(values[0]),
893
              'k_minor': int(values[1]),
894
              'k_point': int(values[2]),
895
              'api': int(values[3]),
896
              'proto': int(values[4]),
897
             }
898
    if values[5] is not None:
899
      retval['proto2'] = values[5]
900

    
901
    return retval
902

    
903
  @staticmethod
904
  def _DevPath(minor):
905
    """Return the path to a drbd device for a given minor.
906

907
    """
908
    return "/dev/drbd%d" % minor
909

    
910
  @classmethod
911
  def GetUsedDevs(cls):
912
    """Compute the list of used DRBD devices.
913

914
    """
915
    data = cls._GetProcData()
916

    
917
    used_devs = {}
918
    for line in data:
919
      match = cls._VALID_LINE_RE.match(line)
920
      if not match:
921
        continue
922
      minor = int(match.group(1))
923
      state = match.group(2)
924
      if state == cls._ST_UNCONFIGURED:
925
        continue
926
      used_devs[minor] = state, line
927

    
928
    return used_devs
929

    
930
  def _SetFromMinor(self, minor):
931
    """Set our parameters based on the given minor.
932

933
    This sets our minor variable and our dev_path.
934

935
    """
936
    if minor is None:
937
      self.minor = self.dev_path = None
938
      self.attached = False
939
    else:
940
      self.minor = minor
941
      self.dev_path = self._DevPath(minor)
942
      self.attached = True
943

    
944
  @staticmethod
945
  def _CheckMetaSize(meta_device):
946
    """Check if the given meta device looks like a valid one.
947

948
    This currently only check the size, which must be around
949
    128MiB.
950

951
    """
952
    result = utils.RunCmd(["blockdev", "--getsize", meta_device])
953
    if result.failed:
954
      _ThrowError("Failed to get device size: %s - %s",
955
                  result.fail_reason, result.output)
956
    try:
957
      sectors = int(result.stdout)
958
    except (TypeError, ValueError):
959
      _ThrowError("Invalid output from blockdev: '%s'", result.stdout)
960
    bytes = sectors * 512
961
    if bytes < 128 * 1024 * 1024: # less than 128MiB
962
      _ThrowError("Meta device too small (%.2fMib)", (bytes / 1024 / 1024))
963
    # the maximum *valid* size of the meta device when living on top
964
    # of LVM is hard to compute: it depends on the number of stripes
965
    # and the PE size; e.g. a 2-stripe, 64MB PE will result in a 128MB
966
    # (normal size), but an eight-stripe 128MB PE will result in a 1GB
967
    # size meta device; as such, we restrict it to 1GB (a little bit
968
    # too generous, but making assumptions about PE size is hard)
969
    if bytes > 1024 * 1024 * 1024:
970
      _ThrowError("Meta device too big (%.2fMiB)", (bytes / 1024 / 1024))
971

    
972
  def Rename(self, new_id):
973
    """Rename a device.
974

975
    This is not supported for drbd devices.
976

977
    """
978
    raise errors.ProgrammerError("Can't rename a drbd device")
979

    
980

    
981
class DRBD8(BaseDRBD):
982
  """DRBD v8.x block device.
983

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

988
  The unique_id for the drbd device is the (local_ip, local_port,
989
  remote_ip, remote_port) tuple, and it must have two children: the
990
  data device and the meta_device. The meta device is checked for
991
  valid size and is zeroed on create.
992

993
  """
994
  _MAX_MINORS = 255
995
  _PARSE_SHOW = None
996

    
997
  # timeout constants
998
  _NET_RECONFIG_TIMEOUT = 60
999

    
1000
  def __init__(self, unique_id, children, size):
1001
    if children and children.count(None) > 0:
1002
      children = []
1003
    if len(children) not in (0, 2):
1004
      raise ValueError("Invalid configuration data %s" % str(children))
1005
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 6:
1006
      raise ValueError("Invalid configuration data %s" % str(unique_id))
1007
    (self._lhost, self._lport,
1008
     self._rhost, self._rport,
1009
     self._aminor, self._secret) = unique_id
1010
    if children:
1011
      if not _CanReadDevice(children[1].dev_path):
1012
        logging.info("drbd%s: Ignoring unreadable meta device", self._aminor)
1013
        children = []
1014
    super(DRBD8, self).__init__(unique_id, children, size)
1015
    self.major = self._DRBD_MAJOR
1016
    version = self._GetVersion()
1017
    if version['k_major'] != 8 :
1018
      _ThrowError("Mismatch in DRBD kernel version and requested ganeti"
1019
                  " usage: kernel is %s.%s, ganeti wants 8.x",
1020
                  version['k_major'], version['k_minor'])
1021

    
1022
    if (self._lhost is not None and self._lhost == self._rhost and
1023
        self._lport == self._rport):
1024
      raise ValueError("Invalid configuration data, same local/remote %s" %
1025
                       (unique_id,))
1026
    self.Attach()
1027

    
1028
  @classmethod
1029
  def _InitMeta(cls, minor, dev_path):
1030
    """Initialize a meta device.
1031

1032
    This will not work if the given minor is in use.
1033

1034
    """
1035
    result = utils.RunCmd(["drbdmeta", "--force", cls._DevPath(minor),
1036
                           "v08", dev_path, "0", "create-md"])
1037
    if result.failed:
1038
      _ThrowError("Can't initialize meta device: %s", result.output)
1039

    
1040
  @classmethod
1041
  def _FindUnusedMinor(cls):
1042
    """Find an unused DRBD device.
1043

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

1047
    """
1048
    data = cls._GetProcData()
1049

    
1050
    highest = None
1051
    for line in data:
1052
      match = cls._UNUSED_LINE_RE.match(line)
1053
      if match:
1054
        return int(match.group(1))
1055
      match = cls._VALID_LINE_RE.match(line)
1056
      if match:
1057
        minor = int(match.group(1))
1058
        highest = max(highest, minor)
1059
    if highest is None: # there are no minors in use at all
1060
      return 0
1061
    if highest >= cls._MAX_MINORS:
1062
      logging.error("Error: no free drbd minors!")
1063
      raise errors.BlockDeviceError("Can't find a free DRBD minor")
1064
    return highest + 1
1065

    
1066
  @classmethod
1067
  def _GetShowParser(cls):
1068
    """Return a parser for `drbd show` output.
1069

1070
    This will either create or return an already-create parser for the
1071
    output of the command `drbd show`.
1072

1073
    """
1074
    if cls._PARSE_SHOW is not None:
1075
      return cls._PARSE_SHOW
1076

    
1077
    # pyparsing setup
1078
    lbrace = pyp.Literal("{").suppress()
1079
    rbrace = pyp.Literal("}").suppress()
1080
    semi = pyp.Literal(";").suppress()
1081
    # this also converts the value to an int
1082
    number = pyp.Word(pyp.nums).setParseAction(lambda s, l, t: int(t[0]))
1083

    
1084
    comment = pyp.Literal ("#") + pyp.Optional(pyp.restOfLine)
1085
    defa = pyp.Literal("_is_default").suppress()
1086
    dbl_quote = pyp.Literal('"').suppress()
1087

    
1088
    keyword = pyp.Word(pyp.alphanums + '-')
1089

    
1090
    # value types
1091
    value = pyp.Word(pyp.alphanums + '_-/.:')
1092
    quoted = dbl_quote + pyp.CharsNotIn('"') + dbl_quote
1093
    addr_type = (pyp.Optional(pyp.Literal("ipv4")).suppress() +
1094
                 pyp.Optional(pyp.Literal("ipv6")).suppress())
1095
    addr_port = (addr_type + pyp.Word(pyp.nums + '.') +
1096
                 pyp.Literal(':').suppress() + number)
1097
    # meta device, extended syntax
1098
    meta_value = ((value ^ quoted) + pyp.Literal('[').suppress() +
1099
                  number + pyp.Word(']').suppress())
1100
    # device name, extended syntax
1101
    device_value = pyp.Literal("minor").suppress() + number
1102

    
1103
    # a statement
1104
    stmt = (~rbrace + keyword + ~lbrace +
1105
            pyp.Optional(addr_port ^ value ^ quoted ^ meta_value ^
1106
                         device_value) +
1107
            pyp.Optional(defa) + semi +
1108
            pyp.Optional(pyp.restOfLine).suppress())
1109

    
1110
    # an entire section
1111
    section_name = pyp.Word(pyp.alphas + '_')
1112
    section = section_name + lbrace + pyp.ZeroOrMore(pyp.Group(stmt)) + rbrace
1113

    
1114
    bnf = pyp.ZeroOrMore(pyp.Group(section ^ stmt))
1115
    bnf.ignore(comment)
1116

    
1117
    cls._PARSE_SHOW = bnf
1118

    
1119
    return bnf
1120

    
1121
  @classmethod
1122
  def _GetShowData(cls, minor):
1123
    """Return the `drbdsetup show` data for a minor.
1124

1125
    """
1126
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "show"])
1127
    if result.failed:
1128
      logging.error("Can't display the drbd config: %s - %s",
1129
                    result.fail_reason, result.output)
1130
      return None
1131
    return result.stdout
1132

    
1133
  @classmethod
1134
  def _GetDevInfo(cls, out):
1135
    """Parse details about a given DRBD minor.
1136

1137
    This return, if available, the local backing device (as a path)
1138
    and the local and remote (ip, port) information from a string
1139
    containing the output of the `drbdsetup show` command as returned
1140
    by _GetShowData.
1141

1142
    """
1143
    data = {}
1144
    if not out:
1145
      return data
1146

    
1147
    bnf = cls._GetShowParser()
1148
    # run pyparse
1149

    
1150
    try:
1151
      results = bnf.parseString(out)
1152
    except pyp.ParseException, err:
1153
      _ThrowError("Can't parse drbdsetup show output: %s", str(err))
1154

    
1155
    # and massage the results into our desired format
1156
    for section in results:
1157
      sname = section[0]
1158
      if sname == "_this_host":
1159
        for lst in section[1:]:
1160
          if lst[0] == "disk":
1161
            data["local_dev"] = lst[1]
1162
          elif lst[0] == "meta-disk":
1163
            data["meta_dev"] = lst[1]
1164
            data["meta_index"] = lst[2]
1165
          elif lst[0] == "address":
1166
            data["local_addr"] = tuple(lst[1:])
1167
      elif sname == "_remote_host":
1168
        for lst in section[1:]:
1169
          if lst[0] == "address":
1170
            data["remote_addr"] = tuple(lst[1:])
1171
    return data
1172

    
1173
  def _MatchesLocal(self, info):
1174
    """Test if our local config matches with an existing device.
1175

1176
    The parameter should be as returned from `_GetDevInfo()`. This
1177
    method tests if our local backing device is the same as the one in
1178
    the info parameter, in effect testing if we look like the given
1179
    device.
1180

1181
    """
1182
    if self._children:
1183
      backend, meta = self._children
1184
    else:
1185
      backend = meta = None
1186

    
1187
    if backend is not None:
1188
      retval = ("local_dev" in info and info["local_dev"] == backend.dev_path)
1189
    else:
1190
      retval = ("local_dev" not in info)
1191

    
1192
    if meta is not None:
1193
      retval = retval and ("meta_dev" in info and
1194
                           info["meta_dev"] == meta.dev_path)
1195
      retval = retval and ("meta_index" in info and
1196
                           info["meta_index"] == 0)
1197
    else:
1198
      retval = retval and ("meta_dev" not in info and
1199
                           "meta_index" not in info)
1200
    return retval
1201

    
1202
  def _MatchesNet(self, info):
1203
    """Test if our network config matches with an existing device.
1204

1205
    The parameter should be as returned from `_GetDevInfo()`. This
1206
    method tests if our network configuration is the same as the one
1207
    in the info parameter, in effect testing if we look like the given
1208
    device.
1209

1210
    """
1211
    if (((self._lhost is None and not ("local_addr" in info)) and
1212
         (self._rhost is None and not ("remote_addr" in info)))):
1213
      return True
1214

    
1215
    if self._lhost is None:
1216
      return False
1217

    
1218
    if not ("local_addr" in info and
1219
            "remote_addr" in info):
1220
      return False
1221

    
1222
    retval = (info["local_addr"] == (self._lhost, self._lport))
1223
    retval = (retval and
1224
              info["remote_addr"] == (self._rhost, self._rport))
1225
    return retval
1226

    
1227
  @classmethod
1228
  def _AssembleLocal(cls, minor, backend, meta, size):
1229
    """Configure the local part of a DRBD device.
1230

1231
    """
1232
    args = ["drbdsetup", cls._DevPath(minor), "disk",
1233
            backend, meta, "0",
1234
            "-e", "detach",
1235
            "--create-device"]
1236
    if size:
1237
      args.extend(["-d", "%sm" % size])
1238
    if not constants.DRBD_BARRIERS: # disable barriers, if configured so
1239
      version = cls._GetVersion()
1240
      # various DRBD versions support different disk barrier options;
1241
      # what we aim here is to revert back to the 'drain' method of
1242
      # disk flushes and to disable metadata barriers, in effect going
1243
      # back to pre-8.0.7 behaviour
1244
      vmaj = version['k_major']
1245
      vmin = version['k_minor']
1246
      vrel = version['k_point']
1247
      assert vmaj == 8
1248
      if vmin == 0: # 8.0.x
1249
        if vrel >= 12:
1250
          args.extend(['-i', '-m'])
1251
      elif vmin == 2: # 8.2.x
1252
        if vrel >= 7:
1253
          args.extend(['-i', '-m'])
1254
      elif vmaj >= 3: # 8.3.x or newer
1255
        args.extend(['-i', '-a', 'm'])
1256
    result = utils.RunCmd(args)
1257
    if result.failed:
1258
      _ThrowError("drbd%d: can't attach local disk: %s", minor, result.output)
1259

    
1260
  @classmethod
1261
  def _AssembleNet(cls, minor, net_info, protocol,
1262
                   dual_pri=False, hmac=None, secret=None):
1263
    """Configure the network part of the device.
1264

1265
    """
1266
    lhost, lport, rhost, rport = net_info
1267
    if None in net_info:
1268
      # we don't want network connection and actually want to make
1269
      # sure its shutdown
1270
      cls._ShutdownNet(minor)
1271
      return
1272

    
1273
    # Workaround for a race condition. When DRBD is doing its dance to
1274
    # establish a connection with its peer, it also sends the
1275
    # synchronization speed over the wire. In some cases setting the
1276
    # sync speed only after setting up both sides can race with DRBD
1277
    # connecting, hence we set it here before telling DRBD anything
1278
    # about its peer.
1279
    cls._SetMinorSyncSpeed(minor, constants.SYNC_SPEED)
1280

    
1281
    args = ["drbdsetup", cls._DevPath(minor), "net",
1282
            "%s:%s" % (lhost, lport), "%s:%s" % (rhost, rport), protocol,
1283
            "-A", "discard-zero-changes",
1284
            "-B", "consensus",
1285
            "--create-device",
1286
            ]
1287
    if dual_pri:
1288
      args.append("-m")
1289
    if hmac and secret:
1290
      args.extend(["-a", hmac, "-x", secret])
1291
    result = utils.RunCmd(args)
1292
    if result.failed:
1293
      _ThrowError("drbd%d: can't setup network: %s - %s",
1294
                  minor, result.fail_reason, result.output)
1295

    
1296
    def _CheckNetworkConfig():
1297
      info = cls._GetDevInfo(cls._GetShowData(minor))
1298
      if not "local_addr" in info or not "remote_addr" in info:
1299
        raise utils.RetryAgain()
1300

    
1301
      if (info["local_addr"] != (lhost, lport) or
1302
          info["remote_addr"] != (rhost, rport)):
1303
        raise utils.RetryAgain()
1304

    
1305
    try:
1306
      utils.Retry(_CheckNetworkConfig, 1.0, 10.0)
1307
    except utils.RetryTimeout:
1308
      _ThrowError("drbd%d: timeout while configuring network", minor)
1309

    
1310
  def AddChildren(self, devices):
1311
    """Add a disk to the DRBD device.
1312

1313
    """
1314
    if self.minor is None:
1315
      _ThrowError("drbd%d: can't attach to dbrd8 during AddChildren",
1316
                  self._aminor)
1317
    if len(devices) != 2:
1318
      _ThrowError("drbd%d: need two devices for AddChildren", self.minor)
1319
    info = self._GetDevInfo(self._GetShowData(self.minor))
1320
    if "local_dev" in info:
1321
      _ThrowError("drbd%d: already attached to a local disk", self.minor)
1322
    backend, meta = devices
1323
    if backend.dev_path is None or meta.dev_path is None:
1324
      _ThrowError("drbd%d: children not ready during AddChildren", self.minor)
1325
    backend.Open()
1326
    meta.Open()
1327
    self._CheckMetaSize(meta.dev_path)
1328
    self._InitMeta(self._FindUnusedMinor(), meta.dev_path)
1329

    
1330
    self._AssembleLocal(self.minor, backend.dev_path, meta.dev_path, self.size)
1331
    self._children = devices
1332

    
1333
  def RemoveChildren(self, devices):
1334
    """Detach the drbd device from local storage.
1335

1336
    """
1337
    if self.minor is None:
1338
      _ThrowError("drbd%d: can't attach to drbd8 during RemoveChildren",
1339
                  self._aminor)
1340
    # early return if we don't actually have backing storage
1341
    info = self._GetDevInfo(self._GetShowData(self.minor))
1342
    if "local_dev" not in info:
1343
      return
1344
    if len(self._children) != 2:
1345
      _ThrowError("drbd%d: we don't have two children: %s", self.minor,
1346
                  self._children)
1347
    if self._children.count(None) == 2: # we don't actually have children :)
1348
      logging.warning("drbd%d: requested detach while detached", self.minor)
1349
      return
1350
    if len(devices) != 2:
1351
      _ThrowError("drbd%d: we need two children in RemoveChildren", self.minor)
1352
    for child, dev in zip(self._children, devices):
1353
      if dev != child.dev_path:
1354
        _ThrowError("drbd%d: mismatch in local storage (%s != %s) in"
1355
                    " RemoveChildren", self.minor, dev, child.dev_path)
1356

    
1357
    self._ShutdownLocal(self.minor)
1358
    self._children = []
1359

    
1360
  @classmethod
1361
  def _SetMinorSyncSpeed(cls, minor, kbytes):
1362
    """Set the speed of the DRBD syncer.
1363

1364
    This is the low-level implementation.
1365

1366
    @type minor: int
1367
    @param minor: the drbd minor whose settings we change
1368
    @type kbytes: int
1369
    @param kbytes: the speed in kbytes/second
1370
    @rtype: boolean
1371
    @return: the success of the operation
1372

1373
    """
1374
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "syncer",
1375
                           "-r", "%d" % kbytes, "--create-device"])
1376
    if result.failed:
1377
      logging.error("Can't change syncer rate: %s - %s",
1378
                    result.fail_reason, result.output)
1379
    return not result.failed
1380

    
1381
  def SetSyncSpeed(self, kbytes):
1382
    """Set the speed of the DRBD syncer.
1383

1384
    @type kbytes: int
1385
    @param kbytes: the speed in kbytes/second
1386
    @rtype: boolean
1387
    @return: the success of the operation
1388

1389
    """
1390
    if self.minor is None:
1391
      logging.info("Not attached during SetSyncSpeed")
1392
      return False
1393
    children_result = super(DRBD8, self).SetSyncSpeed(kbytes)
1394
    return self._SetMinorSyncSpeed(self.minor, kbytes) and children_result
1395

    
1396
  def GetProcStatus(self):
1397
    """Return device data from /proc.
1398

1399
    """
1400
    if self.minor is None:
1401
      _ThrowError("drbd%d: GetStats() called while not attached", self._aminor)
1402
    proc_info = self._MassageProcData(self._GetProcData())
1403
    if self.minor not in proc_info:
1404
      _ThrowError("drbd%d: can't find myself in /proc", self.minor)
1405
    return DRBD8Status(proc_info[self.minor])
1406

    
1407
  def GetSyncStatus(self):
1408
    """Returns the sync status of the device.
1409

1410

1411
    If sync_percent is None, it means all is ok
1412
    If estimated_time is None, it means we can't estimate
1413
    the time needed, otherwise it's the time left in seconds.
1414

1415

1416
    We set the is_degraded parameter to True on two conditions:
1417
    network not connected or local disk missing.
1418

1419
    We compute the ldisk parameter based on whether we have a local
1420
    disk or not.
1421

1422
    @rtype: objects.BlockDevStatus
1423

1424
    """
1425
    if self.minor is None and not self.Attach():
1426
      _ThrowError("drbd%d: can't Attach() in GetSyncStatus", self._aminor)
1427

    
1428
    stats = self.GetProcStatus()
1429
    is_degraded = not stats.is_connected or not stats.is_disk_uptodate
1430

    
1431
    if stats.is_disk_uptodate:
1432
      ldisk_status = constants.LDS_OKAY
1433
    elif stats.is_diskless:
1434
      ldisk_status = constants.LDS_FAULTY
1435
    else:
1436
      ldisk_status = constants.LDS_UNKNOWN
1437

    
1438
    return objects.BlockDevStatus(dev_path=self.dev_path,
1439
                                  major=self.major,
1440
                                  minor=self.minor,
1441
                                  sync_percent=stats.sync_percent,
1442
                                  estimated_time=stats.est_time,
1443
                                  is_degraded=is_degraded,
1444
                                  ldisk_status=ldisk_status)
1445

    
1446
  def Open(self, force=False):
1447
    """Make the local state primary.
1448

1449
    If the 'force' parameter is given, the '-o' option is passed to
1450
    drbdsetup. Since this is a potentially dangerous operation, the
1451
    force flag should be only given after creation, when it actually
1452
    is mandatory.
1453

1454
    """
1455
    if self.minor is None and not self.Attach():
1456
      logging.error("DRBD cannot attach to a device during open")
1457
      return False
1458
    cmd = ["drbdsetup", self.dev_path, "primary"]
1459
    if force:
1460
      cmd.append("-o")
1461
    result = utils.RunCmd(cmd)
1462
    if result.failed:
1463
      _ThrowError("drbd%d: can't make drbd device primary: %s", self.minor,
1464
                  result.output)
1465

    
1466
  def Close(self):
1467
    """Make the local state secondary.
1468

1469
    This will, of course, fail if the device is in use.
1470

1471
    """
1472
    if self.minor is None and not self.Attach():
1473
      _ThrowError("drbd%d: can't Attach() in Close()", self._aminor)
1474
    result = utils.RunCmd(["drbdsetup", self.dev_path, "secondary"])
1475
    if result.failed:
1476
      _ThrowError("drbd%d: can't switch drbd device to secondary: %s",
1477
                  self.minor, result.output)
1478

    
1479
  def DisconnectNet(self):
1480
    """Removes network configuration.
1481

1482
    This method shutdowns the network side of the device.
1483

1484
    The method will wait up to a hardcoded timeout for the device to
1485
    go into standalone after the 'disconnect' command before
1486
    re-configuring it, as sometimes it takes a while for the
1487
    disconnect to actually propagate and thus we might issue a 'net'
1488
    command while the device is still connected. If the device will
1489
    still be attached to the network and we time out, we raise an
1490
    exception.
1491

1492
    """
1493
    if self.minor is None:
1494
      _ThrowError("drbd%d: disk not attached in re-attach net", self._aminor)
1495

    
1496
    if None in (self._lhost, self._lport, self._rhost, self._rport):
1497
      _ThrowError("drbd%d: DRBD disk missing network info in"
1498
                  " DisconnectNet()", self.minor)
1499

    
1500
    class _DisconnectStatus:
1501
      def __init__(self, ever_disconnected):
1502
        self.ever_disconnected = ever_disconnected
1503

    
1504
    dstatus = _DisconnectStatus(_IgnoreError(self._ShutdownNet, self.minor))
1505

    
1506
    def _WaitForDisconnect():
1507
      if self.GetProcStatus().is_standalone:
1508
        return
1509

    
1510
      # retry the disconnect, it seems possible that due to a well-time
1511
      # disconnect on the peer, my disconnect command might be ignored and
1512
      # forgotten
1513
      dstatus.ever_disconnected = \
1514
        _IgnoreError(self._ShutdownNet, self.minor) or dstatus.ever_disconnected
1515

    
1516
      raise utils.RetryAgain()
1517

    
1518
    # Keep start time
1519
    start_time = time.time()
1520

    
1521
    try:
1522
      # Start delay at 100 milliseconds and grow up to 2 seconds
1523
      utils.Retry(_WaitForDisconnect, (0.1, 1.5, 2.0),
1524
                  self._NET_RECONFIG_TIMEOUT)
1525
    except utils.RetryTimeout:
1526
      if dstatus.ever_disconnected:
1527
        msg = ("drbd%d: device did not react to the"
1528
               " 'disconnect' command in a timely manner")
1529
      else:
1530
        msg = "drbd%d: can't shutdown network, even after multiple retries"
1531

    
1532
      _ThrowError(msg, self.minor)
1533

    
1534
    reconfig_time = time.time() - start_time
1535
    if reconfig_time > (self._NET_RECONFIG_TIMEOUT * 0.25):
1536
      logging.info("drbd%d: DisconnectNet: detach took %.3f seconds",
1537
                   self.minor, reconfig_time)
1538

    
1539
  def AttachNet(self, multimaster):
1540
    """Reconnects the network.
1541

1542
    This method connects the network side of the device with a
1543
    specified multi-master flag. The device needs to be 'Standalone'
1544
    but have valid network configuration data.
1545

1546
    Args:
1547
      - multimaster: init the network in dual-primary mode
1548

1549
    """
1550
    if self.minor is None:
1551
      _ThrowError("drbd%d: device not attached in AttachNet", self._aminor)
1552

    
1553
    if None in (self._lhost, self._lport, self._rhost, self._rport):
1554
      _ThrowError("drbd%d: missing network info in AttachNet()", self.minor)
1555

    
1556
    status = self.GetProcStatus()
1557

    
1558
    if not status.is_standalone:
1559
      _ThrowError("drbd%d: device is not standalone in AttachNet", self.minor)
1560

    
1561
    self._AssembleNet(self.minor,
1562
                      (self._lhost, self._lport, self._rhost, self._rport),
1563
                      constants.DRBD_NET_PROTOCOL, dual_pri=multimaster,
1564
                      hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1565

    
1566
  def Attach(self):
1567
    """Check if our minor is configured.
1568

1569
    This doesn't do any device configurations - it only checks if the
1570
    minor is in a state different from Unconfigured.
1571

1572
    Note that this function will not change the state of the system in
1573
    any way (except in case of side-effects caused by reading from
1574
    /proc).
1575

1576
    """
1577
    used_devs = self.GetUsedDevs()
1578
    if self._aminor in used_devs:
1579
      minor = self._aminor
1580
    else:
1581
      minor = None
1582

    
1583
    self._SetFromMinor(minor)
1584
    return minor is not None
1585

    
1586
  def Assemble(self):
1587
    """Assemble the drbd.
1588

1589
    Method:
1590
      - if we have a configured device, we try to ensure that it matches
1591
        our config
1592
      - if not, we create it from zero
1593

1594
    """
1595
    super(DRBD8, self).Assemble()
1596

    
1597
    self.Attach()
1598
    if self.minor is None:
1599
      # local device completely unconfigured
1600
      self._FastAssemble()
1601
    else:
1602
      # we have to recheck the local and network status and try to fix
1603
      # the device
1604
      self._SlowAssemble()
1605

    
1606
  def _SlowAssemble(self):
1607
    """Assembles the DRBD device from a (partially) configured device.
1608

1609
    In case of partially attached (local device matches but no network
1610
    setup), we perform the network attach. If successful, we re-test
1611
    the attach if can return success.
1612

1613
    """
1614
    # TODO: Rewrite to not use a for loop just because there is 'break'
1615
    # pylint: disable-msg=W0631
1616
    net_data = (self._lhost, self._lport, self._rhost, self._rport)
1617
    for minor in (self._aminor,):
1618
      info = self._GetDevInfo(self._GetShowData(minor))
1619
      match_l = self._MatchesLocal(info)
1620
      match_r = self._MatchesNet(info)
1621

    
1622
      if match_l and match_r:
1623
        # everything matches
1624
        break
1625

    
1626
      if match_l and not match_r and "local_addr" not in info:
1627
        # disk matches, but not attached to network, attach and recheck
1628
        self._AssembleNet(minor, net_data, constants.DRBD_NET_PROTOCOL,
1629
                          hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1630
        if self._MatchesNet(self._GetDevInfo(self._GetShowData(minor))):
1631
          break
1632
        else:
1633
          _ThrowError("drbd%d: network attach successful, but 'drbdsetup"
1634
                      " show' disagrees", minor)
1635

    
1636
      if match_r and "local_dev" not in info:
1637
        # no local disk, but network attached and it matches
1638
        self._AssembleLocal(minor, self._children[0].dev_path,
1639
                            self._children[1].dev_path, self.size)
1640
        if self._MatchesNet(self._GetDevInfo(self._GetShowData(minor))):
1641
          break
1642
        else:
1643
          _ThrowError("drbd%d: disk attach successful, but 'drbdsetup"
1644
                      " show' disagrees", minor)
1645

    
1646
      # this case must be considered only if we actually have local
1647
      # storage, i.e. not in diskless mode, because all diskless
1648
      # devices are equal from the point of view of local
1649
      # configuration
1650
      if (match_l and "local_dev" in info and
1651
          not match_r and "local_addr" in info):
1652
        # strange case - the device network part points to somewhere
1653
        # else, even though its local storage is ours; as we own the
1654
        # drbd space, we try to disconnect from the remote peer and
1655
        # reconnect to our correct one
1656
        try:
1657
          self._ShutdownNet(minor)
1658
        except errors.BlockDeviceError, err:
1659
          _ThrowError("drbd%d: device has correct local storage, wrong"
1660
                      " remote peer and is unable to disconnect in order"
1661
                      " to attach to the correct peer: %s", minor, str(err))
1662
        # note: _AssembleNet also handles the case when we don't want
1663
        # local storage (i.e. one or more of the _[lr](host|port) is
1664
        # None)
1665
        self._AssembleNet(minor, net_data, constants.DRBD_NET_PROTOCOL,
1666
                          hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1667
        if self._MatchesNet(self._GetDevInfo(self._GetShowData(minor))):
1668
          break
1669
        else:
1670
          _ThrowError("drbd%d: network attach successful, but 'drbdsetup"
1671
                      " show' disagrees", minor)
1672

    
1673
    else:
1674
      minor = None
1675

    
1676
    self._SetFromMinor(minor)
1677
    if minor is None:
1678
      _ThrowError("drbd%d: cannot activate, unknown or unhandled reason",
1679
                  self._aminor)
1680

    
1681
  def _FastAssemble(self):
1682
    """Assemble the drbd device from zero.
1683

1684
    This is run when in Assemble we detect our minor is unused.
1685

1686
    """
1687
    minor = self._aminor
1688
    if self._children and self._children[0] and self._children[1]:
1689
      self._AssembleLocal(minor, self._children[0].dev_path,
1690
                          self._children[1].dev_path, self.size)
1691
    if self._lhost and self._lport and self._rhost and self._rport:
1692
      self._AssembleNet(minor,
1693
                        (self._lhost, self._lport, self._rhost, self._rport),
1694
                        constants.DRBD_NET_PROTOCOL,
1695
                        hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1696
    self._SetFromMinor(minor)
1697

    
1698
  @classmethod
1699
  def _ShutdownLocal(cls, minor):
1700
    """Detach from the local device.
1701

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

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

    
1710
  @classmethod
1711
  def _ShutdownNet(cls, minor):
1712
    """Disconnect from the remote peer.
1713

1714
    This fails if we don't have a local device.
1715

1716
    """
1717
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "disconnect"])
1718
    if result.failed:
1719
      _ThrowError("drbd%d: can't shutdown network: %s", minor, result.output)
1720

    
1721
  @classmethod
1722
  def _ShutdownAll(cls, minor):
1723
    """Deactivate the device.
1724

1725
    This will, of course, fail if the device is in use.
1726

1727
    """
1728
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "down"])
1729
    if result.failed:
1730
      _ThrowError("drbd%d: can't shutdown drbd device: %s",
1731
                  minor, result.output)
1732

    
1733
  def Shutdown(self):
1734
    """Shutdown the DRBD device.
1735

1736
    """
1737
    if self.minor is None and not self.Attach():
1738
      logging.info("drbd%d: not attached during Shutdown()", self._aminor)
1739
      return
1740
    minor = self.minor
1741
    self.minor = None
1742
    self.dev_path = None
1743
    self._ShutdownAll(minor)
1744

    
1745
  def Remove(self):
1746
    """Stub remove for DRBD devices.
1747

1748
    """
1749
    self.Shutdown()
1750

    
1751
  @classmethod
1752
  def Create(cls, unique_id, children, size):
1753
    """Create a new DRBD8 device.
1754

1755
    Since DRBD devices are not created per se, just assembled, this
1756
    function only initializes the metadata.
1757

1758
    """
1759
    if len(children) != 2:
1760
      raise errors.ProgrammerError("Invalid setup for the drbd device")
1761
    # check that the minor is unused
1762
    aminor = unique_id[4]
1763
    proc_info = cls._MassageProcData(cls._GetProcData())
1764
    if aminor in proc_info:
1765
      status = DRBD8Status(proc_info[aminor])
1766
      in_use = status.is_in_use
1767
    else:
1768
      in_use = False
1769
    if in_use:
1770
      _ThrowError("drbd%d: minor is already in use at Create() time", aminor)
1771
    meta = children[1]
1772
    meta.Assemble()
1773
    if not meta.Attach():
1774
      _ThrowError("drbd%d: can't attach to meta device '%s'",
1775
                  aminor, meta)
1776
    cls._CheckMetaSize(meta.dev_path)
1777
    cls._InitMeta(aminor, meta.dev_path)
1778
    return cls(unique_id, children, size)
1779

    
1780
  def Grow(self, amount):
1781
    """Resize the DRBD device and its backing storage.
1782

1783
    """
1784
    if self.minor is None:
1785
      _ThrowError("drbd%d: Grow called while not attached", self._aminor)
1786
    if len(self._children) != 2 or None in self._children:
1787
      _ThrowError("drbd%d: cannot grow diskless device", self.minor)
1788
    self._children[0].Grow(amount)
1789
    result = utils.RunCmd(["drbdsetup", self.dev_path, "resize", "-s",
1790
                           "%dm" % (self.size + amount)])
1791
    if result.failed:
1792
      _ThrowError("drbd%d: resize failed: %s", self.minor, result.output)
1793

    
1794

    
1795
class FileStorage(BlockDev):
1796
  """File device.
1797

1798
  This class represents the a file storage backend device.
1799

1800
  The unique_id for the file device is a (file_driver, file_path) tuple.
1801

1802
  """
1803
  def __init__(self, unique_id, children, size):
1804
    """Initalizes a file device backend.
1805

1806
    """
1807
    if children:
1808
      raise errors.BlockDeviceError("Invalid setup for file device")
1809
    super(FileStorage, self).__init__(unique_id, children, size)
1810
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
1811
      raise ValueError("Invalid configuration data %s" % str(unique_id))
1812
    self.driver = unique_id[0]
1813
    self.dev_path = unique_id[1]
1814
    self.Attach()
1815

    
1816
  def Assemble(self):
1817
    """Assemble the device.
1818

1819
    Checks whether the file device exists, raises BlockDeviceError otherwise.
1820

1821
    """
1822
    if not os.path.exists(self.dev_path):
1823
      _ThrowError("File device '%s' does not exist" % self.dev_path)
1824

    
1825
  def Shutdown(self):
1826
    """Shutdown the device.
1827

1828
    This is a no-op for the file type, as we don't deactivate
1829
    the file on shutdown.
1830

1831
    """
1832
    pass
1833

    
1834
  def Open(self, force=False):
1835
    """Make the device ready for I/O.
1836

1837
    This is a no-op for the file type.
1838

1839
    """
1840
    pass
1841

    
1842
  def Close(self):
1843
    """Notifies that the device will no longer be used for I/O.
1844

1845
    This is a no-op for the file type.
1846

1847
    """
1848
    pass
1849

    
1850
  def Remove(self):
1851
    """Remove the file backing the block device.
1852

1853
    @rtype: boolean
1854
    @return: True if the removal was successful
1855

1856
    """
1857
    try:
1858
      os.remove(self.dev_path)
1859
    except OSError, err:
1860
      if err.errno != errno.ENOENT:
1861
        _ThrowError("Can't remove file '%s': %s", self.dev_path, err)
1862

    
1863
  def Rename(self, new_id):
1864
    """Renames the file.
1865

1866
    """
1867
    # TODO: implement rename for file-based storage
1868
    _ThrowError("Rename is not supported for file-based storage")
1869

    
1870
  def Grow(self, amount):
1871
    """Grow the file
1872

1873
    @param amount: the amount (in mebibytes) to grow with
1874

1875
    """
1876
    # TODO: implement grow for file-based storage
1877
    _ThrowError("Grow not supported for file-based storage")
1878

    
1879
  def Attach(self):
1880
    """Attach to an existing file.
1881

1882
    Check if this file already exists.
1883

1884
    @rtype: boolean
1885
    @return: True if file exists
1886

1887
    """
1888
    self.attached = os.path.exists(self.dev_path)
1889
    return self.attached
1890

    
1891
  def GetActualSize(self):
1892
    """Return the actual disk size.
1893

1894
    @note: the device needs to be active when this is called
1895

1896
    """
1897
    assert self.attached, "BlockDevice not attached in GetActualSize()"
1898
    try:
1899
      st = os.stat(self.dev_path)
1900
      return st.st_size
1901
    except OSError, err:
1902
      _ThrowError("Can't stat %s: %s", self.dev_path, err)
1903

    
1904
  @classmethod
1905
  def Create(cls, unique_id, children, size):
1906
    """Create a new file.
1907

1908
    @param size: the size of file in MiB
1909

1910
    @rtype: L{bdev.FileStorage}
1911
    @return: an instance of FileStorage
1912

1913
    """
1914
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
1915
      raise ValueError("Invalid configuration data %s" % str(unique_id))
1916
    dev_path = unique_id[1]
1917
    if os.path.exists(dev_path):
1918
      _ThrowError("File already existing: %s", dev_path)
1919
    try:
1920
      f = open(dev_path, 'w')
1921
      f.truncate(size * 1024 * 1024)
1922
      f.close()
1923
    except IOError, err:
1924
      _ThrowError("Error in file creation: %", str(err))
1925

    
1926
    return FileStorage(unique_id, children, size)
1927

    
1928

    
1929
DEV_MAP = {
1930
  constants.LD_LV: LogicalVolume,
1931
  constants.LD_DRBD8: DRBD8,
1932
  }
1933

    
1934
if constants.ENABLE_FILE_STORAGE:
1935
  DEV_MAP[constants.LD_FILE] = FileStorage
1936

    
1937

    
1938
def FindDevice(dev_type, unique_id, children, size):
1939
  """Search for an existing, assembled device.
1940

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

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

    
1952

    
1953
def Assemble(dev_type, unique_id, children, size):
1954
  """Try to attach or assemble an existing device.
1955

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

1959
  """
1960
  if dev_type not in DEV_MAP:
1961
    raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
1962
  device = DEV_MAP[dev_type](unique_id, children, size)
1963
  device.Assemble()
1964
  return device
1965

    
1966

    
1967
def Create(dev_type, unique_id, children, size):
1968
  """Create a device.
1969

1970
  """
1971
  if dev_type not in DEV_MAP:
1972
    raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
1973
  device = DEV_MAP[dev_type].Create(unique_id, children, size)
1974
  return device