Statistics
| Branch: | Tag: | Revision:

root / lib / bdev.py @ a8083063

History | View | Annotate | Download (43.5 kB)

1
#!/usr/bin/python
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

    
28
from ganeti import utils
29
from ganeti import logger
30
from ganeti import errors
31

    
32

    
33
class BlockDev(object):
34
  """Block device abstract class.
35

36
  A block device can be in the following states:
37
    - not existing on the system, and by `Create()` it goes into:
38
    - existing but not setup/not active, and by `Assemble()` goes into:
39
    - active read-write and by `Open()` it goes into
40
    - online (=used, or ready for use)
41

42
  A device can also be online but read-only, however we are not using
43
  the readonly state (MD and LV have it, if needed in the future)
44
  and we are usually looking at this like at a stack, so it's easier
45
  to conceptualise the transition from not-existing to online and back
46
  like a linear one.
47

48
  The many different states of the device are due to the fact that we
49
  need to cover many device types:
50
    - logical volumes are created, lvchange -a y $lv, and used
51
    - md arrays are created or assembled and used
52
    - drbd devices are attached to a local disk/remote peer and made primary
53

54
  The status of the device can be examined by `GetStatus()`, which
55
  returns a numerical value, depending on the position in the
56
  transition stack of the device.
57

58
  A block device is identified by three items:
59
    - the /dev path of the device (dynamic)
60
    - a unique ID of the device (static)
61
    - it's major/minor pair (dynamic)
62

63
  Not all devices implement both the first two as distinct items. LVM
64
  logical volumes have their unique ID (the pair volume group, logical
65
  volume name) in a 1-to-1 relation to the dev path. For MD devices,
66
  the /dev path is dynamic and the unique ID is the UUID generated at
67
  array creation plus the slave list. For DRBD devices, the /dev path
68
  is again dynamic and the unique id is the pair (host1, dev1),
69
  (host2, dev2).
70

71
  You can get to a device in two ways:
72
    - creating the (real) device, which returns you
73
      an attached instance (lvcreate, mdadm --create)
74
    - attaching of a python instance to an existing (real) device
75

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

82
  """
83

    
84
  STATUS_UNKNOWN = 0
85
  STATUS_EXISTING = 1
86
  STATUS_STANDBY = 2
87
  STATUS_ONLINE = 3
88

    
89
  STATUS_MAP = {
90
    STATUS_UNKNOWN: "unknown",
91
    STATUS_EXISTING: "existing",
92
    STATUS_STANDBY: "ready for use",
93
    STATUS_ONLINE: "online",
94
    }
95

    
96

    
97
  def __init__(self, unique_id, children):
98
    self._children = children
99
    self.dev_path = None
100
    self.unique_id = unique_id
101
    self.major = None
102
    self.minor = None
103

    
104

    
105
  def Assemble(self):
106
    """Assemble the device from its components.
107

108
    If this is a plain block device (e.g. LVM) than assemble does
109
    nothing, as the LVM has no children and we don't put logical
110
    volumes offline.
111

112
    One guarantee is that after the device has been assembled, it
113
    knows its major/minor numbers. This allows other devices (usually
114
    parents) to probe correctly for their children.
115

116
    """
117
    status = True
118
    for child in self._children:
119
      if not isinstance(child, BlockDev):
120
        raise TypeError("Invalid child passed of type '%s'" % type(child))
121
      if not status:
122
        break
123
      status = status and child.Assemble()
124
      if not status:
125
        break
126
      status = status and child.Open()
127

    
128
    if not status:
129
      for child in self._children:
130
        child.Shutdown()
131
    return status
132

    
133

    
134
  def Attach(self):
135
    """Find a device which matches our config and attach to it.
136

137
    """
138
    raise NotImplementedError
139

    
140

    
141
  def Close(self):
142
    """Notifies that the device will no longer be used for I/O.
143

144
    """
145
    raise NotImplementedError
146

    
147

    
148
  @classmethod
149
  def Create(cls, unique_id, children, size):
150
    """Create the device.
151

152
    If the device cannot be created, it will return None
153
    instead. Error messages go to the logging system.
154

155
    Note that for some devices, the unique_id is used, and for other,
156
    the children. The idea is that these two, taken together, are
157
    enough for both creation and assembly (later).
158

159
    """
160
    raise NotImplementedError
161

    
162

    
163
  def Remove(self):
164
    """Remove this device.
165

166
    This makes sense only for some of the device types: LV and to a
167
    lesser degree, md devices. Also note that if the device can't
168
    attach, the removal can't be completed.
169

170
    """
171
    raise NotImplementedError
172

    
173

    
174
  def GetStatus(self):
175
    """Return the status of the device.
176

177
    """
178
    raise NotImplementedError
179

    
180

    
181
  def Open(self, force=False):
182
    """Make the device ready for use.
183

184
    This makes the device ready for I/O. For now, just the DRBD
185
    devices need this.
186

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

190
    """
191
    raise NotImplementedError
192

    
193

    
194
  def Shutdown(self):
195
    """Shut down the device, freeing its children.
196

197
    This undoes the `Assemble()` work, except for the child
198
    assembling; as such, the children on the device are still
199
    assembled after this call.
200

201
    """
202
    raise NotImplementedError
203

    
204

    
205
  def SetSyncSpeed(self, speed):
206
    """Adjust the sync speed of the mirror.
207

208
    In case this is not a mirroring device, this is no-op.
209

210
    """
211
    result = True
212
    if self._children:
213
      for child in self._children:
214
        result = result and child.SetSyncSpeed(speed)
215
    return result
216

    
217

    
218
  def GetSyncStatus(self):
219
    """Returns the sync status of the device.
220

221
    If this device is a mirroring device, this function returns the
222
    status of the mirror.
223

224
    Returns:
225
     (sync_percent, estimated_time, is_degraded)
226

227
    If sync_percent is None, it means all is ok
228
    If estimated_time is None, it means we can't estimate
229
    the time needed, otherwise it's the time left in seconds
230
    If is_degraded is True, it means the device is missing
231
    redundancy. This is usually a sign that something went wrong in
232
    the device setup, if sync_percent is None.
233

234
    """
