Statistics
| Branch: | Tag: | Revision:

root / lib / bdev.py @ 549071a0

History | View | Annotate | Download (64.9 kB)

1
#
2
#
3

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

    
21

    
22
"""Block device abstraction"""
23

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

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

    
37

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

    
41

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

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

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

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

    
59

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

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

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

    
73

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

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

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

    
87

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

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

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

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

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

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

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

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

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

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

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

151
    """
152
    pass
153

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

157
    """
158
    raise NotImplementedError
159

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

163
    """
164
    raise NotImplementedError
165

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

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

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

177
    """
178
    raise NotImplementedError
179

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

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

187
    """
188
    raise NotImplementedError
189

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

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

195
    """
196
    raise NotImplementedError
197

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

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

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

207
    """
208
    raise NotImplementedError
209

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

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

217
    """
218
    raise NotImplementedError
219

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

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

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

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

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

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

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

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

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

251
    @rtype: objects.BlockDevStatus
252

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

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

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

269
    @rtype: objects.BlockDevStatus
270

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

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

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

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

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

    
293
        is_degraded = is_degraded or child_status.is_degraded
294

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

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

    
308

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

312
    Only supported for some device types.
313

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

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

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

323
    """
324
    raise NotImplementedError
325

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

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

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

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

    
348

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

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

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

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

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

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

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

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

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

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

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

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

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

    
453
    return data
454

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

581
    """
582
    pass
583

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

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

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

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

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

603
    @rtype: objects.BlockDevStatus
604

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

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

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

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

624
    """
625
    pass
626

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

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

632
    """
633
    pass
634

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

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

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

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

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

    
661
    return snap_name
662

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

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

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

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

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

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

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

    
704

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

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

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

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

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

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

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

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

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

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

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

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

    
809

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

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

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

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

    
827
  _STATUS_FILE = "/proc/drbd"
828
  _USERMODE_HELPER_FILE = "/sys/module/drbd/parameters/usermode_helper"
829

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

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

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

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

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

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

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

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

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

    
904
    return retval
905

    
906
  @staticmethod
907
  def GetUsermodeHelper(filename=_USERMODE_HELPER_FILE):
908
    """Returns DRBD usermode_helper currently set.
909

910
    """
911
    try:
912
      helper = utils.ReadFile(filename).splitlines()[0]
913
    except EnvironmentError, err:
914
      if err.errno == errno.ENOENT:
915
        _ThrowError("The file %s cannot be opened, check if the module"
916
                    " is loaded (%s)", filename, str(err))
917
      else:
918
        _ThrowError("Can't read DRBD helper file %s: %s", filename, str(err))
919
    if not helper:
920
      _ThrowError("Can't read any data from %s", filename)
921
    return helper
922

    
923
  @staticmethod
924
  def _DevPath(minor):
925
    """Return the path to a drbd device for a given minor.
926

927
    """
928
    return "/dev/drbd%d" % minor
929

    
930
  @classmethod
931
  def GetUsedDevs(cls):
932
    """Compute the list of used DRBD devices.
933

934
    """
935
    data = cls._GetProcData()
936

    
937
    used_devs = {}
938
    for line in data:
939
      match = cls._VALID_LINE_RE.match(line)
940
      if not match:
941
        continue
942
      minor = int(match.group(1))
943
      state = match.group(2)
944
      if state == cls._ST_UNCONFIGURED:
945
        continue
946
      used_devs[minor] = state, line
947

    
948
    return used_devs
949

    
950
  def _SetFromMinor(self, minor):
951
    """Set our parameters based on the given minor.
952

953
    This sets our minor variable and our dev_path.
954

955
    """
956
    if minor is None:
957
      self.minor = self.dev_path = None
958
      self.attached = False
959
    else:
960
      self.minor = minor
961
      self.dev_path = self._DevPath(minor)
962
      self.attached = True
963

    
964
  @staticmethod
965
  def _CheckMetaSize(meta_device):
966
    """Check if the given meta device looks like a valid one.
967

968
    This currently only check the size, which must be around
969
    128MiB.
970

971
    """
972
    result = utils.RunCmd(["blockdev", "--getsize", meta_device])
973
    if result.failed:
974
      _ThrowError("Failed to get device size: %s - %s",
975
                  result.fail_reason, result.output)
976
    try:
977
      sectors = int(result.stdout)
978
    except (TypeError, ValueError):
979
      _ThrowError("Invalid output from blockdev: '%s'", result.stdout)
980
    num_bytes = sectors * 512
981
    if num_bytes < 128 * 1024 * 1024: # less than 128MiB
982
      _ThrowError("Meta device too small (%.2fMib)", (num_bytes / 1024 / 1024))
983
    # the maximum *valid* size of the meta device when living on top
984
    # of LVM is hard to compute: it depends on the number of stripes
985
    # and the PE size; e.g. a 2-stripe, 64MB PE will result in a 128MB
986
    # (normal size), but an eight-stripe 128MB PE will result in a 1GB
987
    # size meta device; as such, we restrict it to 1GB (a little bit
988
    # too generous, but making assumptions about PE size is hard)
989
    if num_bytes > 1024 * 1024 * 1024:
990
      _ThrowError("Meta device too big (%.2fMiB)", (num_bytes / 1024 / 1024))
991

    
992
  def Rename(self, new_id):
993
    """Rename a device.
994

995
    This is not supported for drbd devices.
996

997
    """
998
    raise errors.ProgrammerError("Can't rename a drbd device")
