Statistics
| Branch: | Tag: | Revision:

root / lib / bdev.py @ 5a672c30

History | View | Annotate | Download (64.2 kB)

1
#
2
#
3

    
4
# Copyright (C) 2006, 2007 Google Inc.
5
#
6
# This program is free software; you can redistribute it and/or modify
7
# it under the terms of the GNU General Public License as published by
8
# the Free Software Foundation; either version 2 of the License, or
9
# (at your option) any later version.
10
#
11
# This program is distributed in the hope that it will be useful, but
12
# WITHOUT ANY WARRANTY; without even the implied warranty of
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14
# General Public License for more details.
15
#
16
# You should have received a copy of the GNU General Public License
17
# along with this program; if not, write to the Free Software
18
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
19
# 02110-1301, USA.
20

    
21

    
22
"""Block device abstraction"""
23

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

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

    
37

    
38
# Size of reads in _CanReadDevice
39
_DEVICE_READ_SIZE = 128 * 1024
40

    
41

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

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

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

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

    
59

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

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

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

    
73

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

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

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

    
87

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

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

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

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

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

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

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

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

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

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

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

151
    """
152
    pass
153

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

157
    """
158
    raise NotImplementedError
159

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

163
    """
164
    raise NotImplementedError
165

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

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

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

177
    """
178
    raise NotImplementedError
179

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

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

187
    """
188
    raise NotImplementedError
189

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

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

195
    """
196
    raise NotImplementedError
197

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

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

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

207
    """
208
    raise NotImplementedError
209

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

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

217
    """
218
    raise NotImplementedError
219

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

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

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

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

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

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

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

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

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

251
    @rtype: objects.BlockDevStatus
252

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

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

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

269
    @rtype: objects.BlockDevStatus
270

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

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

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

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

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

    
293
        is_degraded = is_degraded or child_status.is_degraded
294

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

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

    
308

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

312
    Only supported for some device types.
313

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

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

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

323
    """
324
    raise NotImplementedError
325

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

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

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

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

    
348

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

352
  """
353
  _VALID_NAME_RE = re.compile("^[a-zA-Z0-9+_.-]*$")
354
  _INVALID_NAMES = frozenset([".", "..", "snapshot", "pvmove"])
355
  _INVALID_SUBSTRINGS = frozenset(["_mlog", "_mimage"])
356

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

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

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

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

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

    
391
    pvlist = [ pv[1] for pv in pvs_info ]
392
    if compat.any(":" in v for v in pvlist):
393
      _ThrowError("Some of your PVs have the invalid character ':' in their"
394
                  " name, this is not supported - please filter them out"
395
                  " in lvm.conf using either 'filter' or 'preferred_names'")
396
    free_size = sum([ pv[0] for pv in pvs_info ])
397
    current_pvs = len(pvlist)
398
    stripes = min(current_pvs, constants.LVM_STRIPECOUNT)
399

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

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

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

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

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

    
453
    return data
454

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

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

463
    """
464
    if (not cls._VALID_NAME_RE.match(name) or
465
        name in cls._INVALID_NAMES or
466
        compat.any(substring in name for substring in cls._INVALID_SUBSTRINGS)):
467
      _ThrowError("Invalid LVM name '%s'", name)
468

    
469
  def Remove(self):
470
    """Remove this logical volume.
471

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

    
481
  def Rename(self, new_id):
482
    """Rename this logical volume.
483

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

    
498
  def Attach(self):
499
    """Attach to an existing LV.
500

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

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

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

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

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

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

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

    
563
  def Assemble(self):
564
    """Assemble the device.
565

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

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

    
575
  def Shutdown(self):
576
    """Shutdown the device.
577

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

581
    """
582
    pass
583

    
584
  def GetSyncStatus(self):
585
    """Returns the sync status of the device.
586

587
    If this device is a mirroring device, this function returns the
588
    status of the mirror.
589

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

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

601
    The status was already read in Attach, so we just return it.
602

603
    @rtype: objects.BlockDevStatus
604

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

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

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

622
    This is a no-op for the LV device type.
623

624
    """
625
    pass
626

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

630
    This is a no-op for the LV device type.
631

632
    """
633
    pass
634

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

638
    """
639
    snap_name = self._lv_name + ".snap"
640

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

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

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

    
661
    return snap_name
662

    
663
  def SetInfo(self, text):
664
    """Update metadata with info text.
665

666
    """
667
    BlockDev.SetInfo(self, text)
668

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

    
673
    # Only up to 128 characters are allowed
674
    text = text[:128]
675

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

    
682
  def Grow(self, amount):
683
    """Grow the logical volume.