235
    return None, None, False
236

    
237

    
238
  def CombinedSyncStatus(self):
239
    """Calculate the mirror status recursively for our children.
240

241
    The return value is the same as for `GetSyncStatus()` except the
242
    minimum percent and maximum time are calculated across our
243
    children.
244

245
    """
246
    min_percent, max_time, is_degraded = self.GetSyncStatus()
247
    if self._children:
248
      for child in self._children:
249
        c_percent, c_time, c_degraded = child.GetSyncStatus()
250
        if min_percent is None:
251
          min_percent = c_percent
252
        elif c_percent is not None:
253
          min_percent = min(min_percent, c_percent)
254
        if max_time is None:
255
          max_time = c_time
256
        elif c_time is not None:
257
          max_time = max(max_time, c_time)
258
        is_degraded = is_degraded or c_degraded
259
    return min_percent, max_time, is_degraded
260

    
261

    
262
  def __repr__(self):
263
    return ("<%s: unique_id: %s, children: %s, %s:%s, %s>" %
264
            (self.__class__, self.unique_id, self._children,
265
             self.major, self.minor, self.dev_path))
266

    
267

    
268
class LogicalVolume(BlockDev):
269
  """Logical Volume block device.
270

271
  """
272
  def __init__(self, unique_id, children):
273
    """Attaches to a LV device.
274

275
    The unique_id is a tuple (vg_name, lv_name)
276

277
    """
278
    super(LogicalVolume, self).__init__(unique_id, children)
279
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
280
      raise ValueError("Invalid configuration data %s" % str(unique_id))
281
    self._vg_name, self._lv_name = unique_id
282
    self.dev_path = "/dev/%s/%s" % (self._vg_name, self._lv_name)
283
    self.Attach()
284

    
285

    
286
  @classmethod
287
  def Create(cls, unique_id, children, size):
288
    """Create a new logical volume.
289

290
    """
291
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
292
      raise ValueError("Invalid configuration data %s" % str(unique_id))
293
    vg_name, lv_name = unique_id
294
    pvs_info = cls.GetPVInfo(vg_name)
295
    if not pvs_info:
296
      raise errors.BlockDeviceError, ("Can't compute PV info for vg %s" %
297
                                      vg_name)
298
    pvs_info.sort()
299
    pvs_info.reverse()
300
    free_size, pv_name = pvs_info[0]
301
    if free_size < size:
302
      raise errors.BlockDeviceError, ("Not enough free space: required %s,"
303
                                      " available %s" % (size, free_size))
304
    result = utils.RunCmd(["lvcreate", "-L%dm" % size, "-n%s" % lv_name,
305
                           vg_name, pv_name])
306
    if result.failed:
307
      raise errors.BlockDeviceError(result.fail_reason)
308
    return LogicalVolume(unique_id, children)
309

    
310
  @staticmethod
311
  def GetPVInfo(vg_name):
312
    """Get the free space info for PVs in a volume group.
313

314
    Args:
315
      vg_name: the volume group name
316

317
    Returns:
318
      list of (free_space, name) with free_space in mebibytes
319
    """
320
    command = ["pvs", "--noheadings", "--nosuffix", "--units=m",
321
               "-opv_name,vg_name,pv_free,pv_attr", "--unbuffered",
322
               "--separator=:"]
323
    result = utils.RunCmd(command)
324
    if result.failed:
325
      logger.Error("Can't get the PV information: %s" % result.fail_reason)
326
      return None
327
    data = []
328
    for line in result.stdout.splitlines():
329
      fields = line.strip().split(':')
330
      if len(fields) != 4:
331
        logger.Error("Can't parse pvs output: line '%s'" % line)
332
        return None
333
      # skip over pvs from another vg or ones which are not allocatable
334
      if fields[1] != vg_name or fields[3][0] != 'a':
335
        continue
336
      data.append((float(fields[2]), fields[0]))
337

    
338
    return data
339

    
340
  def Remove(self):
341
    """Remove this logical volume.
342

343
    """
344
    if not self.minor and not self.Attach():
345
      # the LV does not exist
346
      return True
347
    result = utils.RunCmd(["lvremove", "-f", "%s/%s" %
348
                           (self._vg_name, self._lv_name)])
349
    if result.failed:
350
      logger.Error("Can't lvremove: %s" % result.fail_reason)
351

    
352
    return not result.failed
353

    
354

    
355
  def Attach(self):
356
    """Attach to an existing LV.
357

358
    This method will try to see if an existing and active LV exists
359
    which matches the our name. If so, its major/minor will be
360
    recorded.
361

362
    """
363
    result = utils.RunCmd(["lvdisplay", self.dev_path])
364
    if result.failed:
365
      logger.Error("Can't find LV %s: %s" %
366
                   (self.dev_path, result.fail_reason))
367
      return False
368
    match = re.compile("^ *Block device *([0-9]+):([0-9]+).*$")
369
    for line in result.stdout.splitlines():
370
      match_result = match.match(line)
371
      if match_result:
372
        self.major = int(match_result.group(1))
373
        self.minor = int(match_result.group(2))
374
        return True
375
    return False
376

    
377

    
378
  def Assemble(self):
379
    """Assemble the device.
380

381
    This is a no-op for the LV device type. Eventually, we could
382
    lvchange -ay here if we see that the LV is not active.
383

384
    """
385
    return True
386

    
387

    
388
  def Shutdown(self):
389
    """Shutdown the device.
390

391
    This is a no-op for the LV device type, as we don't deactivate the
392
    volumes on shutdown.
393

394
    """
395
    return True
396

    
397

    
398
  def GetStatus(self):
399
    """Return the status of the device.
400

401
    Logical volumes will can be in all four states, although we don't
402
    deactivate (lvchange -an) them when shutdown, so STATUS_EXISTING
403
    should not be seen for our devices.
404

405
    """
406
    result = utils.RunCmd(["lvs", "--noheadings", "-olv_attr", self.dev_path])
407
    if result.failed:
408
      logger.Error("Can't display lv: %s" % result.fail_reason)
409
      return self.STATUS_UNKNOWN
410
    out = result.stdout.strip()
411
    # format: type/permissions/alloc/fixed_minor/state/open