999

    
1000

    
1001
class DRBD8(BaseDRBD):
1002
  """DRBD v8.x block device.
1003

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

1008
  The unique_id for the drbd device is the (local_ip, local_port,
1009
  remote_ip, remote_port) tuple, and it must have two children: the
1010
  data device and the meta_device. The meta device is checked for
1011
  valid size and is zeroed on create.
1012

1013
  """
1014
  _MAX_MINORS = 255
1015
  _PARSE_SHOW = None
1016

    
1017
  # timeout constants
1018
  _NET_RECONFIG_TIMEOUT = 60
1019

    
1020
  def __init__(self, unique_id, children, size):
1021
    if children and children.count(None) > 0:
1022
      children = []
1023
    if len(children) not in (0, 2):
1024
      raise ValueError("Invalid configuration data %s" % str(children))
1025
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 6:
1026
      raise ValueError("Invalid configuration data %s" % str(unique_id))
1027
    (self._lhost, self._lport,
1028
     self._rhost, self._rport,
1029
     self._aminor, self._secret) = unique_id
1030
    if children:
1031
      if not _CanReadDevice(children[1].dev_path):
1032
        logging.info("drbd%s: Ignoring unreadable meta device", self._aminor)
1033
        children = []
1034
    super(DRBD8, self).__init__(unique_id, children, size)
1035
    self.major = self._DRBD_MAJOR
1036
    version = self._GetVersion()
1037
    if version['k_major'] != 8 :
1038
      _ThrowError("Mismatch in DRBD kernel version and requested ganeti"
1039
                  " usage: kernel is %s.%s, ganeti wants 8.x",
1040
                  version['k_major'], version['k_minor'])
1041

    
1042
    if (self._lhost is not None and self._lhost == self._rhost and
1043
        self._lport == self._rport):
1044
      raise ValueError("Invalid configuration data, same local/remote %s" %
1045
                       (unique_id,))
1046
    self.Attach()
1047

    
1048
  @classmethod
1049
  def _InitMeta(cls, minor, dev_path):
1050
    """Initialize a meta device.
1051

1052
    This will not work if the given minor is in use.
1053

1054
    """
1055
    result = utils.RunCmd(["drbdmeta", "--force", cls._DevPath(minor),
1056
                           "v08", dev_path, "0", "create-md"])
1057
    if result.failed:
1058
      _ThrowError("Can't initialize meta device: %s", result.output)
1059

    
1060
  @classmethod
1061
  def _FindUnusedMinor(cls):
1062
    """Find an unused DRBD device.
1063

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

1067
    """
1068
    data = cls._GetProcData()
1069

    
1070
    highest = None
1071
    for line in data:
1072
      match = cls._UNUSED_LINE_RE.match(line)
1073
      if match:
1074
        return int(match.group(1))
1075
      match = cls._VALID_LINE_RE.match(line)
1076
      if match:
1077
        minor = int(match.group(1))
1078
        highest = max(highest, minor)
1079
    if highest is None: # there are no minors in use at all
1080
      return 0
1081
    if highest >= cls._MAX_MINORS:
1082
      logging.error("Error: no free drbd minors!")
1083
      raise errors.BlockDeviceError("Can't find a free DRBD minor")
1084
    return highest + 1
1085

    
1086
  @classmethod
1087
  def _GetShowParser(cls):
1088
    """Return a parser for `drbd show` output.
1089

1090
    This will either create or return an already-create parser for the
1091
    output of the command `drbd show`.
1092

1093
    """
1094
    if cls._PARSE_SHOW is not None:
1095
      return cls._PARSE_SHOW
1096

    
1097
    # pyparsing setup
1098
    lbrace = pyp.Literal("{").suppress()
1099
    rbrace = pyp.Literal("}").suppress()
1100
    lbracket = pyp.Literal("[").suppress()
1101
    rbracket = pyp.Literal("]").suppress()
1102
    semi = pyp.Literal(";").suppress()
1103
    colon = pyp.Literal(":").suppress()
1104
    # this also converts the value to an int
1105
    number = pyp.Word(pyp.nums).setParseAction(lambda s, l, t: int(t[0]))
1106

    
1107
    comment = pyp.Literal ("#") + pyp.Optional(pyp.restOfLine)
1108
    defa = pyp.Literal("_is_default").suppress()
1109
    dbl_quote = pyp.Literal('"').suppress()
1110

    
1111
    keyword = pyp.Word(pyp.alphanums + '-')
1112

    
1113
    # value types
1114
    value = pyp.Word(pyp.alphanums + '_-/.:')
1115
    quoted = dbl_quote + pyp.CharsNotIn('"') + dbl_quote
1116
    ipv4_addr = (pyp.Optional(pyp.Literal("ipv4")).suppress() +
1117
                 pyp.Word(pyp.nums + ".") + colon + number)
1118
    ipv6_addr = (pyp.Optional(pyp.Literal("ipv6")).suppress() +
1119
                 pyp.Optional(lbracket) + pyp.Word(pyp.hexnums + ":") +
1120
                 pyp.Optional(rbracket) + colon + number)
1121
    # meta device, extended syntax
1122
    meta_value = ((value ^ quoted) + lbracket + number + rbracket)
1123
    # device name, extended syntax
1124
    device_value = pyp.Literal("minor").suppress() + number
1125

    
1126
    # a statement
1127
    stmt = (~rbrace + keyword + ~lbrace +
1128
            pyp.Optional(ipv4_addr ^ ipv6_addr ^ value ^ quoted ^ meta_value ^
1129
                         device_value) +
1130
            pyp.Optional(defa) + semi +
1131
            pyp.Optional(pyp.restOfLine).suppress())
1132

    
1133
    # an entire section
1134
    section_name = pyp.Word(pyp.alphas + '_')
1135
    section = section_name + lbrace + pyp.ZeroOrMore(pyp.Group(stmt)) + rbrace