684

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

    
704

    
705
class DRBD8Status(object):
706
  """A DRBD status representation class.
707

708
  Note that this doesn't support unconfigured devices (cs:Unconfigured).
709

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

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

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

    
754
  RO_PRIMARY = "Primary"
755
  RO_SECONDARY = "Secondary"
756
  RO_UNKNOWN = "Unknown"
757

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

    
773
    # end reading of data from the LINE_RE or UNCONF_RE
774

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

    
785
    self.is_diskless = self.ldisk == self.DS_DISKLESS
786
    self.is_disk_uptodate = self.ldisk == self.DS_UPTODATE
787

    
788
    self.is_in_resync = self.cstatus in self.CSET_SYNC
789
    self.is_in_use = self.cstatus != self.CS_UNCONFIGURED
790

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

    
809

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

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

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

    
822
  _DRBD_MAJOR = 147
823
  _ST_UNCONFIGURED = "Unconfigured"
824
  _ST_WFCONNECTION = "WFConnection"
825
  _ST_CONNECTED = "Connected"
826

    
827
  _STATUS_FILE = "/proc/drbd"
828

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

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

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

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

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

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

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

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

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

    
903
    return retval
904

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

909
    """
910
    return "/dev/drbd%d" % minor
911

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

916
    """
917
    data = cls._GetProcData()
918

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

    
930
    return used_devs
931

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

935
    This sets our minor variable and our dev_path.
936

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

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

950
    This currently only check the size, which must be around
951
    128MiB.
952

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

    
974
  def Rename(self, new_id):
975
    """Rename a device.
976

977
    This is not supported for drbd devices.
978

979
    """
980
    raise errors.ProgrammerError("Can't rename a drbd device")
981

    
982

    
983
class DRBD8(BaseDRBD):
984
  """DRBD v8.x block device.
985

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

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

995
  """
996
  _MAX_MINORS = 255
997
  _PARSE_SHOW = None
998

    
999
  # timeout constants
1000
  _NET_RECONFIG_TIMEOUT = 60
1001

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

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

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

1034
    This will not work if the given minor is in use.
1035

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

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

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

1049
    """
1050
    data = cls._GetProcData()
1051

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

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

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

1075
    """
1076
    if cls._PARSE_SHOW is not None:
1077
      return cls._PARSE_SHOW
1078

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

    
1089
    comment = pyp.Literal ("#") + pyp.Optional(pyp.restOfLine)
1090
    defa = pyp.Literal("_is_default").suppress()
1091
    dbl_quote = pyp.Literal('"').suppress()
1092

    
1093
    keyword = pyp.Word(pyp.alphanums + '-')
1094

    
1095
    # value types
1096
    value = pyp.Word(pyp.alphanums + '_-/.:')
1097
    quoted = dbl_quote + pyp.CharsNotIn('"') + dbl_quote
1098
    ipv4_addr = (pyp.Optional(pyp.Literal("ipv4")).suppress() +
1099
                 pyp.Word(pyp.nums + ".") + colon + number)
1100
    ipv6_addr = (pyp.Optional(pyp.Literal("ipv6")).suppress() +
1101
                 pyp.Optional(lbracket) + pyp.Word(pyp.hexnums + ":") +
1102
                 pyp.Optional(rbracket) + colon + number)
1103
    # meta device, extended syntax
1104
    meta_value = ((value ^ quoted) + lbracket + number + rbracket)
1105
    # device name, extended syntax
1106
    device_value = pyp.Literal("minor").suppress() + number
1107

    
1108
    # a statement
1109
    stmt = (~rbrace + keyword + ~lbrace +
1110
            pyp.Optional(ipv4_addr ^ ipv6_addr ^ value ^ quoted ^ meta_value ^
1111
                         device_value) +
1112
            pyp.Optional(defa) + semi +
1113
            pyp.Optional(pyp.restOfLine).suppress())
1114

    
1115
    # an entire section
1116
    section_name = pyp.Word(pyp.alphas + '_')
1117
    section = section_name + lbrace + pyp.ZeroOrMore(pyp.Group(stmt)) + rbrace
1118

    
1119
    bnf = pyp.ZeroOrMore(pyp.Group(section ^ stmt))
1120
    bnf.ignore(comment)
1121

    
1122
    cls._PARSE_SHOW = bnf
1123

    
1124
    return bnf
1125

    
1126
  @classmethod
1127
  def _GetShowData(cls, minor):
1128
    """Return the `drbdsetup show` data for a minor.
1129

1130
    """
1131
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "show"])
1132
    if result.failed:
1133
      logging.error("Can't display the drbd config: %s - %s",
1134
                    result.fail_reason, result.output)
1135
      return None
1136
    return result.stdout
1137

    
1138
  @classmethod
1139
  def _GetDevInfo(cls, out):
1140
    """Parse details about a given DRBD minor.
1141

1142
    This return, if available, the local backing device (as a path)
1143
    and the local and remote (ip, port) information from a string
1144
    containing the output of the `drbdsetup show` command as returned
1145
    by _GetShowData.
1146

1147
    """
1148
    data = {}
1149
    if not out:
1150
      return data
1151

    
1152
    bnf = cls._GetShowParser()
1153
    # run pyparse
1154

    
1155
    try:
1156
      results = bnf.parseString(out)
1157
    except pyp.ParseException, err:
1158
      _ThrowError("Can't parse drbdsetup show output: %s", str(err))
1159

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

    
1178
  def _MatchesLocal(self, info):
1179
    """Test if our local config matches with an existing device.