412
    if len(out) != 6:
413
      return self.STATUS_UNKNOWN
414
    #writable = (out[1] == "w")
415
    active = (out[4] == "a")
416
    online = (out[5] == "o")
417
    if online:
418
      retval = self.STATUS_ONLINE
419
    elif active:
420
      retval = self.STATUS_STANDBY
421
    else:
422
      retval = self.STATUS_EXISTING
423

    
424
    return retval
425

    
426

    
427
  def Open(self, force=False):
428
    """Make the device ready for I/O.
429

430
    This is a no-op for the LV device type.
431

432
    """
433
    return True
434

    
435

    
436
  def Close(self):
437
    """Notifies that the device will no longer be used for I/O.
438

439
    This is a no-op for the LV device type.
440

441
    """
442
    return True
443

    
444

    
445
  def Snapshot(self, size):
446
    """Create a snapshot copy of an lvm block device.
447

448
    """
449

    
450
    snap_name = self._lv_name + ".snap"
451

    
452
    # remove existing snapshot if found
453
    snap = LogicalVolume((self._vg_name, snap_name), None)
454
    snap.Remove()
455

    
456
    pvs_info = self.GetPVInfo(self._vg_name)
457
    if not pvs_info:
458
      raise errors.BlockDeviceError, ("Can't compute PV info for vg %s" %
459
                                      self._vg_name)
460
    pvs_info.sort()
461
    pvs_info.reverse()
462
    free_size, pv_name = pvs_info[0]
463
    if free_size < size:
464
      raise errors.BlockDeviceError, ("Not enough free space: required %s,"
465
                                      " available %s" % (size, free_size))
466

    
467
    result = utils.RunCmd(["lvcreate", "-L%dm" % size, "-s",
468
                           "-n%s" % snap_name, self.dev_path])
469
    if result.failed:
470
      raise errors.BlockDeviceError, ("command: %s error: %s" %
471
                                      (result.cmd, result.fail_reason))
472

    
473
    return snap_name
474

    
475

    
476
class MDRaid1(BlockDev):
477
  """raid1 device implemented via md.
478

479
  """
480
  def __init__(self, unique_id, children):
481
    super(MDRaid1, self).__init__(unique_id, children)
482
    self.major = 9
483
    self.Attach()
484

    
485

    
486
  def Attach(self):
487
    """Find an array which matches our config and attach to it.
488

489
    This tries to find a MD array which has the same UUID as our own.
490

491
    """
492
    minor = self._FindMDByUUID(self.unique_id)
493
    if minor is not None:
494
      self._SetFromMinor(minor)
495
    else:
496
      self.minor = None
497
      self.dev_path = None
498

    
499
    return (minor is not None)
500

    
501

    
502
  @staticmethod
503
  def _GetUsedDevs():
504
    """Compute the list of in-use MD devices.
505

506
    It doesn't matter if the used device have other raid level, just
507
    that they are in use.
508

509
    """
510
    mdstat = open("/proc/mdstat", "r")
511
    data = mdstat.readlines()
512
    mdstat.close()
513

    
514
    used_md = {}
515
    valid_line = re.compile("^md([0-9]+) : .*$")
516
    for line in data:
517
      match = valid_line.match(line)
518
      if match:
519
        md_no = int(match.group(1))
520
        used_md[md_no] = line
521

    
522
    return used_md
523

    
524

    
525
  @staticmethod
526
  def _GetDevInfo(minor):
527
    """Get info about a MD device.
528

529
    Currently only uuid is returned.
530

531
    """
532
    result = utils.RunCmd(["mdadm", "-D", "/dev/md%d" % minor])
533
    if result.failed:
534
      logger.Error("Can't display md: %s" % result.fail_reason)
535
      return None
536
    retval = {}
537
    for line in result.stdout.splitlines():
538
      line = line.strip()
539
      kv = line.split(" : ", 1)
540
      if kv:
541
        if kv[0] == "UUID":
542
          retval["uuid"] = kv[1]
543
        elif kv[0] == "State":
544
          retval["state"] = kv[1].split(", ")
545
    return retval
546

    
547

    
548
  @staticmethod
549
  def _FindUnusedMinor():
550
    """Compute an unused MD minor.
551

552
    This code assumes that there are 256 minors only.
553

554
    """
555
    used_md = MDRaid1._GetUsedDevs()
556
    i = 0
557
    while i < 256:
558
      if i not in used_md:
559
        break
560
      i += 1
561
    if i == 256:
562
      logger.Error("Critical: Out of md minor numbers.")
563
      return None
564
    return i
565

    
566

    
567
  @classmethod
568
  def _FindMDByUUID(cls, uuid):
569
    """Find the minor of an MD array with a given UUID.
570

571
    """
572
    md_list = cls._GetUsedDevs()
573
    for minor in md_list:
574
      info = cls._GetDevInfo(minor)
575
      if info and info["uuid"] == uuid:
576
        return minor
577
    return None
578

    
579

    
580
  @classmethod
581
  def Create(cls, unique_id, children, size):
582
    """Create a new MD raid1 array.
583

584
    """
585
    if not isinstance(children, (tuple, list)):
586
      raise ValueError("Invalid setup data for MDRaid1 dev: %s" %
587
                       str(children))
588
    for i in children:
589
      if not isinstance(i, BlockDev):
590
        raise ValueError("Invalid member in MDRaid1 dev: %s" % type(i))
591
    for i in children:
592
      result = utils.RunCmd(["mdadm", "--zero-superblock", "--force",
593
                             i.dev_path])
594
      if result.failed:
595
        logger.Error("Can't zero superblock: %s" % result.fail_reason)
596
        return None
597
    minor = cls._FindUnusedMinor()
598
    result = utils.RunCmd(["mdadm", "--create", "/dev/md%d" % minor,
599
                           "--auto=yes", "--force", "-l1",
600
                           "-n%d" % len(children)] +
601
                          [dev.dev_path for dev in children])
602

    
603
    if result.failed:
604
      logger.Error("Can't create md: %s" % result.fail_reason)
605
      return None
606
    info = cls._GetDevInfo(minor)
607
    if not info or not "uuid" in info:
608
      logger.Error("Wrong information returned from mdadm -D: %s" % str(info))
609
      return None
610
    return MDRaid1(info["uuid"], children)
611

    
612

    
613
  def Remove(self):
614
    """Stub remove function for MD RAID 1 arrays.
615

616
    We don't remove the superblock right now. Mark a to do.
617

618
    """
619
    #TODO: maybe zero superblock on child devices?
620
    return self.Shutdown()
621

    
622

    
623
  def AddChild(self, device):
624
    """Add a new member to the md raid1.
625

626
    """
627
    if self.minor is None and not self.Attach():
628
      raise errors.BlockDeviceError, "Can't attach to device"
629
    if device.dev_path is None:
630
      raise errors.BlockDeviceError, "New child is not initialised"
631
    result = utils.RunCmd(["mdadm", "-a", self.dev_path, device.dev_path])
632
    if result.failed:
633
      raise errors.BlockDeviceError, ("Failed to add new device to array: %s" %
634
                                      result.output)
635
    new_len = len(self._children) + 1
636
    result = utils.RunCmd(["mdadm", "--grow", self.dev_path, "-n", new_len])
637
    if result.failed:
638
      raise errors.BlockDeviceError, ("Can't grow md array: %s" %
639
                                      result.output)
640
    self._children.append(device)
641

    
642

    
643
  def RemoveChild(self, dev_path):
644
    """Remove member from the md raid1.
645

646
    """
647
    if self.minor is None and not self.Attach():
648
      raise errors.BlockDeviceError, "Can't attach to device"
649
    if len(self._children) == 1:
650
      raise errors.BlockDeviceError, ("Can't reduce member when only one"
651
                                      " child left")
652
    for device in self._children:
653
      if device.dev_path == dev_path:
654
        break
655
    else:
656
      raise errors.BlockDeviceError, "Can't find child with this path"
657
    new_len = len(self._children) - 1
658
    result = utils.RunCmd(["mdadm", "-f", self.dev_path, dev_path])
659
    if result.failed:
660
      raise errors.BlockDeviceError, ("Failed to mark device as failed: %s" %
661
                                      result.output)
662

    
663
    # it seems here we need a short delay for MD to update its
664
    # superblocks
665
    time.sleep(0.5)
666
    result = utils.RunCmd(["mdadm", "-r", self.dev_path, dev_path])
667
    if result.failed:
668
      raise errors.BlockDeviceError, ("Failed to remove device from array:"
669
                                      " %s" % result.output)
670
    result = utils.RunCmd(["mdadm", "--grow", "--force", self.dev_path,
671
                           "-n", new_len])
672
    if result.failed:
673
      raise errors.BlockDeviceError, ("Can't shrink md array: %s" %
674
                                      result.output)
675
    self._children.remove(device)
676

    
677

    
678
  def GetStatus(self):
679
    """Return the status of the device.
680

681
    """
682
    self.Attach()
683
    if self.minor is None:
684
      retval = self.STATUS_UNKNOWN
685
    else:
686
      retval = self.STATUS_ONLINE
687
    return retval
688

    
689

    
690
  def _SetFromMinor(self, minor):
691
    """Set our parameters based on the given minor.
692

693
    This sets our minor variable and our dev_path.
694

695
    """
696
    self.minor = minor
697
    self.dev_path = "/dev/md%d" % minor
698

    
699

    
700
  def Assemble(self):
701
    """Assemble the MD device.
702

703
    At this point we should have:
704
      - list of children devices
705
      - uuid
706

707
    """
708
    result = super(MDRaid1, self).Assemble()
709
    if not result:
710
      return result
711
    md_list = self._GetUsedDevs()
712
    for minor in md_list:
713
      info = self._GetDevInfo(minor)
714
      if info and info["uuid"] == self.unique_id:
715
        self._SetFromMinor(minor)
716
        logger.Info("MD array %s already started" % str(self))
717
        return True
718
    free_minor = self._FindUnusedMinor()
719
    result = utils.RunCmd(["mdadm", "-A", "--auto=yes", "--uuid",
720
                           self.unique_id, "/dev/md%d" % free_minor] +
721
                          [bdev.dev_path for bdev in self._children])
722
    if result.failed:
723
      logger.Error("Can't assemble MD array: %s" % result.fail_reason)
724
      self.minor = None
725
    else:
726
      self.minor = free_minor
727
    return not result.failed
728

    
729

    
730
  def Shutdown(self):
731
    """Tear down the MD array.
732

733
    This does a 'mdadm --stop' so after this command, the array is no
734
    longer available.
735

736
    """
737
    if self.minor is None and not self.Attach():
738
      logger.Info("MD object not attached to a device")
739
      return True
740

    
741
    result = utils.RunCmd(["mdadm", "--stop", "/dev/md%d" % self.minor])
742
    if result.failed:
743
      logger.Error("Can't stop MD array: %s" % result.fail_reason)
744
      return False
745
    self.minor = None
746
    self.dev_path = None
747
    return True
748

    
749

    
750
  def SetSyncSpeed(self, kbytes):
751
    """Set the maximum sync speed for the MD array.
752

753
    """
754
    result = super(MDRaid1, self).SetSyncSpeed(kbytes)
755
    if self.minor is None:
756
      logger.Error("MD array not attached to a device")
757
      return False
758
    f = open("/sys/block/md%d/md/sync_speed_max" % self.minor, "w")
759
    try:
760
      f.write("%d" % kbytes)
761
    finally:
762
      f.close()
763
    f = open("/sys/block/md%d/md/sync_speed_min" % self.minor, "w")
764
    try:
765
      f.write("%d" % (kbytes/2))
766
    finally:
767
      f.close()
768
    return result
769

    
770

    
771
  def GetSyncStatus(self):
772
    """Returns the sync status of the device.
773

774
    Returns:
775
     (sync_percent, estimated_time)
776

777
    If sync_percent is None, it means all is ok
778
    If estimated_time is None, it means we can't esimate
779
    the time needed, otherwise it's the time left in seconds
780

781
    """
782
    if self.minor is None and not self.Attach():
783
      raise errors.BlockDeviceError("Can't attach to device in GetSyncStatus")