1136

    
1137
    bnf = pyp.ZeroOrMore(pyp.Group(section ^ stmt))
1138
    bnf.ignore(comment)
1139

    
1140
    cls._PARSE_SHOW = bnf
1141

    
1142
    return bnf
1143

    
1144
  @classmethod
1145
  def _GetShowData(cls, minor):
1146
    """Return the `drbdsetup show` data for a minor.
1147

1148
    """
1149
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "show"])
1150
    if result.failed:
1151
      logging.error("Can't display the drbd config: %s - %s",
1152
                    result.fail_reason, result.output)
1153
      return None
1154
    return result.stdout
1155

    
1156
  @classmethod
1157
  def _GetDevInfo(cls, out):
1158
    """Parse details about a given DRBD minor.
1159

1160
    This return, if available, the local backing device (as a path)
1161
    and the local and remote (ip, port) information from a string
1162
    containing the output of the `drbdsetup show` command as returned
1163
    by _GetShowData.
1164

1165
    """
1166
    data = {}
1167
    if not out:
1168
      return data
1169

    
1170
    bnf = cls._GetShowParser()
1171
    # run pyparse
1172

    
1173
    try:
1174
      results = bnf.parseString(out)
1175
    except pyp.ParseException, err:
1176
      _ThrowError("Can't parse drbdsetup show output: %s", str(err))
1177

    
1178
    # and massage the results into our desired format
1179
    for section in results:
1180
      sname = section[0]
1181
      if sname == "_this_host":
1182
        for lst in section[1:]:
1183
          if lst[0] == "disk":
1184
            data["local_dev"] = lst[1]
1185
          elif lst[0] == "meta-disk":
1186
            data["meta_dev"] = lst[1]
1187
            data["meta_index"] = lst[2]
1188
          elif lst[0] == "address":
1189
            data["local_addr"] = tuple(lst[1:])
1190
      elif sname == "_remote_host":
1191
        for lst in section[1:]:
1192
          if lst[0] == "address":
1193
            data["remote_addr"] = tuple(lst[1:])
1194
    return data
1195

    
1196
  def _MatchesLocal(self, info):
1197
    """Test if our local config matches with an existing device.
1198

1199
    The parameter should be as returned from `_GetDevInfo()`. This
1200
    method tests if our local backing device is the same as the one in
1201
    the info parameter, in effect testing if we look like the given
1202
    device.
1203

1204
    """
1205
    if self._children:
1206
      backend, meta = self._children
1207
    else:
1208
      backend = meta = None
1209

    
1210
    if backend is not None:
1211
      retval = ("local_dev" in info and info["local_dev"] == backend.dev_path)
1212
    else:
1213
      retval = ("local_dev" not in info)
1214

    
1215
    if meta is not None:
1216
      retval = retval and ("meta_dev" in info and
1217
                           info["meta_dev"] == meta.dev_path)
1218
      retval = retval and ("meta_index" in info and
1219
                           info["meta_index"] == 0)
1220
    else:
1221
      retval = retval and ("meta_dev" not in info and
1222
                           "meta_index" not in info)
1223
    return retval
1224

    
1225
  def _MatchesNet(self, info):
1226
    """Test if our network config matches with an existing device.
1227

1228
    The parameter should be as returned from `_GetDevInfo()`. This
1229
    method tests if our network configuration is the same as the one
1230
    in the info parameter, in effect testing if we look like the given
1231
    device.
1232

1233
    """
1234
    if (((self._lhost is None and not ("local_addr" in info)) and
1235
         (self._rhost is None and not ("remote_addr" in info)))):
1236
      return True
1237

    
1238
    if self._lhost is None:
1239
      return False
1240

    
1241
    if not ("local_addr" in info and
1242
            "remote_addr" in info):
1243
      return False
1244

    
1245
    retval = (info["local_addr"] == (self._lhost, self._lport))
1246
    retval = (retval and
1247
              info["remote_addr"] == (self._rhost, self._rport))
1248
    return retval
1249

    
1250
  @classmethod
1251
  def _AssembleLocal(cls, minor, backend, meta, size):
1252
    """Configure the local part of a DRBD device.
1253

1254
    """
1255
    args = ["drbdsetup", cls._DevPath(minor), "disk",
1256
            backend, meta, "0",
1257
            "-e", "detach",
1258
            "--create-device"]
1259
    if size:
1260
      args.extend(["-d", "%sm" % size])
1261
    if not constants.DRBD_BARRIERS: # disable barriers, if configured so
1262
      version = cls._GetVersion()
1263
      # various DRBD versions support different disk barrier options;
1264
      # what we aim here is to revert back to the 'drain' method of
1265
      # disk flushes and to disable metadata barriers, in effect going
1266
      # back to pre-8.0.7 behaviour
1267
      vmaj = version['k_major']
1268
      vmin = version['k_minor']
1269
      vrel = version['k_point']
1270
      assert vmaj == 8
1271
      if vmin == 0: # 8.0.x
1272
        if vrel >= 12:
1273
          args.extend(['-i', '-m'])
1274
      elif vmin == 2: # 8.2.x
1275
        if vrel >= 7:
1276
          args.extend(['-i', '-m'])
1277
      elif vmaj >= 3: # 8.3.x or newer
1278
        args.extend(['-i', '-a', 'm'])
1279
    result = utils.RunCmd(args)
1280
    if result.failed:
1281
      _ThrowError("drbd%d: can't attach local disk: %s", minor, result.output)
1282

    
1283
  @classmethod
1284
  def _AssembleNet(cls, minor, net_info, protocol,
1285
                   dual_pri=False, hmac=None, secret=None):