1180

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

1186
    """
1187
    if self._children:
1188
      backend, meta = self._children
1189
    else:
1190
      backend = meta = None
1191

    
1192
    if backend is not None:
1193
      retval = ("local_dev" in info and info["local_dev"] == backend.dev_path)
1194
    else:
1195
      retval = ("local_dev" not in info)
1196

    
1197
    if meta is not None:
1198
      retval = retval and ("meta_dev" in info and
1199
                           info["meta_dev"] == meta.dev_path)
1200
      retval = retval and ("meta_index" in info and
1201
                           info["meta_index"] == 0)
1202
    else:
1203
      retval = retval and ("meta_dev" not in info and
1204
                           "meta_index" not in info)
1205
    return retval
1206

    
1207
  def _MatchesNet(self, info):
1208
    """Test if our network config matches with an existing device.
1209

1210
    The parameter should be as returned from `_GetDevInfo()`. This
1211
    method tests if our network configuration is the same as the one
1212
    in the info parameter, in effect testing if we look like the given
1213
    device.
1214

1215
    """
1216
    if (((self._lhost is None and not ("local_addr" in info)) and
1217
         (self._rhost is None and not ("remote_addr" in info)))):
1218
      return True
1219

    
1220
    if self._lhost is None:
1221
      return False
1222

    
1223
    if not ("local_addr" in info and
1224
            "remote_addr" in info):
1225
      return False
1226

    
1227
    retval = (info["local_addr"] == (self._lhost, self._lport))
1228
    retval = (retval and
1229
              info["remote_addr"] == (self._rhost, self._rport))
1230
    return retval
1231

    
1232
  @classmethod
1233
  def _AssembleLocal(cls, minor, backend, meta, size):
1234
    """Configure the local part of a DRBD device.
1235

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

    
1265
  @classmethod
1266
  def _AssembleNet(cls, minor, net_info, protocol,
1267
                   dual_pri=False, hmac=None, secret=None):
1268
    """Configure the network part of the device.
1269

1270
    """
1271
    lhost, lport, rhost, rport = net_info
1272
    if None in net_info:
1273
      # we don't want network connection and actually want to make
1274
      # sure its shutdown
1275
      cls._ShutdownNet(minor)
1276
      return
1277

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

    
1286
    if utils.IsValidIP6(lhost):
1287
      if not utils.IsValidIP6(rhost):
1288
        _ThrowError("drbd%d: can't connect ip %s to ip %s" %
1289
                    (minor, lhost, rhost))
1290
      family = "ipv6"
1291
    elif utils.IsValidIP4(lhost):
1292
      if not utils.IsValidIP4(rhost):
1293
        _ThrowError("drbd%d: can't connect ip %s to ip %s" %
1294
                    (minor, lhost, rhost))
1295
      family = "ipv4"
1296
    else:
1297
      _ThrowError("drbd%d: Invalid ip %s" % (minor, lhost))
1298

    
1299
    args = ["drbdsetup", cls._DevPath(minor), "net",
1300
            "%s:%s:%s" % (family, lhost, lport),
1301
            "%s:%s:%s" % (family, rhost, rport), protocol,
1302
            "-A", "discard-zero-changes",
1303
            "-B", "consensus",
1304
            "--create-device",
1305
            ]
1306
    if dual_pri:
1307
      args.append("-m")
1308
    if hmac and secret:
1309
      args.extend(["-a", hmac, "-x", secret])
1310
    result = utils.RunCmd(args)
1311
    if result.failed:
1312
      _ThrowError("drbd%d: can't setup network: %s - %s",
1313
                  minor, result.fail_reason, result.output)
1314

    
1315
    def _CheckNetworkConfig():
1316
      info = cls._GetDevInfo(cls._GetShowData(minor))
1317
      if not "local_addr" in info or not "remote_addr" in info:
1318
        raise utils.RetryAgain()
1319

    
1320
      if (info["local_addr"] != (lhost, lport) or
1321
          info["remote_addr"] != (rhost, rport)):
1322
        raise utils.RetryAgain()
1323

    
1324
    try:
1325
      utils.Retry(_CheckNetworkConfig, 1.0, 10.0)
1326
    except utils.RetryTimeout:
1327
      _ThrowError("drbd%d: timeout while configuring network", minor)
1328

    
1329
  def AddChildren(self, devices):
1330
    """Add a disk to the DRBD device.
1331

1332
    """
1333
    if self.minor is None:
1334
      _ThrowError("drbd%d: can't attach to dbrd8 during AddChildren",
1335
                  self._aminor)
1336
    if len(devices) != 2:
1337
      _ThrowError("drbd%d: need two devices for AddChildren", self.minor)
1338
    info = self._GetDevInfo(self._GetShowData(self.minor))
1339
    if "local_dev" in info:
1340
      _ThrowError("drbd%d: already attached to a local disk", self.minor)
1341
    backend, meta = devices
1342
    if backend.dev_path is None or meta.dev_path is None:
1343
      _ThrowError("drbd%d: children not ready during AddChildren", self.minor)
1344
    backend.Open()
1345
    meta.Open()
1346
    self._CheckMetaSize(meta.dev_path)
1347
    self._InitMeta(self._FindUnusedMinor(), meta.dev_path)
1348

    
1349
    self._AssembleLocal(self.minor, backend.dev_path, meta.dev_path, self.size)
1350
    self._children = devices
1351

    
1352
  def RemoveChildren(self, devices):
1353
    """Detach the drbd device from local storage.
1354

1355
    """
1356
    if self.minor is None:
1357
      _ThrowError("drbd%d: can't attach to drbd8 during RemoveChildren",
1358
                  self._aminor)
1359
    # early return if we don't actually have backing storage
1360
    info = self._GetDevInfo(self._GetShowData(self.minor))
1361
    if "local_dev" not in info:
1362
      return
1363
    if len(self._children) != 2:
1364
      _ThrowError("drbd%d: we don't have two children: %s", self.minor,
1365
                  self._children)
1366
    if self._children.count(None) == 2: # we don't actually have children :)
1367
      logging.warning("drbd%d: requested detach while detached", self.minor)
1368
      return
1369
    if len(devices) != 2:
1370
      _ThrowError("drbd%d: we need two children in RemoveChildren", self.minor)
1371
    for child, dev in zip(self._children, devices):
1372
      if dev != child.dev_path:
1373
        _ThrowError("drbd%d: mismatch in local storage (%s != %s) in"
1374
                    " RemoveChildren", self.minor, dev, child.dev_path)
1375

    
1376
    self._ShutdownLocal(self.minor)
1377
    self._children = []
1378

    
1379
  @classmethod
1380
  def _SetMinorSyncSpeed(cls, minor, kbytes):
1381
    """Set the speed of the DRBD syncer.
1382

1383
    This is the low-level implementation.
1384

1385
    @type minor: int
1386
    @param minor: the drbd minor whose settings we change
1387
    @type kbytes: int
1388
    @param kbytes: the speed in kbytes/second
1389
    @rtype: boolean
1390
    @return: the success of the operation
1391

1392
    """
1393
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "syncer",
1394
                           "-r", "%d" % kbytes, "--create-device"])
1395
    if result.failed:
1396
      logging.error("Can't change syncer rate: %s - %s",
1397
                    result.fail_reason, result.output)
1398
    return not result.failed
1399

    
1400
  def SetSyncSpeed(self, kbytes):
1401
    """Set the speed of the DRBD syncer.
1402

1403
    @type kbytes: int
1404
    @param kbytes: the speed in kbytes/second
1405
    @rtype: boolean
1406
    @return: the success of the operation
1407

1408
    """
1409
    if self.minor is None:
1410
      logging.info("Not attached during SetSyncSpeed")
1411
      return False
1412
    children_result = super(DRBD8, self).SetSyncSpeed(kbytes)
1413
    return self._SetMinorSyncSpeed(self.minor, kbytes) and children_result
1414

    
1415
  def GetProcStatus(self):
1416
    """Return device data from /proc.
1417

1418
    """
1419
    if self.minor is None:
1420
      _ThrowError("drbd%d: GetStats() called while not attached", self._aminor)
1421
    proc_info = self._MassageProcData(self._GetProcData())
1422
    if self.minor not in proc_info:
1423
      _ThrowError("drbd%d: can't find myself in /proc", self.minor)
1424
    return DRBD8Status(proc_info[self.minor])
1425

    
1426
  def GetSyncStatus(self):
1427
    """Returns the sync status of the device.
1428

1429

1430
    If sync_percent is None, it means all is ok
1431
    If estimated_time is None, it means we can't estimate
1432
    the time needed, otherwise it's the time left in seconds.
1433

1434

1435
    We set the is_degraded parameter to True on two conditions:
1436
    network not connected or local disk missing.
1437

1438
    We compute the ldisk parameter based on whether we have a local
1439
    disk or not.
1440

1441
    @rtype: objects.BlockDevStatus
1442

1443
    """
1444
    if self.minor is None and not self.Attach():
1445
      _ThrowError("drbd%d: can't Attach() in GetSyncStatus", self._aminor)
1446

    
1447
    stats = self.GetProcStatus()
1448
    is_degraded = not stats.is_connected or not stats.is_disk_uptodate
1449

    
1450
    if stats.is_disk_uptodate:
1451
      ldisk_status = constants.LDS_OKAY
1452
    elif stats.is_diskless:
1453
      ldisk_status = constants.LDS_FAULTY
1454
    else:
1455
      ldisk_status = constants.LDS_UNKNOWN