784
    dev_info = self._GetDevInfo(self.minor)
785
    is_clean = ("state" in dev_info and
786
                len(dev_info["state"]) == 1 and
787
                dev_info["state"][0] in ("clean", "active"))
788
    sys_path = "/sys/block/md%s/md/" % self.minor
789
    f = file(sys_path + "sync_action")
790
    sync_status = f.readline().strip()
791
    f.close()
792
    if sync_status == "idle":
793
      return None, None, not is_clean
794
    f = file(sys_path + "sync_completed")
795
    sync_completed = f.readline().strip().split(" / ")
796
    f.close()
797
    if len(sync_completed) != 2:
798
      return 0, None, not is_clean
799
    sync_done, sync_total = [float(i) for i in sync_completed]
800
    sync_percent = 100.0*sync_done/sync_total
801
    f = file(sys_path + "sync_speed")
802
    sync_speed_k = int(f.readline().strip())
803
    if sync_speed_k == 0:
804
      time_est = None
805
    else:
806
      time_est = (sync_total - sync_done) / 2 / sync_speed_k
807
    return sync_percent, time_est, not is_clean
808

    
809

    
810
  def Open(self, force=False):
811
    """Make the device ready for I/O.
812

813
    This is a no-op for the MDRaid1 device type, although we could use
814
    the 2.6.18's new array_state thing.
815

816
    """
817
    return True
818

    
819

    
820
  def Close(self):
821
    """Notifies that the device will no longer be used for I/O.
822

823
    This is a no-op for the MDRaid1 device type, but see comment for
824
    `Open()`.
825

826
    """
827
    return True
828

    
829

    
830

    
831
class DRBDev(BlockDev):
832
  """DRBD block device.
833

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

838
  The unique_id for the drbd device is the (local_ip, local_port,
839
  remote_ip, remote_port) tuple, and it must have two children: the
840
  data device and the meta_device. The meta device is checked for
841
  valid size and is zeroed on create.
842

843
  """
844
  _DRBD_MAJOR = 147
845
  _ST_UNCONFIGURED = "Unconfigured"
846
  _ST_WFCONNECTION = "WFConnection"
847
  _ST_CONNECTED = "Connected"
848

    
849
  def __init__(self, unique_id, children):
850
    super(DRBDev, self).__init__(unique_id, children)
851
    self.major = self._DRBD_MAJOR
852
    if len(children) != 2:
853
      raise ValueError("Invalid configuration data %s" % str(children))
854
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 4:
855
      raise ValueError("Invalid configuration data %s" % str(unique_id))
856
    self._lhost, self._lport, self._rhost, self._rport = unique_id
857
    self.Attach()
858

    
859
  @staticmethod
860
  def _DevPath(minor):
861
    """Return the path to a drbd device for a given minor.
862

863
    """
864
    return "/dev/drbd%d" % minor
865

    
866
  @staticmethod
867
  def _GetProcData():
868
    """Return data from /proc/drbd.
869

870
    """
871
    stat = open("/proc/drbd", "r")
872
    data = stat.read().splitlines()
873
    stat.close()
874
    return data
875

    
876

    
877
  @classmethod
878
  def _GetUsedDevs(cls):
879
    """Compute the list of used DRBD devices.
880

881
    """
882
    data = cls._GetProcData()
883

    
884
    used_devs = {}
885
    valid_line = re.compile("^ *([0-9]+): cs:([^ ]+).*$")
886
    for line in data:
887
      match = valid_line.match(line)
888
      if not match:
889
        continue
890
      minor = int(match.group(1))
891
      state = match.group(2)
892
      if state == cls._ST_UNCONFIGURED:
893
        continue
894
      used_devs[minor] = state, line
895

    
896
    return used_devs
897

    
898

    
899
  @classmethod
900
  def _FindUnusedMinor(cls):
901
    """Find an unused DRBD device.
902

903
    """
904
    data = cls._GetProcData()
905

    
906
    valid_line = re.compile("^ *([0-9]+): cs:Unconfigured$")
907
    for line in data:
908
      match = valid_line.match(line)
909
      if match:
910
        return int(match.group(1))
911
    logger.Error("Error: no free drbd minors!")
912
    return None
913

    
914

    
915
  @classmethod
916
  def _GetDevInfo(cls, minor):
917
    """Get details about a given DRBD minor.
918

919
    This return, if available, the local backing device in (major,
920
    minor) formant and the local and remote (ip, port) information.
921

922
    """
923
    data = {}
924
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "show"])
925
    if result.failed:
926
      logger.Error("Can't display the drbd config: %s" % result.fail_reason)
927
      return data
928
    out = result.stdout
929
    if out == "Not configured\n":
930
      return data
931
    for line in out.splitlines():
932
      if "local_dev" not in data:
933
        match = re.match("^Lower device: ([0-9]+):([0-9]+) .*$", line)
934
        if match:
935
          data["local_dev"] = (int(match.group(1)), int(match.group(2)))
936
          continue
937
      if "meta_dev" not in data:
938
        match = re.match("^Meta device: (([0-9]+):([0-9]+)|internal).*$", line)
939
        if match:
940
          if match.group(2) is not None and match.group(3) is not None:
941
            # matched on the major/minor
942
            data["meta_dev"] = (int(match.group(2)), int(match.group(3)))
943
          else:
944
            # matched on the "internal" string
945
            data["meta_dev"] = match.group(1)
946
            # in this case, no meta_index is in the output
947
            data["meta_index"] = -1
948
          continue
949
      if "meta_index" not in data:
950
        match = re.match("^Meta index: ([0-9]+).*$", line)
951
        if match:
952
          data["meta_index"] = int(match.group(1))
953
          continue
954
      if "local_addr" not in data:
955
        match = re.match("^Local address: ([0-9.]+):([0-9]+)$", line)
956
        if match:
957
          data["local_addr"] = (match.group(1), int(match.group(2)))
958
          continue
959
      if "remote_addr" not in data:
960
        match = re.match("^Remote address: ([0-9.]+):([0-9]+)$", line)
961
        if match:
962
          data["remote_addr"] = (match.group(1), int(match.group(2)))
963
          continue