1286
    """Configure the network part of the device.
1287

1288
    """
1289
    lhost, lport, rhost, rport = net_info
1290
    if None in net_info:
1291
      # we don't want network connection and actually want to make
1292
      # sure its shutdown
1293
      cls._ShutdownNet(minor)
1294
      return
1295

    
1296
    # Workaround for a race condition. When DRBD is doing its dance to
1297
    # establish a connection with its peer, it also sends the
1298
    # synchronization speed over the wire. In some cases setting the
1299
    # sync speed only after setting up both sides can race with DRBD
1300
    # connecting, hence we set it here before telling DRBD anything
1301
    # about its peer.
1302
    cls._SetMinorSyncSpeed(minor, constants.SYNC_SPEED)
1303

    
1304
    if utils.IsValidIP6(lhost):
1305
      if not utils.IsValidIP6(rhost):
1306
        _ThrowError("drbd%d: can't connect ip %s to ip %s" %
1307
                    (minor, lhost, rhost))
1308
      family = "ipv6"
1309
    elif utils.IsValidIP4(lhost):
1310
      if not utils.IsValidIP4(rhost):
1311
        _ThrowError("drbd%d: can't connect ip %s to ip %s" %
1312
                    (minor, lhost, rhost))
1313
      family = "ipv4"
1314
    else:
1315
      _ThrowError("drbd%d: Invalid ip %s" % (minor, lhost))
1316

    
1317
    args = ["drbdsetup", cls._DevPath(minor), "net",
1318
            "%s:%s:%s" % (family, lhost, lport),
1319
            "%s:%s:%s" % (family, rhost, rport), protocol,
1320
            "-A", "discard-zero-changes",
1321
            "-B", "consensus",
1322
            "--create-device",
1323
            ]
1324
    if dual_pri:
1325
      args.append("-m")
1326
    if hmac and secret:
1327
      args.extend(["-a", hmac, "-x", secret])
1328
    result = utils.RunCmd(args)
1329
    if result.failed:
1330
      _ThrowError("drbd%d: can't setup network: %s - %s",
1331
                  minor, result.fail_reason, result.output)
1332

    
1333
    def _CheckNetworkConfig():
1334
      info = cls._GetDevInfo(cls._GetShowData(minor))
1335
      if not "local_addr" in info or not "remote_addr" in info:
1336
        raise utils.RetryAgain()
1337

    
1338
      if (info["local_addr"] != (lhost, lport) or
1339
          info["remote_addr"] != (rhost, rport)):
1340
        raise utils.RetryAgain()
1341

    
1342
    try:
1343
      utils.Retry(_CheckNetworkConfig, 1.0, 10.0)
1344
    except utils.RetryTimeout:
1345
      _ThrowError("drbd%d: timeout while configuring network", minor)
1346

    
1347
  def AddChildren(self, devices):
1348
    """Add a disk to the DRBD device.
1349

1350
    """
1351
    if self.minor is None:
1352
      _ThrowError("drbd%d: can't attach to dbrd8 during AddChildren",
1353
                  self._aminor)
1354
    if len(devices) != 2:
1355
      _ThrowError("drbd%d: need two devices for AddChildren", self.minor)
1356
    info = self._GetDevInfo(self._GetShowData(self.minor))
1357
    if "local_dev" in info:
1358
      _ThrowError("drbd%d: already attached to a local disk", self.minor)
1359
    backend, meta = devices
1360
    if backend.dev_path is None or meta.dev_path is None:
1361
      _ThrowError("drbd%d: children not ready during AddChildren", self.minor)
1362
    backend.Open()
1363
    meta.Open()
1364
    self._CheckMetaSize(meta.dev_path)
1365
    self._InitMeta(self._FindUnusedMinor(), meta.dev_path)
1366

    
1367
    self._AssembleLocal(self.minor, backend.dev_path, meta.dev_path, self.size)
1368
    self._children = devices
1369

    
1370
  def RemoveChildren(self, devices):
1371
    """Detach the drbd device from local storage.
1372

1373
    """
1374
    if self.minor is None:
1375
      _ThrowError("drbd%d: can't attach to drbd8 during RemoveChildren",
1376
                  self._aminor)
1377
    # early return if we don't actually have backing storage
1378
    info = self._GetDevInfo(self._GetShowData(self.minor))
1379
    if "local_dev" not in info:
1380
      return
1381
    if len(self._children) != 2:
1382
      _ThrowError("drbd%d: we don't have two children: %s", self.minor,
1383
                  self._children)
1384
    if self._children.count(None) == 2: # we don't actually have children :)
1385
      logging.warning("drbd%d: requested detach while detached", self.minor)
1386
      return
1387
    if len(devices) != 2:
1388
      _ThrowError("drbd%d: we need two children in RemoveChildren", self.minor)
1389
    for child, dev in zip(self._children, devices):
1390
      if dev != child.dev_path:
1391
        _ThrowError("drbd%d: mismatch in local storage (%s != %s) in"
1392
                    " RemoveChildren", self.minor, dev, child.dev_path)
1393

    
1394
    self._ShutdownLocal(self.minor)
1395
    self._children = []
1396

    
1397
  @classmethod
1398
  def _SetMinorSyncSpeed(cls, minor, kbytes):
1399
    """Set the speed of the DRBD syncer.
1400

1401
    This is the low-level implementation.
1402

1403
    @type minor: int
1404
    @param minor: the drbd minor whose settings we change
1405
    @type kbytes: int
1406
    @param kbytes: the speed in kbytes/second
1407
    @rtype: boolean
1408
    @return: the success of the operation
1409

1410
    """
