Statistics
| Branch: | Tag: | Revision:

root / lib / bdev.py @ cea881e5

History | View | Annotate | Download (63.5 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(pvlist, lambda v: ":" in v):
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(cls._INVALID_SUBSTRINGS, lambda x: x in name)):
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
    bytes = sectors * 512
963
    if bytes < 128 * 1024 * 1024: # less than 128MiB
964
      _ThrowError("Meta device too small (%.2fMib)", (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 bytes > 1024 * 1024 * 1024:
972
      _ThrowError("Meta device too big (%.2fMiB)", (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
    semi = pyp.Literal(";").suppress()
1083
    # this also converts the value to an int
1084
    number = pyp.Word(pyp.nums).setParseAction(lambda s, l, t: int(t[0]))
1085

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

    
1090
    keyword = pyp.Word(pyp.alphanums + '-')
1091

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

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

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

    
1116
    bnf = pyp.ZeroOrMore(pyp.Group(section ^ stmt))
1117
    bnf.ignore(comment)
1118

    
1119
    cls._PARSE_SHOW = bnf
1120

    
1121
    return bnf
1122

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

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

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

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

1144
    """
1145
    data = {}
1146
    if not out:
1147
      return data
1148

    
1149
    bnf = cls._GetShowParser()
1150
    # run pyparse
1151

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

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

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

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

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

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

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

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

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

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

    
1217
    if self._lhost is None:
1218
      return False
1219

    
1220
    if not ("local_addr" in info and
1221
            "remote_addr" in info):
1222
      return False
1223

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

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

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

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

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

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

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

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

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

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

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

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

    
1332
    self._AssembleLocal(self.minor, backend.dev_path, meta.dev_path, self.size)
1333
    self._children = devices
1334

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

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

    
1359
    self._ShutdownLocal(self.minor)
1360
    self._children = []
1361

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

1366
    This is the low-level implementation.
1367

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

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

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

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

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

    
1398
  def GetProcStatus(self):
1399
    """Return device data from /proc.
1400

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

    
1409
  def GetSyncStatus(self):
1410
    """Returns the sync status of the device.
1411

1412

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

1417

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

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

1424
    @rtype: objects.BlockDevStatus
1425

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

    
1430
    stats = self.GetProcStatus()
1431
    is_degraded = not stats.is_connected or not stats.is_disk_uptodate
1432

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

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

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

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

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

    
1468
  def Close(self):
1469
    """Make the local state secondary.
1470

1471
    This will, of course, fail if the device is in use.
1472

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

    
1481
  def DisconnectNet(self):
1482
    """Removes network configuration.
1483

1484
    This method shutdowns the network side of the device.
1485

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

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

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

    
1502
    class _DisconnectStatus:
1503
      def __init__(self, ever_disconnected):
1504
        self.ever_disconnected = ever_disconnected
1505

    
1506
    dstatus = _DisconnectStatus(_IgnoreError(self._ShutdownNet, self.minor))
1507

    
1508
    def _WaitForDisconnect():
1509
      if self.GetProcStatus().is_standalone:
1510
        return
1511

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

    
1518
      raise utils.RetryAgain()
1519

    
1520
    # Keep start time
1521
    start_time = time.time()
1522

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

    
1534
      _ThrowError(msg, self.minor)
1535

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

    
1541
  def AttachNet(self, multimaster):
1542
    """Reconnects the network.
1543

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

1548
    Args:
1549
      - multimaster: init the network in dual-primary mode
1550

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

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

    
1558
    status = self.GetProcStatus()
1559

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

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

    
1568
  def Attach(self):
1569
    """Check if our minor is configured.
1570

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

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

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

    
1585
    self._SetFromMinor(minor)
1586
    return minor is not None
1587

    
1588
  def Assemble(self):
1589
    """Assemble the drbd.
1590

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

1596
    """
1597
    super(DRBD8, self).Assemble()
1598

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

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

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

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

    
1624
      if match_l and match_r:
1625
        # everything matches
1626
        break
1627

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

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

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

    
1675
    else:
1676
      minor = None
1677

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

    
1683
  def _FastAssemble(self):
1684
    """Assemble the drbd device from zero.
1685

1686
    This is run when in Assemble we detect our minor is unused.
1687

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

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

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

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

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

1716
    This fails if we don't have a local device.
1717

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

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

1727
    This will, of course, fail if the device is in use.
1728

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

    
1735
  def Shutdown(self):
1736
    """Shutdown the DRBD device.
1737

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

    
1747
  def Remove(self):
1748
    """Stub remove for DRBD devices.
1749

1750
    """
1751
    self.Shutdown()
1752

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

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

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

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

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

    
1796

    
1797
class FileStorage(BlockDev):
1798
  """File device.
1799

1800
  This class represents the a file storage backend device.
1801

1802
  The unique_id for the file device is a (file_driver, file_path) tuple.
1803

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

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

    
1818
  def Assemble(self):
1819
    """Assemble the device.
1820

1821
    Checks whether the file device exists, raises BlockDeviceError otherwise.
1822

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

    
1827
  def Shutdown(self):
1828
    """Shutdown the device.
1829

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

1833
    """
1834
    pass
1835

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

1839
    This is a no-op for the file type.
1840

1841
    """
1842
    pass
1843

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

1847
    This is a no-op for the file type.
1848

1849
    """
1850
    pass
1851

    
1852
  def Remove(self):
1853
    """Remove the file backing the block device.
1854

1855
    @rtype: boolean
1856
    @return: True if the removal was successful
1857

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

    
1865
  def Rename(self, new_id):
1866
    """Renames the file.
1867

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

    
1872
  def Grow(self, amount):
1873
    """Grow the file
1874

1875
    @param amount: the amount (in mebibytes) to grow with
1876

1877
    """
1878
    # Check that the file exists
1879
    self.Assemble()
1880
    current_size = self.GetActualSize()
1881
    new_size = current_size + amount * 1024 * 1024
1882
    assert new_size > current_size, "Cannot Grow with a negative amount"
1883
    try:
1884
      f = open(self.dev_path, "a+")
1885
      f.truncate(new_size)
1886
      f.close()
1887
    except EnvironmentError, err:
1888
      _ThrowError("Error in file growth: %", str(err))
1889

    
1890
  def Attach(self):
1891
    """Attach to an existing file.
1892

1893
    Check if this file already exists.
1894

1895
    @rtype: boolean
1896
    @return: True if file exists
1897

1898
    """
1899
    self.attached = os.path.exists(self.dev_path)
1900
    return self.attached
1901

    
1902
  def GetActualSize(self):
1903
    """Return the actual disk size.
1904

1905
    @note: the device needs to be active when this is called
1906

1907
    """
1908
    assert self.attached, "BlockDevice not attached in GetActualSize()"
1909
    try:
1910
      st = os.stat(self.dev_path)
1911
      return st.st_size
1912
    except OSError, err:
1913
      _ThrowError("Can't stat %s: %s", self.dev_path, err)
1914

    
1915
  @classmethod
1916
  def Create(cls, unique_id, children, size):
1917
    """Create a new file.
1918

1919
    @param size: the size of file in MiB
1920

1921
    @rtype: L{bdev.FileStorage}
1922
    @return: an instance of FileStorage
1923

1924
    """
1925
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
1926
      raise ValueError("Invalid configuration data %s" % str(unique_id))
1927
    dev_path = unique_id[1]
1928
    try:
1929
      fd = os.open(dev_path, os.O_RDWR | os.O_CREAT | os.O_EXCL)
1930
      f = os.fdopen(fd, "w")
1931
      f.truncate(size * 1024 * 1024)
1932
      f.close()
1933
    except EnvironmentError, err:
1934
      if err.errno == errno.EEXIST:
1935
        _ThrowError("File already existing: %s", dev_path)
1936
      _ThrowError("Error in file creation: %", str(err))
1937

    
1938
    return FileStorage(unique_id, children, size)
1939

    
1940

    
1941
DEV_MAP = {
1942
  constants.LD_LV: LogicalVolume,
1943
  constants.LD_DRBD8: DRBD8,
1944
  }
1945

    
1946
if constants.ENABLE_FILE_STORAGE:
1947
  DEV_MAP[constants.LD_FILE] = FileStorage
1948

    
1949

    
1950
def FindDevice(dev_type, unique_id, children, size):
1951
  """Search for an existing, assembled device.
1952

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

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

    
1964

    
1965
def Assemble(dev_type, unique_id, children, size):
1966
  """Try to attach or assemble an existing device.
1967

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

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

    
1978

    
1979
def Create(dev_type, unique_id, children, size):
1980
  """Create a device.
1981

1982
  """
1983
  if dev_type not in DEV_MAP:
1984
    raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
1985
  device = DEV_MAP[dev_type].Create(unique_id, children, size)
1986
  return device