964
    return data
965

    
966

    
967
  def _MatchesLocal(self, info):
968
    """Test if our local config matches with an existing device.
969

970
    The parameter should be as returned from `_GetDevInfo()`. This
971
    method tests if our local backing device is the same as the one in
972
    the info parameter, in effect testing if we look like the given
973
    device.
974

975
    """
976
    if not ("local_dev" in info and "meta_dev" in info and
977
            "meta_index" in info):
978
      return False
979

    
980
    backend = self._children[0]
981
    if backend is not None:
982
      retval = (info["local_dev"] == (backend.major, backend.minor))
983
    else:
984
      retval = (info["local_dev"] == (0, 0))
985
    meta = self._children[1]
986
    if meta is not None:
987
      retval = retval and (info["meta_dev"] == (meta.major, meta.minor))
988
      retval = retval and (info["meta_index"] == 0)
989
    else:
990
      retval = retval and (info["meta_dev"] == "internal" and
991
                           info["meta_index"] == -1)
992
    return retval
993

    
994

    
995
  def _MatchesNet(self, info):
996
    """Test if our network config matches with an existing device.
997

998
    The parameter should be as returned from `_GetDevInfo()`. This
999
    method tests if our network configuration is the same as the one
1000
    in the info parameter, in effect testing if we look like the given
1001
    device.
1002

1003
    """
1004
    if (((self._lhost is None and not ("local_addr" in info)) and
1005
         (self._rhost is None and not ("remote_addr" in info)))):
1006
      return True
1007

    
1008
    if self._lhost is None:
1009
      return False
1010

    
1011
    if not ("local_addr" in info and
1012
            "remote_addr" in info):
1013
      return False
1014

    
1015
    retval = (info["local_addr"] == (self._lhost, self._lport))
1016
    retval = (retval and
1017
              info["remote_addr"] == (self._rhost, self._rport))
1018
    return retval
1019

    
1020

    
1021
  @staticmethod
1022
  def _IsValidMeta(meta_device):
1023
    """Check if the given meta device looks like a valid one.
1024

1025
    This currently only check the size, which must be around
1026
    128MiB.
1027

1028
    """
1029
    result = utils.RunCmd(["blockdev", "--getsize", meta_device])
1030
    if result.failed:
1031
      logger.Error("Failed to get device size: %s" % result.fail_reason)
1032
      return False
1033
    try:
1034
      sectors = int(result.stdout)
1035
    except ValueError:
1036
      logger.Error("Invalid output from blockdev: '%s'" % result.stdout)
1037
      return False
1038
    bytes = sectors * 512
1039
    if bytes < 128*1024*1024: # less than 128MiB
1040
      logger.Error("Meta device too small (%.2fMib)" % (bytes/1024/1024))
1041
      return False
1042
    if bytes > (128+32)*1024*1024: # account for an extra (big) PE on LVM
1043
      logger.Error("Meta device too big (%.2fMiB)" % (bytes/1024/1024))
1044
      return False
1045
    return True
1046

    
1047

    
1048
  @classmethod
1049
  def _AssembleLocal(cls, minor, backend, meta):
1050
    """Configure the local part of a DRBD device.
1051

1052
    This is the first thing that must be done on an unconfigured DRBD
1053
    device. And it must be done only once.
1054

1055
    """
1056
    if not cls._IsValidMeta(meta):
1057
      return False
1058
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "disk",
1059
                           backend, meta, "0", "-e", "detach"])
1060
    if result.failed:
1061
      logger.Error("Can't attach local disk: %s" % result.output)
1062
    return not result.failed
1063

    
1064

    
1065
  @classmethod
1066
  def _ShutdownLocal(cls, minor):
1067
    """Detach from the local device.
1068

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

1072
    """
1073
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "detach"])
1074
    if result.failed:
1075
      logger.Error("Can't detach local device: %s" % result.output)
1076
    return not result.failed
1077

    
1078

    
1079
  @staticmethod
1080
  def _ShutdownAll(minor):
1081
    """Deactivate the device.
1082

1083
    This will, of course, fail if the device is in use.
1084

1085
    """
1086
    result = utils.RunCmd(["drbdsetup", DRBDev._DevPath(minor), "down"])
1087
    if result.failed:
1088
      logger.Error("Can't shutdown drbd device: %s" % result.output)
1089
    return not result.failed
1090

    
1091

    
1092
  @classmethod
1093
  def _AssembleNet(cls, minor, net_info, protocol):
1094
    """Configure the network part of the device.
1095

1096
    This operation can be, in theory, done multiple times, but there
1097
    have been cases (in lab testing) in which the network part of the
1098
    device had become stuck and couldn't be shut down because activity
1099
    from the new peer (also stuck) triggered a timer re-init and
1100
    needed remote peer interface shutdown in order to clear. So please
1101
    don't change online the net config.
1102

1103
    """
1104
    lhost, lport, rhost, rport = net_info
1105
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "net",
1106
                           "%s:%s" % (lhost, lport), "%s:%s" % (rhost, rport),
1107
                           protocol])
1108
    if result.failed:
1109
      logger.Error("Can't setup network for dbrd device: %s" %
1110
                   result.fail_reason)
1111
      return False
1112

    
1113
    timeout = time.time() + 10
1114
    ok = False
1115
    while time.time() < timeout:
1116
      info = cls._GetDevInfo(minor)
1117
      if not "local_addr" in info or not "remote_addr" in info:
1118
        time.sleep(1)
1119
        continue
1120
      if (info["local_addr"] != (lhost, lport) or
1121
          info["remote_addr"] != (rhost, rport)):
1122
        time.sleep(1)
1123
        continue
1124
      ok = True
1125
      break
1126
    if not ok:
1127
      logger.Error("Timeout while configuring network")
1128
      return False
1129
    return True
1130

    
1131

    
1132
  @classmethod
1133
  def _ShutdownNet(cls, minor):
1134
    """Disconnect from the remote peer.
1135

1136
    This fails if we don't have a local device.
1137

1138
    """
1139
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "disconnect"])
1140
    logger.Error("Can't shutdown network: %s" % result.output)
1141
    return not result.failed