1411
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "syncer",
1412
                           "-r", "%d" % kbytes, "--create-device"])
1413
    if result.failed:
1414
      logging.error("Can't change syncer rate: %s - %s",
1415
                    result.fail_reason, result.output)
1416
    return not result.failed
1417

    
1418
  def SetSyncSpeed(self, kbytes):
1419
    """Set the speed of the DRBD syncer.
1420

1421
    @type kbytes: int
1422
    @param kbytes: the speed in kbytes/second
1423
    @rtype: boolean
1424
    @return: the success of the operation
1425

1426
    """
1427
    if self.minor is None:
1428
      logging.info("Not attached during SetSyncSpeed")
1429
      return False
1430
    children_result = super(DRBD8, self).SetSyncSpeed(kbytes)
1431
    return self._SetMinorSyncSpeed(self.minor, kbytes) and children_result
1432

    
1433
  def GetProcStatus(self):
1434
    """Return device data from /proc.
1435

1436
    """
1437
    if self.minor is None:
1438
      _ThrowError("drbd%d: GetStats() called while not attached", self._aminor)
1439
    proc_info = self._MassageProcData(self._GetProcData())
1440
    if self.minor not in proc_info:
1441
      _ThrowError("drbd%d: can't find myself in /proc", self.minor)
1442
    return DRBD8Status(proc_info[self.minor])
1443

    
1444
  def GetSyncStatus(self):
1445
    """Returns the sync status of the device.
1446

1447

1448
    If sync_percent is None, it means all is ok
1449
    If estimated_time is None, it means we can't estimate
1450
    the time needed, otherwise it's the time left in seconds.
1451

1452

1453
    We set the is_degraded parameter to True on two conditions:
1454
    network not connected or local disk missing.
1455

1456
    We compute the ldisk parameter based on whether we have a local
1457
    disk or not.
1458

1459
    @rtype: objects.BlockDevStatus
1460

1461
    """
1462
    if self.minor is None and not self.Attach():
1463
      _ThrowError("drbd%d: can't Attach() in GetSyncStatus", self._aminor)
1464

    
1465
    stats = self.GetProcStatus()
1466
    is_degraded = not stats.is_connected or not stats.is_disk_uptodate
1467

    
1468
    if stats.is_disk_uptodate:
1469
      ldisk_status = constants.LDS_OKAY
1470
    elif stats.is_diskless:
1471
      ldisk_status = constants.LDS_FAULTY
1472
    else:
1473
      ldisk_status = constants.LDS_UNKNOWN
1474

    
1475
    return objects.BlockDevStatus(dev_path=self.dev_path,
1476
                                  major=self.major,
1477
                                  minor=self.minor,
1478
                                  sync_percent=stats.sync_percent,
1479
                                  estimated_time=stats.est_time,
1480
                                  is_degraded=is_degraded,
1481
                                  ldisk_status=ldisk_status)
1482

    
1483
  def Open(self, force=False):
1484
    """Make the local state primary.
1485

1486
    If the 'force' parameter is given, the '-o' option is passed to
1487
    drbdsetup. Since this is a potentially dangerous operation, the
1488
    force flag should be only given after creation, when it actually
1489
    is mandatory.
1490

1491
    """
1492
    if self.minor is None and not self.Attach():
1493
      logging.error("DRBD cannot attach to a device during open")
1494
      return False
1495
    cmd = ["drbdsetup", self.dev_path, "primary"]
1496
    if force:
1497
      cmd.append("-o")
1498
    result = utils.RunCmd(cmd)
1499
    if result.failed:
1500
      _ThrowError("drbd%d: can't make drbd device primary: %s", self.minor,
1501
                  result.output)
1502

    
1503
  def Close(self):
1504
    """Make the local state secondary.
1505

1506
    This will, of course, fail if the device is in use.
1507

1508
    """
1509
    if self.minor is None and not self.Attach():
1510
      _ThrowError("drbd%d: can't Attach() in Close()", self._aminor)
1511
    result = utils.RunCmd(["drbdsetup", self.dev_path, "secondary"])
1512
    if result.failed:
1513
      _ThrowError("drbd%d: can't switch drbd device to secondary: %s",
1514
                  self.minor, result.output)
1515

    
1516
  def DisconnectNet(self):
1517
    """Removes network configuration.
1518

1519
    This method shutdowns the network side of the device.
1520

1521
    The method will wait up to a hardcoded timeout for the device to
1522
    go into standalone after the 'disconnect' command before
1523
    re-configuring it, as sometimes it takes a while for the
1524
    disconnect to actually propagate and thus we might issue a 'net'
1525
    command while the device is still connected. If the device will
1526
    still be attached to the network and we time out, we raise an
1527
    exception.
1528

1529
    """
1530
    if self.minor is None:
1531
      _ThrowError("drbd%d: disk not attached in re-attach net", self._aminor)
1532

    
1533
    if None in (self._lhost, self._lport, self._rhost, self._rport):
1534
      _ThrowError("drbd%d: DRBD disk missing network info in"
1535
                  " DisconnectNet()", self.minor)
1536

    
1537
    class _DisconnectStatus:
1538
      def __init__(self, ever_disconnected):
1539
        self.ever_disconnected = ever_disconnected
1540

    
1541
    dstatus = _DisconnectStatus(_IgnoreError(self._ShutdownNet, self.minor))
1542

    
1543
    def _WaitForDisconnect():
1544
      if self.GetProcStatus().is_standalone:
1545
        return
1546

    
1547
      # retry the disconnect, it seems possible that due to a well-time
1548
      # disconnect on the peer, my disconnect command might be ignored and
1549
      # forgotten
1550
      dstatus.ever_disconnected = \