1456

    
1457
    return objects.BlockDevStatus(dev_path=self.dev_path,
1458
                                  major=self.major,
1459
                                  minor=self.minor,
1460
                                  sync_percent=stats.sync_percent,
1461
                                  estimated_time=stats.est_time,
1462
                                  is_degraded=is_degraded,
1463
                                  ldisk_status=ldisk_status)
1464

    
1465
  def Open(self, force=False):
1466
    """Make the local state primary.
1467

1468
    If the 'force' parameter is given, the '-o' option is passed to
1469
    drbdsetup. Since this is a potentially dangerous operation, the
1470
    force flag should be only given after creation, when it actually
1471
    is mandatory.
1472

1473
    """
1474
    if self.minor is None and not self.Attach():
1475
      logging.error("DRBD cannot attach to a device during open")
1476
      return False
1477
    cmd = ["drbdsetup", self.dev_path, "primary"]
1478
    if force:
1479
      cmd.append("-o")
1480
    result = utils.RunCmd(cmd)
1481
    if result.failed:
1482
      _ThrowError("drbd%d: can't make drbd device primary: %s", self.minor,
1483
                  result.output)
1484

    
1485
  def Close(self):
1486
    """Make the local state secondary.
1487

1488
    This will, of course, fail if the device is in use.
1489

1490
    """
1491
    if self.minor is None and not self.Attach():
1492
      _ThrowError("drbd%d: can't Attach() in Close()", self._aminor)
1493
    result = utils.RunCmd(["drbdsetup", self.dev_path, "secondary"])
1494
    if result.failed:
1495
      _ThrowError("drbd%d: can't switch drbd device to secondary: %s",
1496
                  self.minor, result.output)
1497

    
1498
  def DisconnectNet(self):
1499
    """Removes network configuration.
1500

1501
    This method shutdowns the network side of the device.
1502

1503
    The method will wait up to a hardcoded timeout for the device to
1504
    go into standalone after the 'disconnect' command before
1505
    re-configuring it, as sometimes it takes a while for the
1506
    disconnect to actually propagate and thus we might issue a 'net'
1507
    command while the device is still connected. If the device will
1508
    still be attached to the network and we time out, we raise an
1509
    exception.
1510

1511
    """
1512
    if self.minor is None:
1513
      _ThrowError("drbd%d: disk not attached in re-attach net", self._aminor)
1514

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

    
1519
    class _DisconnectStatus:
1520
      def __init__(self, ever_disconnected):
1521
        self.ever_disconnected = ever_disconnected
1522

    
1523
    dstatus = _DisconnectStatus(_IgnoreError(self._ShutdownNet, self.minor))
1524

    
1525
    def _WaitForDisconnect():
1526
      if self.GetProcStatus().is_standalone:
1527
        return
1528

    
1529
      # retry the disconnect, it seems possible that due to a well-time
1530
      # disconnect on the peer, my disconnect command might be ignored and
1531
      # forgotten
1532
      dstatus.ever_disconnected = \
1533
        _IgnoreError(self._ShutdownNet, self.minor) or dstatus.ever_disconnected
1534

    
1535
      raise utils.RetryAgain()
1536

    
1537
    # Keep start time
1538
    start_time = time.time()
1539

    
1540
    try:
1541
      # Start delay at 100 milliseconds and grow up to 2 seconds
1542
      utils.Retry(_WaitForDisconnect, (0.1, 1.5, 2.0),
1543
                  self._NET_RECONFIG_TIMEOUT)
1544
    except utils.RetryTimeout:
1545
      if dstatus.ever_disconnected:
1546
        msg = ("drbd%d: device did not react to the"
1547
               " 'disconnect' command in a timely manner")
1548
      else:
1549
        msg = "drbd%d: can't shutdown network, even after multiple retries"
1550

    
1551
      _ThrowError(msg, self.minor)
1552

    
1553
    reconfig_time = time.time() - start_time
1554
    if reconfig_time > (self._NET_RECONFIG_TIMEOUT * 0.25):
1555
      logging.info("drbd%d: DisconnectNet: detach took %.3f seconds",
1556
                   self.minor, reconfig_time)
1557

    
1558
  def AttachNet(self, multimaster):
1559
    """Reconnects the network.
1560

1561
    This method connects the network side of the device with a
1562
    specified multi-master flag. The device needs to be 'Standalone'
1563
    but have valid network configuration data.
1564

1565
    Args:
1566
      - multimaster: init the network in dual-primary mode
1567

1568
    """
1569
    if self.minor is None:
1570
      _ThrowError("drbd%d: device not attached in AttachNet", self._aminor)
1571

    
1572
    if None in (self._lhost, self._lport, self._rhost, self._rport):
1573
      _ThrowError("drbd%d: missing network info in AttachNet()", self.minor)
1574

    
1575
    status = self.GetProcStatus()
1576

    
1577
    if not status.is_standalone:
1578
      _ThrowError("drbd%d: device is not standalone in AttachNet", self.minor)