1142

    
1143

    
1144
  def _SetFromMinor(self, minor):
1145
    """Set our parameters based on the given minor.
1146

1147
    This sets our minor variable and our dev_path.
1148

1149
    """
1150
    if minor is None:
1151
      self.minor = self.dev_path = None
1152
    else:
1153
      self.minor = minor
1154
      self.dev_path = self._DevPath(minor)
1155

    
1156

    
1157
  def Assemble(self):
1158
    """Assemble the drbd.
1159

1160
    Method:
1161
      - if we have a local backing device, we bind to it by:
1162
        - checking the list of used drbd devices
1163
        - check if the local minor use of any of them is our own device
1164
        - if yes, abort?
1165
        - if not, bind
1166
      - if we have a local/remote net info:
1167
        - redo the local backing device step for the remote device
1168
        - check if any drbd device is using the local port,
1169
          if yes abort
1170
        - check if any remote drbd device is using the remote
1171
          port, if yes abort (for now)
1172
        - bind our net port
1173
        - bind the remote net port
1174

1175
    """
1176
    self.Attach()
1177
    if self.minor is not None:
1178
      logger.Info("Already assembled")
1179
      return True
1180

    
1181
    result = super(DRBDev, self).Assemble()
1182
    if not result:
1183
      return result
1184

    
1185
    minor = self._FindUnusedMinor()
1186
    if minor is None:
1187
      raise errors.BlockDeviceError, "Not enough free minors for DRBD!"
1188
    need_localdev_teardown = False
1189
    if self._children[0]:
1190
      result = self._AssembleLocal(minor, self._children[0].dev_path,
1191
                                   self._children[1].dev_path)
1192
      if not result:
1193
        return False
1194
      need_localdev_teardown = True
1195
    if self._lhost and self._lport and self._rhost and self._rport:
1196
      result = self._AssembleNet(minor,
1197
                                 (self._lhost, self._lport,
1198
                                  self._rhost, self._rport),
1199
                                 "C")
1200
      if not result:
1201
        if need_localdev_teardown:
1202
          # we will ignore failures from this
1203
          logger.Error("net setup failed, tearing down local device")
1204
          self._ShutdownAll(minor)
1205
        return False
1206
    self._SetFromMinor(minor)
1207
    return True
1208

    
1209

    
1210
  def Shutdown(self):
1211
    """Shutdown the DRBD device.
1212

1213
    """
1214
    if self.minor is None and not self.Attach():
1215
      logger.Info("DRBD device not attached to a device during Shutdown")
1216
      return True
1217
    if not self._ShutdownAll(self.minor):
1218
      return False
1219
    self.minor = None
1220
    self.dev_path = None
1221
    return True
1222

    
1223

    
1224
  def Attach(self):
1225
    """Find a DRBD device which matches our config and attach to it.
1226

1227
    In case of partially attached (local device matches but no network
1228
    setup), we perform the network attach. If successful, we re-test
1229
    the attach if can return success.
1230

1231
    """
1232
    for minor in self._GetUsedDevs():
1233
      info = self._GetDevInfo(minor)
1234
      match_l = self._MatchesLocal(info)
1235
      match_r = self._MatchesNet(info)
1236
      if match_l and match_r:
1237
        break
1238
      if match_l and not match_r and "local_addr" not in info:
1239
        res_r = self._AssembleNet(minor,
1240
                                  (self._lhost, self._lport,
1241
                                   self._rhost, self._rport),
1242
                                  "C")
1243
        if res_r and self._MatchesNet(self._GetDevInfo(minor)):
1244
          break
1245
    else:
1246
      minor = None
1247

    
1248
    self._SetFromMinor(minor)
1249
    return minor is not None
1250

    
1251

    
1252
  def Open(self, force=False):
1253
    """Make the local state primary.
1254

1255
    If the 'force' parameter is given, the '--do-what-I-say' parameter
1256
    is given. Since this is a pottentialy dangerous operation, the
1257
    force flag should be only given after creation, when it actually
1258
    has to be given.
1259

1260
    """
1261
    if self.minor is None and not self.Attach():
1262
      logger.Error("DRBD cannot attach to a device during open")
1263
      return False
1264
    cmd = ["drbdsetup", self.dev_path, "primary"]
1265
    if force:
1266
      cmd.append("--do-what-I-say")
1267
    result = utils.RunCmd(cmd)
1268
    if result.failed:
1269
      logger.Error("Can't make drbd device primary: %s" % result.output)
1270
      return False
1271
    return True
1272

    
1273

    
1274
  def Close(self):
1275
    """Make the local state secondary.
1276

1277
    This will, of course, fail if the device is in use.
1278

1279
    """
1280
    if self.minor is None and not self.Attach():
1281
      logger.Info("Instance not attached to a device")
1282
      raise errors.BlockDeviceError("Can't find device")
1283
    result = utils.RunCmd(["drbdsetup", self.dev_path, "secondary"])
1284
    if result.failed:
1285
      logger.Error("Can't switch drbd device to secondary: %s" % result.output)
1286
      raise errors.BlockDeviceError("Can't switch drbd device to secondary")
1287

    
1288

    
1289
  def SetSyncSpeed(self, kbytes):
1290
    """Set the speed of the DRBD syncer.
1291

1292
    """
1293
    children_result = super(DRBDev, self).SetSyncSpeed(kbytes)
1294
    if self.minor is None:
1295
      logger.Info("Instance not attached to a device")
1296
      return False
1297
    result = utils.RunCmd(["drbdsetup", self.dev_path, "syncer", "-r", "%d" %
1298
                           kbytes])
1299
    if result.failed:
1300
      logger.Error("Can't change syncer rate: %s " % result.fail_reason)
1301
    return not result.failed and children_result
1302

    
1303

    
1304
  def GetSyncStatus(self):
1305
    """Returns the sync status of the device.
1306

1307
    Returns:
1308
     (sync_percent, estimated_time)
1309

1310
    If sync_percent is None, it means all is ok
1311
    If estimated_time is None, it means we can't esimate
1312
    the time needed, otherwise it's the time left in seconds
1313

1314
    """
1315
    if self.minor is None and not self.Attach():