1551
        _IgnoreError(self._ShutdownNet, self.minor) or dstatus.ever_disconnected
1552

    
1553
      raise utils.RetryAgain()
1554

    
1555
    # Keep start time
1556
    start_time = time.time()
1557

    
1558
    try:
1559
      # Start delay at 100 milliseconds and grow up to 2 seconds
1560
      utils.Retry(_WaitForDisconnect, (0.1, 1.5, 2.0),
1561
                  self._NET_RECONFIG_TIMEOUT)
1562
    except utils.RetryTimeout:
1563
      if dstatus.ever_disconnected:
1564
        msg = ("drbd%d: device did not react to the"
1565
               " 'disconnect' command in a timely manner")
1566
      else:
1567
        msg = "drbd%d: can't shutdown network, even after multiple retries"
1568

    
1569
      _ThrowError(msg, self.minor)
1570

    
1571
    reconfig_time = time.time() - start_time
1572
    if reconfig_time > (self._NET_RECONFIG_TIMEOUT * 0.25):
1573
      logging.info("drbd%d: DisconnectNet: detach took %.3f seconds",
1574
                   self.minor, reconfig_time)
1575

    
1576
  def AttachNet(self, multimaster):
1577
    """Reconnects the network.
1578

1579
    This method connects the network side of the device with a
1580
    specified multi-master flag. The device needs to be 'Standalone'
1581
    but have valid network configuration data.
1582

1583
    Args:
1584
      - multimaster: init the network in dual-primary mode
1585

1586
    """
1587
    if self.minor is None:
1588
      _ThrowError("drbd%d: device not attached in AttachNet", self._aminor)
1589

    
1590
    if None in (self._lhost, self._lport, self._rhost, self._rport):
1591
      _ThrowError("drbd%d: missing network info in AttachNet()", self.minor)
1592

    
1593
    status = self.GetProcStatus()
1594

    
1595
    if not status.is_standalone:
1596
      _ThrowError("drbd%d: device is not standalone in AttachNet", self.minor)
1597

    
1598
    self._AssembleNet(self.minor,
1599
                      (self._lhost, self._lport, self._rhost, self._rport),
1600
                      constants.DRBD_NET_PROTOCOL, dual_pri=multimaster,
1601
                      hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1602

    
1603
  def Attach(self):
1604
    """Check if our minor is configured.
1605

1606
    This doesn't do any device configurations - it only checks if the
1607
    minor is in a state different from Unconfigured.
1608

1609
    Note that this function will not change the state of the system in
1610
    any way (except in case of side-effects caused by reading from
1611
    /proc).
1612

1613
    """
1614
    used_devs = self.GetUsedDevs()
1615
    if self._aminor in used_devs:
1616
      minor = self._aminor
1617
    else:
1618
      minor = None
1619

    
1620
    self._SetFromMinor(minor)
1621
    return minor is not None
1622

    
1623
  def Assemble(self):
1624
    """Assemble the drbd.
1625

1626
    Method:
1627
      - if we have a configured device, we try to ensure that it matches
1628
        our config
1629
      - if not, we create it from zero
1630

1631
    """
1632
    super(DRBD8, self).Assemble()
1633

    
1634
    self.Attach()
1635
    if self.minor is None:
1636
      # local device completely unconfigured
1637
      self._FastAssemble()
1638
    else:
1639
      # we have to recheck the local and network status and try to fix
1640
      # the device
1641
      self._SlowAssemble()
1642

    
1643
  def _SlowAssemble(self):
1644
    """Assembles the DRBD device from a (partially) configured device.
1645

1646
    In case of partially attached (local device matches but no network
1647
    setup), we perform the network attach. If successful, we re-test
1648
    the attach if can return success.
1649

1650
    """
1651
    # TODO: Rewrite to not use a for loop just because there is 'break'
1652
    # pylint: disable-msg=W0631
1653
    net_data = (self._lhost, self._lport, self._rhost, self._rport)
1654
    for minor in (self._aminor,):
1655
      info = self._GetDevInfo(self._GetShowData(minor))
1656
      match_l = self._MatchesLocal(info)
1657
      match_r = self._MatchesNet(info)
1658

    
1659
      if match_l and match_r:
1660
        # everything matches
1661
        break
1662

    
1663
      if match_l and not match_r and "local_addr" not in info:
1664
        # disk matches, but not attached to network, attach and recheck
1665
        self._AssembleNet(minor, net_data, constants.DRBD_NET_PROTOCOL,
1666
                          hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1667
        if self._MatchesNet(self._GetDevInfo(self._GetShowData(minor))):
1668
          break
1669
        else:
1670
          _ThrowError("drbd%d: network attach successful, but 'drbdsetup"
1671
                      " show' disagrees", minor)
1672

    
1673
      if match_r and "local_dev" not in info:
1674
        # no local disk, but network attached and it matches
1675
        self._AssembleLocal(minor, self._children[0].dev_path,
1676
                            self._children[1].dev_path, self.size)
1677
        if self._MatchesNet(self._GetDevInfo(self._GetShowData(minor))):
1678
          break
1679
        else:
1680
          _ThrowError("drbd%d: disk attach successful, but 'drbdsetup"
1681
                      " show' disagrees", minor)
1682

    
1683
      # this case must be considered only if we actually have local
1684
      # storage, i.e. not in diskless mode, because all diskless
1685
      # devices are equal from the point of view of local
1686
      # configuration
1687
      if (match_l and "local_dev" in info and
1688
          not match_r and "local_addr" in info):
1689
        # strange case - the device network part points to somewhere
1690
        # else, even though its local storage is ours; as we own the
1691
        # drbd space, we try to disconnect from the remote peer and
1692
        # reconnect to our correct one
1693
        try:
1694
          self._ShutdownNet(minor)
1695
        except errors.BlockDeviceError, err:
1696
          _ThrowError("drbd%d: device has correct local storage, wrong"
1697
                      " remote peer and is unable to disconnect in order"
1698
                      " to attach to the correct peer: %s", minor, str(err))
1699
        # note: _AssembleNet also handles the case when we don't want
1700
        # local storage (i.e. one or more of the _[lr](host|port) is
1701
        # None)
1702
        self._AssembleNet(minor, net_data, constants.DRBD_NET_PROTOCOL,
1703
                          hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1704
        if self._MatchesNet(self._GetDevInfo(self._GetShowData(minor))):
1705
          break
1706
        else:
1707
          _ThrowError("drbd%d: network attach successful, but 'drbdsetup"
1708
                      " show' disagrees", minor)
1709

    
1710
    else:
1711
      minor = None
1712

    
1713
    self._SetFromMinor(minor)
1714
    if minor is None:
1715
      _ThrowError("drbd%d: cannot activate, unknown or unhandled reason",
1716
                  self._aminor)
1717

    
1718
  def _FastAssemble(self):
1719
    """Assemble the drbd device from zero.
1720

1721
    This is run when in Assemble we detect our minor is unused.
1722

1723
    """
1724
    minor = self._aminor
1725
    if self._children and self._children[0] and self._children[1]:
1726
      self._AssembleLocal(minor, self._children[0].dev_path,
1727
                          self._children[1].dev_path, self.size)
1728
    if self._lhost and self._lport and self._rhost and self._rport:
1729
      self._AssembleNet(minor,
1730
                        (self._lhost, self._lport, self._rhost, self._rport),
1731
                        constants.DRBD_NET_PROTOCOL,
1732
                        hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1733
    self._SetFromMinor(minor)
1734

    
1735
  @classmethod
1736
  def _ShutdownLocal(cls, minor):
1737
    """Detach from the local device.
1738

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

1742
    """
1743
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "detach"])
1744
    if result.failed:
1745
      _ThrowError("drbd%d: can't detach local disk: %s", minor, result.output)
1746

    
1747
  @classmethod
1748
  def _ShutdownNet(cls, minor):
1749
    """Disconnect from the remote peer.
1750

1751
    This fails if we don't have a local device.
1752

1753
    """
1754
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "disconnect"])
1755
    if result.failed:
1756
      _ThrowError("drbd%d: can't shutdown network: %s", minor, result.output)
1757

    
1758
  @classmethod
1759
  def _ShutdownAll(cls, minor):
1760
    """Deactivate the device.
1761

1762
    This will, of course, fail if the device is in use.
1763

1764
    """
1765
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "down"])
1766
    if result.failed:
1767
      _ThrowError("drbd%d: can't shutdown drbd device: %s",
1768
                  minor, result.output)
1769

    
1770
  def Shutdown(self):
1771
    """Shutdown the DRBD device.
1772

1773
    """
1774
    if self.minor is None and not self.Attach():
1775
      logging.info("drbd%d: not attached during Shutdown()", self._aminor)
1776
      return
1777
    minor = self.minor
1778
    self.minor = None
1779
    self.dev_path = None
1780
    self._ShutdownAll(minor)
1781

    
1782
  def Remove(self):
1783
    """Stub remove for DRBD devices.
1784

1785
    """
1786
    self.Shutdown()
1787

    
1788
  @classmethod
1789
  def Create(cls, unique_id, children, size):
1790
    """Create a new DRBD8 device.
1791

1792
    Since DRBD devices are not created per se, just assembled, this
1793
    function only initializes the metadata.
1794

1795
    """
1796
    if len(children) != 2:
1797
      raise errors.ProgrammerError("Invalid setup for the drbd device")
1798
    # check that the minor is unused
1799
    aminor = unique_id[4]
1800
    proc_info = cls._MassageProcData(cls._GetProcData())
1801
    if aminor in proc_info:
1802
      status = DRBD8Status(proc_info[aminor])
1803
      in_use = status.is_in_use
1804
    else:
1805
      in_use = False
1806
    if in_use:
1807
      _ThrowError("drbd%d: minor is already in use at Create() time", aminor)
1808
    meta = children[1]
1809
    meta.Assemble()
1810
    if not meta.Attach():
1811
      _ThrowError("drbd%d: can't attach to meta device '%s'",
1812
                  aminor, meta)
1813
    cls._CheckMetaSize(meta.dev_path)
1814
    cls._InitMeta(aminor, meta.dev_path)
1815
    return cls(unique_id, children, size)
1816

    
1817
  def Grow(self, amount):
1818
    """Resize the DRBD device and its backing storage.
1819

1820
    """
1821
    if self.minor is None:
1822
      _ThrowError("drbd%d: Grow called while not attached", self._aminor)
1823
    if len(self._children) != 2 or None in self._children:
1824
      _ThrowError("drbd%d: cannot grow diskless device", self.minor)
1825
    self._children[0].Grow(amount)
1826
    result = utils.RunCmd(["drbdsetup", self.dev_path, "resize", "-s",
1827
                           "%dm" % (self.size + amount)])
1828
    if result.failed:
1829
      _ThrowError("drbd%d: resize failed: %s", self.minor, result.output)
1830

    
1831

    
1832
class FileStorage(BlockDev):
1833
  """File device.
1834

1835
  This class represents the a file storage backend device.
1836

1837
  The unique_id for the file device is a (file_driver, file_path) tuple.
1838

1839
  """
1840
  def __init__(self, unique_id, children, size):