1579

    
1580
    self._AssembleNet(self.minor,
1581
                      (self._lhost, self._lport, self._rhost, self._rport),
1582
                      constants.DRBD_NET_PROTOCOL, dual_pri=multimaster,
1583
                      hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1584

    
1585
  def Attach(self):
1586
    """Check if our minor is configured.
1587

1588
    This doesn't do any device configurations - it only checks if the
1589
    minor is in a state different from Unconfigured.
1590

1591
    Note that this function will not change the state of the system in
1592
    any way (except in case of side-effects caused by reading from
1593
    /proc).
1594

1595
    """
1596
    used_devs = self.GetUsedDevs()
1597
    if self._aminor in used_devs:
1598
      minor = self._aminor
1599
    else:
1600
      minor = None
1601

    
1602
    self._SetFromMinor(minor)
1603
    return minor is not None
1604

    
1605
  def Assemble(self):
1606
    """Assemble the drbd.
1607

1608
    Method:
1609
      - if we have a configured device, we try to ensure that it matches
1610
        our config
1611
      - if not, we create it from zero
1612

1613
    """
1614
    super(DRBD8, self).Assemble()
1615

    
1616
    self.Attach()
1617
    if self.minor is None:
1618
      # local device completely unconfigured
1619
      self._FastAssemble()
1620
    else:
1621
      # we have to recheck the local and network status and try to fix
1622
      # the device
1623
      self._SlowAssemble()
1624

    
1625
  def _SlowAssemble(self):
1626
    """Assembles the DRBD device from a (partially) configured device.
1627

1628
    In case of partially attached (local device matches but no network
1629
    setup), we perform the network attach. If successful, we re-test
1630
    the attach if can return success.
1631

1632
    """
1633
    # TODO: Rewrite to not use a for loop just because there is 'break'
1634
    # pylint: disable-msg=W0631
1635
    net_data = (self._lhost, self._lport, self._rhost, self._rport)
1636
    for minor in (self._aminor,):
1637
      info = self._GetDevInfo(self._GetShowData(minor))
1638
      match_l = self._MatchesLocal(info)
1639
      match_r = self._MatchesNet(info)
1640

    
1641
      if match_l and match_r:
1642
        # everything matches
1643
        break
1644

    
1645
      if match_l and not match_r and "local_addr" not in info:
1646
        # disk matches, but not attached to network, attach and recheck
1647
        self._AssembleNet(minor, net_data, constants.DRBD_NET_PROTOCOL,
1648
                          hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1649
        if self._MatchesNet(self._GetDevInfo(self._GetShowData(minor))):
1650
          break
1651
        else:
1652
          _ThrowError("drbd%d: network attach successful, but 'drbdsetup"
1653
                      " show' disagrees", minor)
1654

    
1655
      if match_r and "local_dev" not in info:
1656
        # no local disk, but network attached and it matches
1657
        self._AssembleLocal(minor, self._children[0].dev_path,
1658
                            self._children[1].dev_path, self.size)
1659
        if self._MatchesNet(self._GetDevInfo(self._GetShowData(minor))):
1660
          break
1661
        else:
1662
          _ThrowError("drbd%d: disk attach successful, but 'drbdsetup"
1663
                      " show' disagrees", minor)
1664

    
1665
      # this case must be considered only if we actually have local
1666
      # storage, i.e. not in diskless mode, because all diskless
1667
      # devices are equal from the point of view of local
1668
      # configuration
1669
      if (match_l and "local_dev" in info and
1670
          not match_r and "local_addr" in info):
1671
        # strange case - the device network part points to somewhere
1672
        # else, even though its local storage is ours; as we own the
1673
        # drbd space, we try to disconnect from the remote peer and
1674
        # reconnect to our correct one
1675
        try:
1676
          self._ShutdownNet(minor)
1677
        except errors.BlockDeviceError, err:
1678
          _ThrowError("drbd%d: device has correct local storage, wrong"
1679
                      " remote peer and is unable to disconnect in order"
1680
                      " to attach to the correct peer: %s", minor, str(err))
1681
        # note: _AssembleNet also handles the case when we don't want
1682
        # local storage (i.e. one or more of the _[lr](host|port) is
1683
        # None)
1684
        self._AssembleNet(minor, net_data, constants.DRBD_NET_PROTOCOL,
1685
                          hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1686
        if self._MatchesNet(self._GetDevInfo(self._GetShowData(minor))):
1687
          break
1688
        else:
1689
          _ThrowError("drbd%d: network attach successful, but 'drbdsetup"
1690
                      " show' disagrees", minor)
1691

    
1692
    else:
1693
      minor = None
1694

    
1695
    self._SetFromMinor(minor)
1696
    if minor is None:
1697
      _ThrowError("drbd%d: cannot activate, unknown or unhandled reason",
1698
                  self._aminor)
1699

    
1700
  def _FastAssemble(self):
1701
    """Assemble the drbd device from zero.
1702

1703
    This is run when in Assemble we detect our minor is unused.
1704

1705
    """
1706
    minor = self._aminor
1707
    if self._children and self._children[0] and self._children[1]:
1708
      self._AssembleLocal(minor, self._children[0].dev_path,
1709
                          self._children[1].dev_path, self.size)
1710
    if self._lhost and self._lport and self._rhost and self._rport:
1711
      self._AssembleNet(minor,
1712
                        (self._lhost, self._lport, self._rhost, self._rport),
1713
                        constants.DRBD_NET_PROTOCOL,
1714
                        hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1715
    self._SetFromMinor(minor)
1716

    
1717
  @classmethod
1718
  def _ShutdownLocal(cls, minor):
1719
    """Detach from the local device.
1720

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

1724
    """
1725
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "detach"])
1726
    if result.failed:
1727
      _ThrowError("drbd%d: can't detach local disk: %s", minor, result.output)
1728

    
1729
  @classmethod
1730
  def _ShutdownNet(cls, minor):
1731
    """Disconnect from the remote peer.
1732

1733
    This fails if we don't have a local device.
1734

1735
    """
1736
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "disconnect"])
1737
    if result.failed:
1738
      _ThrowError("drbd%d: can't shutdown network: %s", minor, result.output)
1739

    
1740
  @classmethod
1741
  def _ShutdownAll(cls, minor):
1742
    """Deactivate the device.
1743

1744
    This will, of course, fail if the device is in use.
1745

1746
    """
1747
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "down"])
1748
    if result.failed:
1749
      _ThrowError("drbd%d: can't shutdown drbd device: %s",
1750
                  minor, result.output)
1751

    
1752
  def Shutdown(self):
1753
    """Shutdown the DRBD device.
1754

1755
    """
1756
    if self.minor is None and not self.Attach():
1757
      logging.info("drbd%d: not attached during Shutdown()", self._aminor)
1758
      return
1759
    minor = self.minor
1760
    self.minor = None
1761
    self.dev_path = None
1762
    self._ShutdownAll(minor)
1763

    
1764
  def Remove(self):
1765
    """Stub remove for DRBD devices.
1766

1767
    """
1768
    self.Shutdown()
1769

    
1770
  @classmethod
1771
  def Create(cls, unique_id, children, size):
1772
    """Create a new DRBD8 device.
1773

1774
    Since DRBD devices are not created per se, just assembled, this
1775
    function only initializes the metadata.
1776

1777
    """
1778
    if len(children) != 2:
1779
      raise errors.ProgrammerError("Invalid setup for the drbd device")
1780
    # check that the minor is unused
1781
    aminor = unique_id[4]
1782
    proc_info = cls._MassageProcData(cls._GetProcData())
1783
    if aminor in proc_info:
1784
      status = DRBD8Status(proc_info[aminor])
1785
      in_use = status.is_in_use
1786
    else:
1787
      in_use = False
1788
    if in_use:
1789
      _ThrowError("drbd%d: minor is already in use at Create() time", aminor)
1790
    meta = children[1]
1791
    meta.Assemble()
1792
    if not meta.Attach():
1793
      _ThrowError("drbd%d: can't attach to meta device '%s'",
1794
                  aminor, meta)
1795
    cls._CheckMetaSize(meta.dev_path)
1796
    cls._InitMeta(aminor, meta.dev_path)
1797
    return cls(unique_id, children, size)
1798

    
1799
  def Grow(self, amount):
1800
    """Resize the DRBD device and its backing storage.
1801

1802
    """
1803
    if self.minor is None:
1804
      _ThrowError("drbd%d: Grow called while not attached", self._aminor)
1805
    if len(self._children) != 2 or None in self._children:
1806
      _ThrowError("drbd%d: cannot grow diskless device", self.minor)
1807
    self._children[0].Grow(amount)
1808
    result = utils.RunCmd(["drbdsetup", self.dev_path, "resize", "-s",
1809
                           "%dm" % (self.size + amount)])
1810
    if result.failed:
1811
      _ThrowError("drbd%d: resize failed: %s", self.minor, result.output)
1812

    
1813

    
1814
class FileStorage(BlockDev):
1815
  """File device.
1816

1817
  This class represents the a file storage backend device.
1818

1819
  The unique_id for the file device is a (file_driver, file_path) tuple.
1820

1821
  """
1822
  def __init__(self, unique_id, children, size):
1823
    """Initalizes a file device backend.
1824

1825
    """
1826
    if children:
1827
      raise errors.BlockDeviceError("Invalid setup for file device")
1828
    super(FileStorage, self).__init__(unique_id, children, size)
1829
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
1830
      raise ValueError("Invalid configuration data %s" % str(unique_id))
1831
    self.driver = unique_id[0]
1832
    self.dev_path = unique_id[1]
1833
    self.Attach()
1834

    
1835
  def Assemble(self):
1836
    """Assemble the device.
1837

1838
    Checks whether the file device exists, raises BlockDeviceError otherwise.
1839

1840
    """
1841
    if not os.path.exists(self.dev_path):
1842
      _ThrowError("File device '%s' does not exist" % self.dev_path)