1316
      raise errors.BlockDeviceError("Can't attach to device in GetSyncStatus")
1317
    proc_info = self._MassageProcData(self._GetProcData())
1318
    if self.minor not in proc_info:
1319
      raise errors.BlockDeviceError("Can't find myself in /proc (minor %d)" %
1320
                                    self.minor)
1321
    line = proc_info[self.minor]
1322
    match = re.match("^.*sync'ed: *([0-9.]+)%.*"
1323
                     " finish: ([0-9]+):([0-9]+):([0-9]+) .*$", line)
1324
    if match:
1325
      sync_percent = float(match.group(1))
1326
      hours = int(match.group(2))
1327
      minutes = int(match.group(3))
1328
      seconds = int(match.group(4))
1329
      est_time = hours * 3600 + minutes * 60 + seconds
1330
    else:
1331
      sync_percent = None
1332
      est_time = None
1333
    match = re.match("^ *[0-9]+: cs:([^ ]+).*$", line)
1334
    if not match:
1335
      raise errors.BlockDeviceError("Can't find my data in /proc (minor %d)" %
1336
                                    self.minor)
1337
    client_state = match.group(1)
1338
    is_degraded = client_state != "Connected"
1339
    return sync_percent, est_time, is_degraded
1340

    
1341

    
1342
  @staticmethod
1343
  def _MassageProcData(data):
1344
    """Transform the output of _GetProdData into a nicer form.
1345

1346
    Returns:
1347
      a dictionary of minor: joined lines from /proc/drbd for that minor
1348

1349
    """
1350
    lmatch = re.compile("^ *([0-9]+):.*$")
1351
    results = {}
1352
    old_minor = old_line = None
1353
    for line in data:
1354
      lresult = lmatch.match(line)
1355
      if lresult is not None:
1356
        if old_minor is not None:
1357
          results[old_minor] = old_line
1358
        old_minor = int(lresult.group(1))
1359
        old_line = line
1360
      else:
1361
        if old_minor is not None:
1362
          old_line += " " + line.strip()
1363
    # add last line
1364
    if old_minor is not None:
1365
      results[old_minor] = old_line
1366
    return results
1367

    
1368

    
1369
  def GetStatus(self):
1370
    """Compute the status of the DRBD device
1371

1372
    Note that DRBD devices don't have the STATUS_EXISTING state.
1373

1374
    """
1375
    if self.minor is None and not self.Attach():
1376
      return self.STATUS_UNKNOWN
1377

    
1378
    data = self._GetProcData()
1379
    match = re.compile("^ *%d: cs:[^ ]+ st:(Primary|Secondary)/.*$" %
1380
                       self.minor)
1381
    for line in data:
1382
      mresult = match.match(line)
1383
      if mresult:
1384
        break
1385
    else:
1386
      logger.Error("Can't find myself!")
1387
      return self.STATUS_UNKNOWN
1388

    
1389
    state = mresult.group(2)
1390
    if state == "Primary":
1391
      result = self.STATUS_ONLINE
1392
    else:
1393
      result = self.STATUS_STANDBY
1394

    
1395
    return result
1396

    
1397

    
1398
  @staticmethod
1399
  def _ZeroDevice(device):
1400
    """Zero a device.
1401

1402
    This writes until we get ENOSPC.
1403

1404
    """
1405
    f = open(device, "w")
1406
    buf = "\0" * 1048576
1407
    try:
1408
      while True:
1409
        f.write(buf)
1410
    except IOError, err:
1411
      if err.errno != errno.ENOSPC:
1412
        raise
1413

    
1414

    
1415
  @classmethod
1416
  def Create(cls, unique_id, children, size):
1417
    """Create a new DRBD device.
1418

1419
    Since DRBD devices are not created per se, just assembled, this
1420
    function just zeroes the meta device.
1421

1422
    """
1423
    if len(children) != 2:
1424
      raise errors.ProgrammerError("Invalid setup for the drbd device")
1425
    meta = children[1]
1426
    meta.Assemble()
1427
    if not meta.Attach():
1428
      raise errors.BlockDeviceError("Can't attach to meta device")
1429
    if not cls._IsValidMeta(meta.dev_path):
1430
      raise errors.BlockDeviceError("Invalid meta device")
1431
    logger.Info("Started zeroing device %s" % meta.dev_path)
1432
    cls._ZeroDevice(meta.dev_path)
1433
    logger.Info("Done zeroing device %s" % meta.dev_path)
1434
    return cls(unique_id, children)
1435

    
1436

    
1437
  def Remove(self):
1438
    """Stub remove for DRBD devices.
1439

1440
    """
1441
    return self.Shutdown()
1442

    
1443

    
1444
DEV_MAP = {
1445
  "lvm": LogicalVolume,
1446
  "md_raid1": MDRaid1,
1447
  "drbd": DRBDev,
1448
  }
1449

    
1450

    
1451
def FindDevice(dev_type, unique_id, children):
1452
  """Search for an existing, assembled device.
1453

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

1457
  """
1458
  if dev_type not in DEV_MAP:
1459
    raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
1460
  device = DEV_MAP[dev_type](unique_id, children)
1461
  if not device.Attach():
1462
    return None
1463
  return  device
1464

    
1465

    
1466
def AttachOrAssemble(dev_type, unique_id, children):
1467
  """Try to attach or assemble an existing device.
1468

1469
  This will attach to an existing assembled device or will assemble
1470
  the device, as needed, to bring it fully up.
1471

1472
  """
1473
  if dev_type not in DEV_MAP:
1474
    raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
1475
  device = DEV_MAP[dev_type](unique_id, children)
1476
  if not device.Attach():
1477
    device.Assemble()
1478
  if not device.Attach():
1479
    raise errors.BlockDeviceError("Can't find a valid block device for"
1480
                                  " %s/%s/%s" %
1481
                                  (dev_type, unique_id, children))
1482
  return device
1483

    
1484

    
1485
def Create(dev_type, unique_id, children, size):
1486
  """Create a device.
1487

1488
  """
1489
  if dev_type not in DEV_MAP:
1490
    raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
1491
  device = DEV_MAP[dev_type].Create(unique_id, children, size)
1492
  return device