1841
    """Initalizes a file device backend.
1842

1843
    """
1844
    if children:
1845
      raise errors.BlockDeviceError("Invalid setup for file device")
1846
    super(FileStorage, self).__init__(unique_id, children, size)
1847
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
1848
      raise ValueError("Invalid configuration data %s" % str(unique_id))
1849
    self.driver = unique_id[0]
1850
    self.dev_path = unique_id[1]
1851
    self.Attach()
1852

    
1853
  def Assemble(self):
1854
    """Assemble the device.
1855

1856
    Checks whether the file device exists, raises BlockDeviceError otherwise.
1857

1858
    """
1859
    if not os.path.exists(self.dev_path):
1860
      _ThrowError("File device '%s' does not exist" % self.dev_path)
1861

    
1862
  def Shutdown(self):
1863
    """Shutdown the device.
1864

1865
    This is a no-op for the file type, as we don't deactivate
1866
    the file on shutdown.
1867

1868
    """
1869
    pass
1870

    
1871
  def Open(self, force=False):
1872
    """Make the device ready for I/O.
1873

1874
    This is a no-op for the file type.
1875

1876
    """
1877
    pass
1878

    
1879
  def Close(self):
1880
    """Notifies that the device will no longer be used for I/O.
1881

1882
    This is a no-op for the file type.
1883

1884
    """
1885
    pass
1886

    
1887
  def Remove(self):
1888
    """Remove the file backing the block device.
1889

1890
    @rtype: boolean
1891
    @return: True if the removal was successful
1892

1893
    """
1894
    try:
1895
      os.remove(self.dev_path)
1896
    except OSError, err:
1897
      if err.errno != errno.ENOENT:
1898
        _ThrowError("Can't remove file '%s': %s", self.dev_path, err)
1899

    
1900
  def Rename(self, new_id):
1901
    """Renames the file.
1902

1903
    """
1904
    # TODO: implement rename for file-based storage
1905
    _ThrowError("Rename is not supported for file-based storage")
1906

    
1907
  def Grow(self, amount):
1908
    """Grow the file
1909

1910
    @param amount: the amount (in mebibytes) to grow with
1911

1912
    """
1913
    # Check that the file exists
1914
    self.Assemble()
1915
    current_size = self.GetActualSize()
1916
    new_size = current_size + amount * 1024 * 1024
1917
    assert new_size > current_size, "Cannot Grow with a negative amount"
1918
    try:
1919
      f = open(self.dev_path, "a+")
1920
      f.truncate(new_size)
1921
      f.close()
1922
    except EnvironmentError, err:
1923
      _ThrowError("Error in file growth: %", str(err))
1924

    
1925
  def Attach(self):
1926
    """Attach to an existing file.
1927

1928
    Check if this file already exists.
1929

1930
    @rtype: boolean
1931
    @return: True if file exists
1932

1933
    """
1934
    self.attached = os.path.exists(self.dev_path)
1935
    return self.attached
1936

    
1937
  def GetActualSize(self):
1938
    """Return the actual disk size.
1939

1940
    @note: the device needs to be active when this is called
1941

1942
    """
1943
    assert self.attached, "BlockDevice not attached in GetActualSize()"
1944
    try:
1945
      st = os.stat(self.dev_path)
1946
      return st.st_size
1947
    except OSError, err:
1948
      _ThrowError("Can't stat %s: %s", self.dev_path, err)
1949

    
1950
  @classmethod
1951
  def Create(cls, unique_id, children, size):
1952
    """Create a new file.
1953

1954
    @param size: the size of file in MiB
1955

1956
    @rtype: L{bdev.FileStorage}
1957
    @return: an instance of FileStorage
1958

1959
    """
1960
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
1961
      raise ValueError("Invalid configuration data %s" % str(unique_id))
1962
    dev_path = unique_id[1]
1963
    try:
1964
      fd = os.open(dev_path, os.O_RDWR | os.O_CREAT | os.O_EXCL)
1965
      f = os.fdopen(fd, "w")
1966
      f.truncate(size * 1024 * 1024)
1967
      f.close()
1968
    except EnvironmentError, err:
1969
      if err.errno == errno.EEXIST:
1970
        _ThrowError("File already existing: %s", dev_path)
1971
      _ThrowError("Error in file creation: %", str(err))
1972

    
1973
    return FileStorage(unique_id, children, size)
1974

    
1975

    
1976
DEV_MAP = {
1977
  constants.LD_LV: LogicalVolume,
1978
  constants.LD_DRBD8: DRBD8,
1979
  }
1980

    
1981
if constants.ENABLE_FILE_STORAGE:
1982
  DEV_MAP[constants.LD_FILE] = FileStorage
1983

    
1984

    
1985
def FindDevice(dev_type, unique_id, children, size):
1986
  """Search for an existing, assembled device.
1987

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

1991
  """
1992
  if dev_type not in DEV_MAP:
1993
    raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
1994
  device = DEV_MAP[dev_type](unique_id, children, size)
1995
  if not device.attached:
1996
    return None
1997
  return device
1998

    
1999

    
2000
def Assemble(dev_type, unique_id, children, size):
2001
  """Try to attach or assemble an existing device.
2002

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

2006
  """
2007
  if dev_type not in DEV_MAP:
2008
    raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
2009
  device = DEV_MAP[dev_type](unique_id, children, size)
2010
  device.Assemble()
2011
  return device
2012

    
2013

    
2014
def Create(dev_type, unique_id, children, size):
2015
  """Create a device.
2016

2017
  """
2018
  if dev_type not in DEV_MAP:
2019
    raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
2020
  device = DEV_MAP[dev_type].Create(unique_id, children, size)
2021
  return device