1843

    
1844
  def Shutdown(self):
1845
    """Shutdown the device.
1846

1847
    This is a no-op for the file type, as we don't deactivate
1848
    the file on shutdown.
1849

1850
    """
1851
    pass
1852

    
1853
  def Open(self, force=False):
1854
    """Make the device ready for I/O.
1855

1856
    This is a no-op for the file type.
1857

1858
    """
1859
    pass
1860

    
1861
  def Close(self):
1862
    """Notifies that the device will no longer be used for I/O.
1863

1864
    This is a no-op for the file type.
1865

1866
    """
1867
    pass
1868

    
1869
  def Remove(self):
1870
    """Remove the file backing the block device.
1871

1872
    @rtype: boolean
1873
    @return: True if the removal was successful
1874

1875
    """
1876
    try:
1877
      os.remove(self.dev_path)
1878
    except OSError, err:
1879
      if err.errno != errno.ENOENT:
1880
        _ThrowError("Can't remove file '%s': %s", self.dev_path, err)
1881

    
1882
  def Rename(self, new_id):
1883
    """Renames the file.
1884

1885
    """
1886
    # TODO: implement rename for file-based storage
1887
    _ThrowError("Rename is not supported for file-based storage")
1888

    
1889
  def Grow(self, amount):
1890
    """Grow the file
1891

1892
    @param amount: the amount (in mebibytes) to grow with
1893

1894
    """
1895
    # Check that the file exists
1896
    self.Assemble()
1897
    current_size = self.GetActualSize()
1898
    new_size = current_size + amount * 1024 * 1024
1899
    assert new_size > current_size, "Cannot Grow with a negative amount"
1900
    try:
1901
      f = open(self.dev_path, "a+")
1902
      f.truncate(new_size)
1903
      f.close()
1904
    except EnvironmentError, err:
1905
      _ThrowError("Error in file growth: %", str(err))
1906

    
1907
  def Attach(self):
1908
    """Attach to an existing file.
1909

1910
    Check if this file already exists.
1911

1912
    @rtype: boolean
1913
    @return: True if file exists
1914

1915
    """
1916
    self.attached = os.path.exists(self.dev_path)
1917
    return self.attached
1918

    
1919
  def GetActualSize(self):
1920
    """Return the actual disk size.
1921

1922
    @note: the device needs to be active when this is called
1923

1924
    """
1925
    assert self.attached, "BlockDevice not attached in GetActualSize()"
1926
    try:
1927
      st = os.stat(self.dev_path)
1928
      return st.st_size
1929
    except OSError, err:
1930
      _ThrowError("Can't stat %s: %s", self.dev_path, err)
1931

    
1932
  @classmethod
1933
  def Create(cls, unique_id, children, size):
1934
    """Create a new file.
1935

1936
    @param size: the size of file in MiB
1937

1938
    @rtype: L{bdev.FileStorage}
1939
    @return: an instance of FileStorage
1940

1941
    """
1942
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
1943
      raise ValueError("Invalid configuration data %s" % str(unique_id))
1944
    dev_path = unique_id[1]
1945
    try:
1946
      fd = os.open(dev_path, os.O_RDWR | os.O_CREAT | os.O_EXCL)
1947
      f = os.fdopen(fd, "w")
1948
      f.truncate(size * 1024 * 1024)
1949
      f.close()
1950
    except EnvironmentError, err:
1951
      if err.errno == errno.EEXIST:
1952
        _ThrowError("File already existing: %s", dev_path)
1953
      _ThrowError("Error in file creation: %", str(err))
1954

    
1955
    return FileStorage(unique_id, children, size)
1956

    
1957

    
1958
DEV_MAP = {
1959
  constants.LD_LV: LogicalVolume,
1960
  constants.LD_DRBD8: DRBD8,
1961
  }
1962

    
1963
if constants.ENABLE_FILE_STORAGE:
1964
  DEV_MAP[constants.LD_FILE] = FileStorage
1965

    
1966

    
1967
def FindDevice(dev_type, unique_id, children, size):
1968
  """Search for an existing, assembled device.
1969

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

1973
  """
1974
  if dev_type not in DEV_MAP:
1975
    raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
1976
  device = DEV_MAP[dev_type](unique_id, children, size)
1977
  if not device.attached:
1978
    return None
1979
  return device
1980

    
1981

    
1982
def Assemble(dev_type, unique_id, children, size):
1983
  """Try to attach or assemble an existing device.
1984

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

1988
  """
1989
  if dev_type not in DEV_MAP:
1990
    raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
1991
  device = DEV_MAP[dev_type](unique_id, children, size)
1992
  device.Assemble()
1993
  return device
1994

    
1995

    
1996
def Create(dev_type, unique_id, children, size):
1997
  """Create a device.
1998

1999
  """
2000
  if dev_type not in DEV_MAP:
2001
    raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
2002
  device = DEV_MAP[dev_type].Create(unique_id, children, size)
2003
  return device