Statistics
| Branch: | Tag: | Revision:

root / lib / bdev.py @ 800ac399

History | View | Annotate | Download (66.7 kB)

1
#
2
#
3

    
4
# Copyright (C) 2006, 2007, 2010 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
from ganeti import netutils
37

    
38

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

    
42

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

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

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

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

    
60

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

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

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

    
74

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

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

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

    
88

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

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

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

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

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

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

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

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

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

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

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

152
    """
153
    pass
154

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

158
    """
159
    raise NotImplementedError
160

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

164
    """
165
    raise NotImplementedError
166

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

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

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

178
    """
179
    raise NotImplementedError
180

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

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

188
    """
189
    raise NotImplementedError
190

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

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

196
    """
197
    raise NotImplementedError
198

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

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

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

208
    """
209
    raise NotImplementedError
210

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

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

218
    """
219
    raise NotImplementedError
220

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

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

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

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

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

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

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

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

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

252
    @rtype: objects.BlockDevStatus
253

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

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

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

270
    @rtype: objects.BlockDevStatus
271

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

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

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

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

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

    
294
        is_degraded = is_degraded or child_status.is_degraded
295

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

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

    
309

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

313
    Only supported for some device types.
314

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

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

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

324
    """
325
    raise NotImplementedError
326

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

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

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

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

    
349

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

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

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

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

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

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

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

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

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

    
420
  @staticmethod
421
  def _GetVolumeInfo(lvm_cmd, fields):
422
    """Returns LVM Volumen infos using lvm_cmd
423

424
    @param lvm_cmd: Should be one of "pvs", "vgs" or "lvs"
425
    @param fields: Fields to return
426
    @return: A list of dicts each with the parsed fields
427

428
    """
429
    if not fields:
430
      raise errors.ProgrammerError("No fields specified")
431

    
432
    sep = "|"
433
    cmd = [lvm_cmd, "--noheadings", "--nosuffix", "--units=m", "--unbuffered",
434
           "--separator=%s" % sep, "-o%s" % ",".join(fields)]
435

    
436
    result = utils.RunCmd(cmd)
437
    if result.failed:
438
      raise errors.CommandError("Can't get the volume information: %s - %s" %
439
                                (result.fail_reason, result.output))
440

    
441
    data = []
442
    for line in result.stdout.splitlines():
443
      splitted_fields = line.strip().split(sep)
444

    
445
      if len(fields) != len(splitted_fields):
446
        raise errors.CommandError("Can't parse %s output: line '%s'" %
447
                                  (lvm_cmd, line))
448

    
449
      data.append(splitted_fields)
450

    
451
    return data
452

    
453
  @classmethod
454
  def GetPVInfo(cls, vg_names, filter_allocatable=True):
455
    """Get the free space info for PVs in a volume group.
456

457
    @param vg_names: list of volume group names, if empty all will be returned
458
    @param filter_allocatable: whether to skip over unallocatable PVs
459

460
    @rtype: list
461
    @return: list of tuples (free_space, name) with free_space in mebibytes
462

463
    """
464
    try:
465
      info = cls._GetVolumeInfo("pvs", ["pv_name", "vg_name", "pv_free",
466
                                        "pv_attr"])
467
    except errors.GenericError, err:
468
      logging.error("Can't get PV information: %s", err)
469
      return None
470

    
471
    data = []
472
    for pv_name, vg_name, pv_free, pv_attr in info:
473
      # (possibly) skip over pvs which are not allocatable
474
      if filter_allocatable and pv_attr[0] != "a":
475
        continue
476
      # (possibly) skip over pvs which are not in the right volume group(s)
477
      if vg_names and vg_name not in vg_names:
478
        continue
479
      data.append((float(pv_free), pv_name, vg_name))
480

    
481
    return data
482

    
483
  @classmethod
484
  def GetVGInfo(cls, vg_names, filter_readonly=True):
485
    """Get the free space info for specific VGs.
486

487
    @param vg_names: list of volume group names, if empty all will be returned
488
    @param filter_readonly: whether to skip over readonly VGs
489

490
    @rtype: list
491
    @return: list of tuples (free_space, total_size, name) with free_space in
492
             MiB
493

494
    """
495
    try:
496
      info = cls._GetVolumeInfo("vgs", ["vg_name", "vg_free", "vg_attr",
497
                                        "vg_size"])
498
    except errors.GenericError, err:
499
      logging.error("Can't get VG information: %s", err)
500
      return None
501

    
502
    data = []
503
    for vg_name, vg_free, vg_attr, vg_size in info:
504
      # (possibly) skip over vgs which are not writable
505
      if filter_readonly and vg_attr[0] == "r":
506
        continue
507
      # (possibly) skip over vgs which are not in the right volume group(s)
508
      if vg_names and vg_name not in vg_names:
509
        continue
510
      data.append((float(vg_free), float(vg_size), vg_name))
511

    
512
    return data
513

    
514
  @classmethod
515
  def _ValidateName(cls, name):
516
    """Validates that a given name is valid as VG or LV name.
517

518
    The list of valid characters and restricted names is taken out of
519
    the lvm(8) manpage, with the simplification that we enforce both
520
    VG and LV restrictions on the names.
521

522
    """
523
    if (not cls._VALID_NAME_RE.match(name) or
524
        name in cls._INVALID_NAMES or
525
        compat.any(substring in name for substring in cls._INVALID_SUBSTRINGS)):
526
      _ThrowError("Invalid LVM name '%s'", name)
527

    
528
  def Remove(self):
529
    """Remove this logical volume.
530

531
    """
532
    if not self.minor and not self.Attach():
533
      # the LV does not exist
534
      return
535
    result = utils.RunCmd(["lvremove", "-f", "%s/%s" %
536
                           (self._vg_name, self._lv_name)])
537
    if result.failed:
538
      _ThrowError("Can't lvremove: %s - %s", result.fail_reason, result.output)
539

    
540
  def Rename(self, new_id):
541
    """Rename this logical volume.
542

543
    """
544
    if not isinstance(new_id, (tuple, list)) or len(new_id) != 2:
545
      raise errors.ProgrammerError("Invalid new logical id '%s'" % new_id)
546
    new_vg, new_name = new_id
547
    if new_vg != self._vg_name:
548
      raise errors.ProgrammerError("Can't move a logical volume across"
549
                                   " volume groups (from %s to to %s)" %
550
                                   (self._vg_name, new_vg))
551
    result = utils.RunCmd(["lvrename", new_vg, self._lv_name, new_name])
552
    if result.failed:
553
      _ThrowError("Failed to rename the logical volume: %s", result.output)
554
    self._lv_name = new_name
555
    self.dev_path = utils.PathJoin("/dev", self._vg_name, self._lv_name)
556

    
557
  def Attach(self):
558
    """Attach to an existing LV.
559

560
    This method will try to see if an existing and active LV exists
561
    which matches our name. If so, its major/minor will be
562
    recorded.
563

564
    """
565
    self.attached = False
566
    result = utils.RunCmd(["lvs", "--noheadings", "--separator=,",
567
                           "--units=m", "--nosuffix",
568
                           "-olv_attr,lv_kernel_major,lv_kernel_minor,"
569
                           "vg_extent_size,stripes", self.dev_path])
570
    if result.failed:
571
      logging.error("Can't find LV %s: %s, %s",
572
                    self.dev_path, result.fail_reason, result.output)
573
      return False
574
    # the output can (and will) have multiple lines for multi-segment
575
    # LVs, as the 'stripes' parameter is a segment one, so we take
576
    # only the last entry, which is the one we're interested in; note
577
    # that with LVM2 anyway the 'stripes' value must be constant
578
    # across segments, so this is a no-op actually
579
    out = result.stdout.splitlines()
580
    if not out: # totally empty result? splitlines() returns at least
581
                # one line for any non-empty string
582
      logging.error("Can't parse LVS output, no lines? Got '%s'", str(out))
583
      return False
584
    out = out[-1].strip().rstrip(',')
585
    out = out.split(",")
586
    if len(out) != 5:
587
      logging.error("Can't parse LVS output, len(%s) != 5", str(out))
588
      return False
589

    
590
    status, major, minor, pe_size, stripes = out
591
    if len(status) != 6:
592
      logging.error("lvs lv_attr is not 6 characters (%s)", status)
593
      return False
594

    
595
    try:
596
      major = int(major)
597
      minor = int(minor)
598
    except (TypeError, ValueError), err:
599
      logging.error("lvs major/minor cannot be parsed: %s", str(err))
600

    
601
    try:
602
      pe_size = int(float(pe_size))
603
    except (TypeError, ValueError), err:
604
      logging.error("Can't parse vg extent size: %s", err)
605
      return False
606

    
607
    try:
608
      stripes = int(stripes)
609
    except (TypeError, ValueError), err:
610
      logging.error("Can't parse the number of stripes: %s", err)
611
      return False
612

    
613
    self.major = major
614
    self.minor = minor
615
    self.pe_size = pe_size
616
    self.stripe_count = stripes
617
    self._degraded = status[0] == 'v' # virtual volume, i.e. doesn't backing
618
                                      # storage
619
    self.attached = True
620
    return True
621

    
622
  def Assemble(self):
623
    """Assemble the device.
624

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

629
    """
630
    result = utils.RunCmd(["lvchange", "-ay", self.dev_path])
631
    if result.failed:
632
      _ThrowError("Can't activate lv %s: %s", self.dev_path, result.output)
633

    
634
  def Shutdown(self):
635
    """Shutdown the device.
636

637
    This is a no-op for the LV device type, as we don't deactivate the
638
    volumes on shutdown.
639

640
    """
641
    pass
642

    
643
  def GetSyncStatus(self):
644
    """Returns the sync status of the device.
645

646
    If this device is a mirroring device, this function returns the
647
    status of the mirror.
648

649
    For logical volumes, sync_percent and estimated_time are always
650
    None (no recovery in progress, as we don't handle the mirrored LV
651
    case). The is_degraded parameter is the inverse of the ldisk
652
    parameter.
653

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

660
    The status was already read in Attach, so we just return it.
661

662
    @rtype: objects.BlockDevStatus
663

664
    """
665
    if self._degraded:
666
      ldisk_status = constants.LDS_FAULTY
667
    else:
668
      ldisk_status = constants.LDS_OKAY
669

    
670
    return objects.BlockDevStatus(dev_path=self.dev_path,
671
                                  major=self.major,
672
                                  minor=self.minor,
673
                                  sync_percent=None,
674
                                  estimated_time=None,
675
                                  is_degraded=self._degraded,
676
                                  ldisk_status=ldisk_status)
677

    
678
  def Open(self, force=False):
679
    """Make the device ready for I/O.
680

681
    This is a no-op for the LV device type.
682

683
    """
684
    pass
685

    
686
  def Close(self):
687
    """Notifies that the device will no longer be used for I/O.
688

689
    This is a no-op for the LV device type.
690

691
    """
692
    pass
693

    
694
  def Snapshot(self, size):
695
    """Create a snapshot copy of an lvm block device.
696

697
    @returns: tuple (vg, lv)
698

699
    """
700
    snap_name = self._lv_name + ".snap"
701

    
702
    # remove existing snapshot if found
703
    snap = LogicalVolume((self._vg_name, snap_name), None, size)
704
    _IgnoreError(snap.Remove)
705

    
706
    vg_info = self.GetVGInfo([self._vg_name])
707
    if not vg_info:
708
      _ThrowError("Can't compute VG info for vg %s", self._vg_name)
709
    free_size, _, _ = vg_info[0]
710
    if free_size < size:
711
      _ThrowError("Not enough free space: required %s,"
712
                  " available %s", size, free_size)
713

    
714
    result = utils.RunCmd(["lvcreate", "-L%dm" % size, "-s",
715
                           "-n%s" % snap_name, self.dev_path])
716
    if result.failed:
717
      _ThrowError("command: %s error: %s - %s",
718
                  result.cmd, result.fail_reason, result.output)
719

    
720
    return (self._vg_name, snap_name)
721

    
722
  def SetInfo(self, text):
723
    """Update metadata with info text.
724

725
    """
726
    BlockDev.SetInfo(self, text)
727

    
728
    # Replace invalid characters
729
    text = re.sub('^[^A-Za-z0-9_+.]', '_', text)
730
    text = re.sub('[^-A-Za-z0-9_+.]', '_', text)
731

    
732
    # Only up to 128 characters are allowed
733
    text = text[:128]
734

    
735
    result = utils.RunCmd(["lvchange", "--addtag", text,
736
                           self.dev_path])
737
    if result.failed:
738
      _ThrowError("Command: %s error: %s - %s", result.cmd, result.fail_reason,
739
                  result.output)
740

    
741
  def Grow(self, amount):
742
    """Grow the logical volume.
743

744
    """
745
    if self.pe_size is None or self.stripe_count is None:
746
      if not self.Attach():
747
        _ThrowError("Can't attach to LV during Grow()")
748
    full_stripe_size = self.pe_size * self.stripe_count
749
    rest = amount % full_stripe_size
750
    if rest != 0:
751
      amount += full_stripe_size - rest
752
    # we try multiple algorithms since the 'best' ones might not have
753
    # space available in the right place, but later ones might (since
754
    # they have less constraints); also note that only recent LVM
755
    # supports 'cling'
756
    for alloc_policy in "contiguous", "cling", "normal":
757
      result = utils.RunCmd(["lvextend", "--alloc", alloc_policy,
758
                             "-L", "+%dm" % amount, self.dev_path])
759
      if not result.failed:
760
        return
761
    _ThrowError("Can't grow LV %s: %s", self.dev_path, result.output)
762

    
763

    
764
class DRBD8Status(object):
765
  """A DRBD status representation class.
766

767
  Note that this doesn't support unconfigured devices (cs:Unconfigured).
768

769
  """
770
  UNCONF_RE = re.compile(r"\s*[0-9]+:\s*cs:Unconfigured$")
771
  LINE_RE = re.compile(r"\s*[0-9]+:\s*cs:(\S+)\s+(?:st|ro):([^/]+)/(\S+)"
772
                       "\s+ds:([^/]+)/(\S+)\s+.*$")
773
  SYNC_RE = re.compile(r"^.*\ssync'ed:\s*([0-9.]+)%.*"
774
                       "\sfinish: ([0-9]+):([0-9]+):([0-9]+)\s.*$")
775

    
776
  CS_UNCONFIGURED = "Unconfigured"
777
  CS_STANDALONE = "StandAlone"
778
  CS_WFCONNECTION = "WFConnection"
779
  CS_WFREPORTPARAMS = "WFReportParams"
780
  CS_CONNECTED = "Connected"
781
  CS_STARTINGSYNCS = "StartingSyncS"
782
  CS_STARTINGSYNCT = "StartingSyncT"
783
  CS_WFBITMAPS = "WFBitMapS"
784
  CS_WFBITMAPT = "WFBitMapT"
785
  CS_WFSYNCUUID = "WFSyncUUID"
786
  CS_SYNCSOURCE = "SyncSource"
787
  CS_SYNCTARGET = "SyncTarget"
788
  CS_PAUSEDSYNCS = "PausedSyncS"
789
  CS_PAUSEDSYNCT = "PausedSyncT"
790
  CSET_SYNC = frozenset([
791
    CS_WFREPORTPARAMS,
792
    CS_STARTINGSYNCS,
793
    CS_STARTINGSYNCT,
794
    CS_WFBITMAPS,
795
    CS_WFBITMAPT,
796
    CS_WFSYNCUUID,
797
    CS_SYNCSOURCE,
798
    CS_SYNCTARGET,
799
    CS_PAUSEDSYNCS,
800
    CS_PAUSEDSYNCT,
801
    ])
802

    
803
  DS_DISKLESS = "Diskless"
804
  DS_ATTACHING = "Attaching" # transient state
805
  DS_FAILED = "Failed" # transient state, next: diskless
806
  DS_NEGOTIATING = "Negotiating" # transient state
807
  DS_INCONSISTENT = "Inconsistent" # while syncing or after creation
808
  DS_OUTDATED = "Outdated"
809
  DS_DUNKNOWN = "DUnknown" # shown for peer disk when not connected
810
  DS_CONSISTENT = "Consistent"
811
  DS_UPTODATE = "UpToDate" # normal state
812

    
813
  RO_PRIMARY = "Primary"
814
  RO_SECONDARY = "Secondary"
815
  RO_UNKNOWN = "Unknown"
816

    
817
  def __init__(self, procline):
818
    u = self.UNCONF_RE.match(procline)
819
    if u:
820
      self.cstatus = self.CS_UNCONFIGURED
821
      self.lrole = self.rrole = self.ldisk = self.rdisk = None
822
    else:
823
      m = self.LINE_RE.match(procline)
824
      if not m:
825
        raise errors.BlockDeviceError("Can't parse input data '%s'" % procline)
826
      self.cstatus = m.group(1)
827
      self.lrole = m.group(2)
828
      self.rrole = m.group(3)
829
      self.ldisk = m.group(4)
830
      self.rdisk = m.group(5)
831

    
832
    # end reading of data from the LINE_RE or UNCONF_RE
833

    
834
    self.is_standalone = self.cstatus == self.CS_STANDALONE
835
    self.is_wfconn = self.cstatus == self.CS_WFCONNECTION
836
    self.is_connected = self.cstatus == self.CS_CONNECTED
837
    self.is_primary = self.lrole == self.RO_PRIMARY
838
    self.is_secondary = self.lrole == self.RO_SECONDARY
839
    self.peer_primary = self.rrole == self.RO_PRIMARY
840
    self.peer_secondary = self.rrole == self.RO_SECONDARY
841
    self.both_primary = self.is_primary and self.peer_primary
842
    self.both_secondary = self.is_secondary and self.peer_secondary
843

    
844
    self.is_diskless = self.ldisk == self.DS_DISKLESS
845
    self.is_disk_uptodate = self.ldisk == self.DS_UPTODATE
846

    
847
    self.is_in_resync = self.cstatus in self.CSET_SYNC
848
    self.is_in_use = self.cstatus != self.CS_UNCONFIGURED
849

    
850
    m = self.SYNC_RE.match(procline)
851
    if m:
852
      self.sync_percent = float(m.group(1))
853
      hours = int(m.group(2))
854
      minutes = int(m.group(3))
855
      seconds = int(m.group(4))
856
      self.est_time = hours * 3600 + minutes * 60 + seconds
857
    else:
858
      # we have (in this if branch) no percent information, but if
859
      # we're resyncing we need to 'fake' a sync percent information,
860
      # as this is how cmdlib determines if it makes sense to wait for
861
      # resyncing or not
862
      if self.is_in_resync:
863
        self.sync_percent = 0
864
      else:
865
        self.sync_percent = None
866
      self.est_time = None
867

    
868

    
869
class BaseDRBD(BlockDev): # pylint: disable-msg=W0223
870
  """Base DRBD class.
871

872
  This class contains a few bits of common functionality between the
873
  0.7 and 8.x versions of DRBD.
874

875
  """
876
  _VERSION_RE = re.compile(r"^version: (\d+)\.(\d+)\.(\d+)(?:\.\d+)?"
877
                           r" \(api:(\d+)/proto:(\d+)(?:-(\d+))?\)")
878
  _VALID_LINE_RE = re.compile("^ *([0-9]+): cs:([^ ]+).*$")
879
  _UNUSED_LINE_RE = re.compile("^ *([0-9]+): cs:Unconfigured$")
880

    
881
  _DRBD_MAJOR = 147
882
  _ST_UNCONFIGURED = "Unconfigured"
883
  _ST_WFCONNECTION = "WFConnection"
884
  _ST_CONNECTED = "Connected"
885

    
886
  _STATUS_FILE = "/proc/drbd"
887
  _USERMODE_HELPER_FILE = "/sys/module/drbd/parameters/usermode_helper"
888

    
889
  @staticmethod
890
  def _GetProcData(filename=_STATUS_FILE):
891
    """Return data from /proc/drbd.
892

893
    """
894
    try:
895
      data = utils.ReadFile(filename).splitlines()
896
    except EnvironmentError, err:
897
      if err.errno == errno.ENOENT:
898
        _ThrowError("The file %s cannot be opened, check if the module"
899
                    " is loaded (%s)", filename, str(err))
900
      else:
901
        _ThrowError("Can't read the DRBD proc file %s: %s", filename, str(err))
902
    if not data:
903
      _ThrowError("Can't read any data from %s", filename)
904
    return data
905

    
906
  @classmethod
907
  def _MassageProcData(cls, data):
908
    """Transform the output of _GetProdData into a nicer form.
909

910
    @return: a dictionary of minor: joined lines from /proc/drbd
911
        for that minor
912

913
    """
914
    results = {}
915
    old_minor = old_line = None
916
    for line in data:
917
      if not line: # completely empty lines, as can be returned by drbd8.0+
918
        continue
919
      lresult = cls._VALID_LINE_RE.match(line)
920
      if lresult is not None:
921
        if old_minor is not None:
922
          results[old_minor] = old_line
923
        old_minor = int(lresult.group(1))
924
        old_line = line
925
      else:
926
        if old_minor is not None:
927
          old_line += " " + line.strip()
928
    # add last line
929
    if old_minor is not None:
930
      results[old_minor] = old_line
931
    return results
932

    
933
  @classmethod
934
  def _GetVersion(cls, proc_data):
935
    """Return the DRBD version.
936

937
    This will return a dict with keys:
938
      - k_major
939
      - k_minor
940
      - k_point
941
      - api
942
      - proto
943
      - proto2 (only on drbd > 8.2.X)
944

945
    """
946
    first_line = proc_data[0].strip()
947
    version = cls._VERSION_RE.match(first_line)
948
    if not version:
949
      raise errors.BlockDeviceError("Can't parse DRBD version from '%s'" %
950
                                    first_line)
951

    
952
    values = version.groups()
953
    retval = {'k_major': int(values[0]),
954
              'k_minor': int(values[1]),
955
              'k_point': int(values[2]),
956
              'api': int(values[3]),
957
              'proto': int(values[4]),
958
             }
959
    if values[5] is not None:
960
      retval['proto2'] = values[5]
961

    
962
    return retval
963

    
964
  @staticmethod
965
  def GetUsermodeHelper(filename=_USERMODE_HELPER_FILE):
966
    """Returns DRBD usermode_helper currently set.
967

968
    """
969
    try:
970
      helper = utils.ReadFile(filename).splitlines()[0]
971
    except EnvironmentError, err:
972
      if err.errno == errno.ENOENT:
973
        _ThrowError("The file %s cannot be opened, check if the module"
974
                    " is loaded (%s)", filename, str(err))
975
      else:
976
        _ThrowError("Can't read DRBD helper file %s: %s", filename, str(err))
977
    if not helper:
978
      _ThrowError("Can't read any data from %s", filename)
979
    return helper
980

    
981
  @staticmethod
982
  def _DevPath(minor):
983
    """Return the path to a drbd device for a given minor.
984

985
    """
986
    return "/dev/drbd%d" % minor
987

    
988
  @classmethod
989
  def GetUsedDevs(cls):
990
    """Compute the list of used DRBD devices.
991

992
    """
993
    data = cls._GetProcData()
994

    
995
    used_devs = {}
996
    for line in data:
997
      match = cls._VALID_LINE_RE.match(line)
998
      if not match:
999
        continue
1000
      minor = int(match.group(1))
1001
      state = match.group(2)
1002
      if state == cls._ST_UNCONFIGURED:
1003
        continue
1004
      used_devs[minor] = state, line
1005

    
1006
    return used_devs
1007

    
1008
  def _SetFromMinor(self, minor):
1009
    """Set our parameters based on the given minor.
1010

1011
    This sets our minor variable and our dev_path.
1012

1013
    """
1014
    if minor is None:
1015
      self.minor = self.dev_path = None
1016
      self.attached = False
1017
    else:
1018
      self.minor = minor
1019
      self.dev_path = self._DevPath(minor)
1020
      self.attached = True
1021

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

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

1029
    """
1030
    result = utils.RunCmd(["blockdev", "--getsize", meta_device])
1031
    if result.failed:
1032
      _ThrowError("Failed to get device size: %s - %s",
1033
                  result.fail_reason, result.output)
1034
    try:
1035
      sectors = int(result.stdout)
1036
    except (TypeError, ValueError):
1037
      _ThrowError("Invalid output from blockdev: '%s'", result.stdout)
1038
    num_bytes = sectors * 512
1039
    if num_bytes < 128 * 1024 * 1024: # less than 128MiB
1040
      _ThrowError("Meta device too small (%.2fMib)", (num_bytes / 1024 / 1024))
1041
    # the maximum *valid* size of the meta device when living on top
1042
    # of LVM is hard to compute: it depends on the number of stripes
1043
    # and the PE size; e.g. a 2-stripe, 64MB PE will result in a 128MB
1044
    # (normal size), but an eight-stripe 128MB PE will result in a 1GB
1045
    # size meta device; as such, we restrict it to 1GB (a little bit
1046
    # too generous, but making assumptions about PE size is hard)
1047
    if num_bytes > 1024 * 1024 * 1024:
1048
      _ThrowError("Meta device too big (%.2fMiB)", (num_bytes / 1024 / 1024))
1049

    
1050
  def Rename(self, new_id):
1051
    """Rename a device.
1052

1053
    This is not supported for drbd devices.
1054

1055
    """
1056
    raise errors.ProgrammerError("Can't rename a drbd device")
1057

    
1058

    
1059
class DRBD8(BaseDRBD):
1060
  """DRBD v8.x block device.
1061

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

1066
  The unique_id for the drbd device is the (local_ip, local_port,
1067
  remote_ip, remote_port) tuple, and it must have two children: the
1068
  data device and the meta_device. The meta device is checked for
1069
  valid size and is zeroed on create.
1070

1071
  """
1072
  _MAX_MINORS = 255
1073
  _PARSE_SHOW = None
1074

    
1075
  # timeout constants
1076
  _NET_RECONFIG_TIMEOUT = 60
1077

    
1078
  def __init__(self, unique_id, children, size):
1079
    if children and children.count(None) > 0:
1080
      children = []
1081
    if len(children) not in (0, 2):
1082
      raise ValueError("Invalid configuration data %s" % str(children))
1083
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 6:
1084
      raise ValueError("Invalid configuration data %s" % str(unique_id))
1085
    (self._lhost, self._lport,
1086
     self._rhost, self._rport,
1087
     self._aminor, self._secret) = unique_id
1088
    if children:
1089
      if not _CanReadDevice(children[1].dev_path):
1090
        logging.info("drbd%s: Ignoring unreadable meta device", self._aminor)
1091
        children = []
1092
    super(DRBD8, self).__init__(unique_id, children, size)
1093
    self.major = self._DRBD_MAJOR
1094
    version = self._GetVersion(self._GetProcData())
1095
    if version['k_major'] != 8 :
1096
      _ThrowError("Mismatch in DRBD kernel version and requested ganeti"
1097
                  " usage: kernel is %s.%s, ganeti wants 8.x",
1098
                  version['k_major'], version['k_minor'])
1099

    
1100
    if (self._lhost is not None and self._lhost == self._rhost and
1101
        self._lport == self._rport):
1102
      raise ValueError("Invalid configuration data, same local/remote %s" %
1103
                       (unique_id,))
1104
    self.Attach()
1105

    
1106
  @classmethod
1107
  def _InitMeta(cls, minor, dev_path):
1108
    """Initialize a meta device.
1109

1110
    This will not work if the given minor is in use.
1111

1112
    """
1113
    result = utils.RunCmd(["drbdmeta", "--force", cls._DevPath(minor),
1114
                           "v08", dev_path, "0", "create-md"])
1115
    if result.failed:
1116
      _ThrowError("Can't initialize meta device: %s", result.output)
1117

    
1118
  @classmethod
1119
  def _FindUnusedMinor(cls):
1120
    """Find an unused DRBD device.
1121

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

1125
    """
1126
    data = cls._GetProcData()
1127

    
1128
    highest = None
1129
    for line in data:
1130
      match = cls._UNUSED_LINE_RE.match(line)
1131
      if match:
1132
        return int(match.group(1))
1133
      match = cls._VALID_LINE_RE.match(line)
1134
      if match:
1135
        minor = int(match.group(1))
1136
        highest = max(highest, minor)
1137
    if highest is None: # there are no minors in use at all
1138
      return 0
1139
    if highest >= cls._MAX_MINORS:
1140
      logging.error("Error: no free drbd minors!")
1141
      raise errors.BlockDeviceError("Can't find a free DRBD minor")
1142
    return highest + 1
1143

    
1144
  @classmethod
1145
  def _GetShowParser(cls):
1146
    """Return a parser for `drbd show` output.
1147

1148
    This will either create or return an already-create parser for the
1149
    output of the command `drbd show`.
1150

1151
    """
1152
    if cls._PARSE_SHOW is not None:
1153
      return cls._PARSE_SHOW
1154

    
1155
    # pyparsing setup
1156
    lbrace = pyp.Literal("{").suppress()
1157
    rbrace = pyp.Literal("}").suppress()
1158
    lbracket = pyp.Literal("[").suppress()
1159
    rbracket = pyp.Literal("]").suppress()
1160
    semi = pyp.Literal(";").suppress()
1161
    colon = pyp.Literal(":").suppress()
1162
    # this also converts the value to an int
1163
    number = pyp.Word(pyp.nums).setParseAction(lambda s, l, t: int(t[0]))
1164

    
1165
    comment = pyp.Literal ("#") + pyp.Optional(pyp.restOfLine)
1166
    defa = pyp.Literal("_is_default").suppress()
1167
    dbl_quote = pyp.Literal('"').suppress()
1168

    
1169
    keyword = pyp.Word(pyp.alphanums + '-')
1170

    
1171
    # value types
1172
    value = pyp.Word(pyp.alphanums + '_-/.:')
1173
    quoted = dbl_quote + pyp.CharsNotIn('"') + dbl_quote
1174
    ipv4_addr = (pyp.Optional(pyp.Literal("ipv4")).suppress() +
1175
                 pyp.Word(pyp.nums + ".") + colon + number)
1176
    ipv6_addr = (pyp.Optional(pyp.Literal("ipv6")).suppress() +
1177
                 pyp.Optional(lbracket) + pyp.Word(pyp.hexnums + ":") +
1178
                 pyp.Optional(rbracket) + colon + number)
1179
    # meta device, extended syntax
1180
    meta_value = ((value ^ quoted) + lbracket + number + rbracket)
1181
    # device name, extended syntax
1182
    device_value = pyp.Literal("minor").suppress() + number
1183

    
1184
    # a statement
1185
    stmt = (~rbrace + keyword + ~lbrace +
1186
            pyp.Optional(ipv4_addr ^ ipv6_addr ^ value ^ quoted ^ meta_value ^
1187
                         device_value) +
1188
            pyp.Optional(defa) + semi +
1189
            pyp.Optional(pyp.restOfLine).suppress())
1190

    
1191
    # an entire section
1192
    section_name = pyp.Word(pyp.alphas + '_')
1193
    section = section_name + lbrace + pyp.ZeroOrMore(pyp.Group(stmt)) + rbrace
1194

    
1195
    bnf = pyp.ZeroOrMore(pyp.Group(section ^ stmt))
1196
    bnf.ignore(comment)
1197

    
1198
    cls._PARSE_SHOW = bnf
1199

    
1200
    return bnf
1201

    
1202
  @classmethod
1203
  def _GetShowData(cls, minor):
1204
    """Return the `drbdsetup show` data for a minor.
1205

1206
    """
1207
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "show"])
1208
    if result.failed:
1209
      logging.error("Can't display the drbd config: %s - %s",
1210
                    result.fail_reason, result.output)
1211
      return None
1212
    return result.stdout
1213

    
1214
  @classmethod
1215
  def _GetDevInfo(cls, out):
1216
    """Parse details about a given DRBD minor.
1217

1218
    This return, if available, the local backing device (as a path)
1219
    and the local and remote (ip, port) information from a string
1220
    containing the output of the `drbdsetup show` command as returned
1221
    by _GetShowData.
1222

1223
    """
1224
    data = {}
1225
    if not out:
1226
      return data
1227

    
1228
    bnf = cls._GetShowParser()
1229
    # run pyparse
1230

    
1231
    try:
1232
      results = bnf.parseString(out)
1233
    except pyp.ParseException, err:
1234
      _ThrowError("Can't parse drbdsetup show output: %s", str(err))
1235

    
1236
    # and massage the results into our desired format
1237
    for section in results:
1238
      sname = section[0]
1239
      if sname == "_this_host":
1240
        for lst in section[1:]:
1241
          if lst[0] == "disk":
1242
            data["local_dev"] = lst[1]
1243
          elif lst[0] == "meta-disk":
1244
            data["meta_dev"] = lst[1]
1245
            data["meta_index"] = lst[2]
1246
          elif lst[0] == "address":
1247
            data["local_addr"] = tuple(lst[1:])
1248
      elif sname == "_remote_host":
1249
        for lst in section[1:]:
1250
          if lst[0] == "address":
1251
            data["remote_addr"] = tuple(lst[1:])
1252
    return data
1253

    
1254
  def _MatchesLocal(self, info):
1255
    """Test if our local config matches with an existing device.
1256

1257
    The parameter should be as returned from `_GetDevInfo()`. This
1258
    method tests if our local backing device is the same as the one in
1259
    the info parameter, in effect testing if we look like the given
1260
    device.
1261

1262
    """
1263
    if self._children:
1264
      backend, meta = self._children
1265
    else:
1266
      backend = meta = None
1267

    
1268
    if backend is not None:
1269
      retval = ("local_dev" in info and info["local_dev"] == backend.dev_path)
1270
    else:
1271
      retval = ("local_dev" not in info)
1272

    
1273
    if meta is not None:
1274
      retval = retval and ("meta_dev" in info and
1275
                           info["meta_dev"] == meta.dev_path)
1276
      retval = retval and ("meta_index" in info and
1277
                           info["meta_index"] == 0)
1278
    else:
1279
      retval = retval and ("meta_dev" not in info and
1280
                           "meta_index" not in info)
1281
    return retval
1282

    
1283
  def _MatchesNet(self, info):
1284
    """Test if our network config matches with an existing device.
1285

1286
    The parameter should be as returned from `_GetDevInfo()`. This
1287
    method tests if our network configuration is the same as the one
1288
    in the info parameter, in effect testing if we look like the given
1289
    device.
1290

1291
    """
1292
    if (((self._lhost is None and not ("local_addr" in info)) and
1293
         (self._rhost is None and not ("remote_addr" in info)))):
1294
      return True
1295

    
1296
    if self._lhost is None:
1297
      return False
1298

    
1299
    if not ("local_addr" in info and
1300
            "remote_addr" in info):
1301
      return False
1302

    
1303
    retval = (info["local_addr"] == (self._lhost, self._lport))
1304
    retval = (retval and
1305
              info["remote_addr"] == (self._rhost, self._rport))
1306
    return retval
1307

    
1308
  @classmethod
1309
  def _AssembleLocal(cls, minor, backend, meta, size):
1310
    """Configure the local part of a DRBD device.
1311

1312
    """
1313
    args = ["drbdsetup", cls._DevPath(minor), "disk",
1314
            backend, meta, "0",
1315
            "-e", "detach",
1316
            "--create-device"]
1317
    if size:
1318
      args.extend(["-d", "%sm" % size])
1319
    if not constants.DRBD_BARRIERS: # disable barriers, if configured so
1320
      version = cls._GetVersion(cls._GetProcData())
1321
      # various DRBD versions support different disk barrier options;
1322
      # what we aim here is to revert back to the 'drain' method of
1323
      # disk flushes and to disable metadata barriers, in effect going
1324
      # back to pre-8.0.7 behaviour
1325
      vmaj = version['k_major']
1326
      vmin = version['k_minor']
1327
      vrel = version['k_point']
1328
      assert vmaj == 8
1329
      if vmin == 0: # 8.0.x
1330
        if vrel >= 12:
1331
          args.extend(['-i', '-m'])
1332
      elif vmin == 2: # 8.2.x
1333
        if vrel >= 7:
1334
          args.extend(['-i', '-m'])
1335
      elif vmaj >= 3: # 8.3.x or newer
1336
        args.extend(['-i', '-a', 'm'])
1337
    result = utils.RunCmd(args)
1338
    if result.failed:
1339
      _ThrowError("drbd%d: can't attach local disk: %s", minor, result.output)
1340

    
1341
  @classmethod
1342
  def _AssembleNet(cls, minor, net_info, protocol,
1343
                   dual_pri=False, hmac=None, secret=None):
1344
    """Configure the network part of the device.
1345

1346
    """
1347
    lhost, lport, rhost, rport = net_info
1348
    if None in net_info:
1349
      # we don't want network connection and actually want to make
1350
      # sure its shutdown
1351
      cls._ShutdownNet(minor)
1352
      return
1353

    
1354
    # Workaround for a race condition. When DRBD is doing its dance to
1355
    # establish a connection with its peer, it also sends the
1356
    # synchronization speed over the wire. In some cases setting the
1357
    # sync speed only after setting up both sides can race with DRBD
1358
    # connecting, hence we set it here before telling DRBD anything
1359
    # about its peer.
1360
    cls._SetMinorSyncSpeed(minor, constants.SYNC_SPEED)
1361

    
1362
    if netutils.IP6Address.IsValid(lhost):
1363
      if not netutils.IP6Address.IsValid(rhost):
1364
        _ThrowError("drbd%d: can't connect ip %s to ip %s" %
1365
                    (minor, lhost, rhost))
1366
      family = "ipv6"
1367
    elif netutils.IP4Address.IsValid(lhost):
1368
      if not netutils.IP4Address.IsValid(rhost):
1369
        _ThrowError("drbd%d: can't connect ip %s to ip %s" %
1370
                    (minor, lhost, rhost))
1371
      family = "ipv4"
1372
    else:
1373
      _ThrowError("drbd%d: Invalid ip %s" % (minor, lhost))
1374

    
1375
    args = ["drbdsetup", cls._DevPath(minor), "net",
1376
            "%s:%s:%s" % (family, lhost, lport),
1377
            "%s:%s:%s" % (family, rhost, rport), protocol,
1378
            "-A", "discard-zero-changes",
1379
            "-B", "consensus",
1380
            "--create-device",
1381
            ]
1382
    if dual_pri:
1383
      args.append("-m")
1384
    if hmac and secret:
1385
      args.extend(["-a", hmac, "-x", secret])
1386
    result = utils.RunCmd(args)
1387
    if result.failed:
1388
      _ThrowError("drbd%d: can't setup network: %s - %s",
1389
                  minor, result.fail_reason, result.output)
1390

    
1391
    def _CheckNetworkConfig():
1392
      info = cls._GetDevInfo(cls._GetShowData(minor))
1393
      if not "local_addr" in info or not "remote_addr" in info:
1394
        raise utils.RetryAgain()
1395

    
1396
      if (info["local_addr"] != (lhost, lport) or
1397
          info["remote_addr"] != (rhost, rport)):
1398
        raise utils.RetryAgain()
1399

    
1400
    try:
1401
      utils.Retry(_CheckNetworkConfig, 1.0, 10.0)
1402
    except utils.RetryTimeout:
1403
      _ThrowError("drbd%d: timeout while configuring network", minor)
1404

    
1405
  def AddChildren(self, devices):
1406
    """Add a disk to the DRBD device.
1407

1408
    """
1409
    if self.minor is None:
1410
      _ThrowError("drbd%d: can't attach to dbrd8 during AddChildren",
1411
                  self._aminor)
1412
    if len(devices) != 2:
1413
      _ThrowError("drbd%d: need two devices for AddChildren", self.minor)
1414
    info = self._GetDevInfo(self._GetShowData(self.minor))
1415
    if "local_dev" in info:
1416
      _ThrowError("drbd%d: already attached to a local disk", self.minor)
1417
    backend, meta = devices
1418
    if backend.dev_path is None or meta.dev_path is None:
1419
      _ThrowError("drbd%d: children not ready during AddChildren", self.minor)
1420
    backend.Open()
1421
    meta.Open()
1422
    self._CheckMetaSize(meta.dev_path)
1423
    self._InitMeta(self._FindUnusedMinor(), meta.dev_path)
1424

    
1425
    self._AssembleLocal(self.minor, backend.dev_path, meta.dev_path, self.size)
1426
    self._children = devices
1427

    
1428
  def RemoveChildren(self, devices):
1429
    """Detach the drbd device from local storage.
1430

1431
    """
1432
    if self.minor is None:
1433
      _ThrowError("drbd%d: can't attach to drbd8 during RemoveChildren",
1434
                  self._aminor)
1435
    # early return if we don't actually have backing storage
1436
    info = self._GetDevInfo(self._GetShowData(self.minor))
1437
    if "local_dev" not in info:
1438
      return
1439
    if len(self._children) != 2:
1440
      _ThrowError("drbd%d: we don't have two children: %s", self.minor,
1441
                  self._children)
1442
    if self._children.count(None) == 2: # we don't actually have children :)
1443
      logging.warning("drbd%d: requested detach while detached", self.minor)
1444
      return
1445
    if len(devices) != 2:
1446
      _ThrowError("drbd%d: we need two children in RemoveChildren", self.minor)
1447
    for child, dev in zip(self._children, devices):
1448
      if dev != child.dev_path:
1449
        _ThrowError("drbd%d: mismatch in local storage (%s != %s) in"
1450
                    " RemoveChildren", self.minor, dev, child.dev_path)
1451

    
1452
    self._ShutdownLocal(self.minor)
1453
    self._children = []
1454

    
1455
  @classmethod
1456
  def _SetMinorSyncSpeed(cls, minor, kbytes):
1457
    """Set the speed of the DRBD syncer.
1458

1459
    This is the low-level implementation.
1460

1461
    @type minor: int
1462
    @param minor: the drbd minor whose settings we change
1463
    @type kbytes: int
1464
    @param kbytes: the speed in kbytes/second
1465
    @rtype: boolean
1466
    @return: the success of the operation
1467

1468
    """
1469
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "syncer",
1470
                           "-r", "%d" % kbytes, "--create-device"])
1471
    if result.failed:
1472
      logging.error("Can't change syncer rate: %s - %s",
1473
                    result.fail_reason, result.output)
1474
    return not result.failed
1475

    
1476
  def SetSyncSpeed(self, kbytes):
1477
    """Set the speed of the DRBD syncer.
1478

1479
    @type kbytes: int
1480
    @param kbytes: the speed in kbytes/second
1481
    @rtype: boolean
1482
    @return: the success of the operation
1483

1484
    """
1485
    if self.minor is None:
1486
      logging.info("Not attached during SetSyncSpeed")
1487
      return False
1488
    children_result = super(DRBD8, self).SetSyncSpeed(kbytes)
1489
    return self._SetMinorSyncSpeed(self.minor, kbytes) and children_result
1490

    
1491
  def GetProcStatus(self):
1492
    """Return device data from /proc.
1493

1494
    """
1495
    if self.minor is None:
1496
      _ThrowError("drbd%d: GetStats() called while not attached", self._aminor)
1497
    proc_info = self._MassageProcData(self._GetProcData())
1498
    if self.minor not in proc_info:
1499
      _ThrowError("drbd%d: can't find myself in /proc", self.minor)
1500
    return DRBD8Status(proc_info[self.minor])
1501

    
1502
  def GetSyncStatus(self):
1503
    """Returns the sync status of the device.
1504

1505

1506
    If sync_percent is None, it means all is ok
1507
    If estimated_time is None, it means we can't estimate
1508
    the time needed, otherwise it's the time left in seconds.
1509

1510

1511
    We set the is_degraded parameter to True on two conditions:
1512
    network not connected or local disk missing.
1513

1514
    We compute the ldisk parameter based on whether we have a local
1515
    disk or not.
1516

1517
    @rtype: objects.BlockDevStatus
1518

1519
    """
1520
    if self.minor is None and not self.Attach():
1521
      _ThrowError("drbd%d: can't Attach() in GetSyncStatus", self._aminor)
1522

    
1523
    stats = self.GetProcStatus()
1524
    is_degraded = not stats.is_connected or not stats.is_disk_uptodate
1525

    
1526
    if stats.is_disk_uptodate:
1527
      ldisk_status = constants.LDS_OKAY
1528
    elif stats.is_diskless:
1529
      ldisk_status = constants.LDS_FAULTY
1530
    else:
1531
      ldisk_status = constants.LDS_UNKNOWN
1532

    
1533
    return objects.BlockDevStatus(dev_path=self.dev_path,
1534
                                  major=self.major,
1535
                                  minor=self.minor,
1536
                                  sync_percent=stats.sync_percent,
1537
                                  estimated_time=stats.est_time,
1538
                                  is_degraded=is_degraded,
1539
                                  ldisk_status=ldisk_status)
1540

    
1541
  def Open(self, force=False):
1542
    """Make the local state primary.
1543

1544
    If the 'force' parameter is given, the '-o' option is passed to
1545
    drbdsetup. Since this is a potentially dangerous operation, the
1546
    force flag should be only given after creation, when it actually
1547
    is mandatory.
1548

1549
    """
1550
    if self.minor is None and not self.Attach():
1551
      logging.error("DRBD cannot attach to a device during open")
1552
      return False
1553
    cmd = ["drbdsetup", self.dev_path, "primary"]
1554
    if force:
1555
      cmd.append("-o")
1556
    result = utils.RunCmd(cmd)
1557
    if result.failed:
1558
      _ThrowError("drbd%d: can't make drbd device primary: %s", self.minor,
1559
                  result.output)
1560

    
1561
  def Close(self):
1562
    """Make the local state secondary.
1563

1564
    This will, of course, fail if the device is in use.
1565

1566
    """
1567
    if self.minor is None and not self.Attach():
1568
      _ThrowError("drbd%d: can't Attach() in Close()", self._aminor)
1569
    result = utils.RunCmd(["drbdsetup", self.dev_path, "secondary"])
1570
    if result.failed:
1571
      _ThrowError("drbd%d: can't switch drbd device to secondary: %s",
1572
                  self.minor, result.output)
1573

    
1574
  def DisconnectNet(self):
1575
    """Removes network configuration.
1576

1577
    This method shutdowns the network side of the device.
1578

1579
    The method will wait up to a hardcoded timeout for the device to
1580
    go into standalone after the 'disconnect' command before
1581
    re-configuring it, as sometimes it takes a while for the
1582
    disconnect to actually propagate and thus we might issue a 'net'
1583
    command while the device is still connected. If the device will
1584
    still be attached to the network and we time out, we raise an
1585
    exception.
1586

1587
    """
1588
    if self.minor is None:
1589
      _ThrowError("drbd%d: disk not attached in re-attach net", self._aminor)
1590

    
1591
    if None in (self._lhost, self._lport, self._rhost, self._rport):
1592
      _ThrowError("drbd%d: DRBD disk missing network info in"
1593
                  " DisconnectNet()", self.minor)
1594

    
1595
    class _DisconnectStatus:
1596
      def __init__(self, ever_disconnected):
1597
        self.ever_disconnected = ever_disconnected
1598

    
1599
    dstatus = _DisconnectStatus(_IgnoreError(self._ShutdownNet, self.minor))
1600

    
1601
    def _WaitForDisconnect():
1602
      if self.GetProcStatus().is_standalone:
1603
        return
1604

    
1605
      # retry the disconnect, it seems possible that due to a well-time
1606
      # disconnect on the peer, my disconnect command might be ignored and
1607
      # forgotten
1608
      dstatus.ever_disconnected = \
1609
        _IgnoreError(self._ShutdownNet, self.minor) or dstatus.ever_disconnected
1610

    
1611
      raise utils.RetryAgain()
1612

    
1613
    # Keep start time
1614
    start_time = time.time()
1615

    
1616
    try:
1617
      # Start delay at 100 milliseconds and grow up to 2 seconds
1618
      utils.Retry(_WaitForDisconnect, (0.1, 1.5, 2.0),
1619
                  self._NET_RECONFIG_TIMEOUT)
1620
    except utils.RetryTimeout:
1621
      if dstatus.ever_disconnected:
1622
        msg = ("drbd%d: device did not react to the"
1623
               " 'disconnect' command in a timely manner")
1624
      else:
1625
        msg = "drbd%d: can't shutdown network, even after multiple retries"
1626

    
1627
      _ThrowError(msg, self.minor)
1628

    
1629
    reconfig_time = time.time() - start_time
1630
    if reconfig_time > (self._NET_RECONFIG_TIMEOUT * 0.25):
1631
      logging.info("drbd%d: DisconnectNet: detach took %.3f seconds",
1632
                   self.minor, reconfig_time)
1633

    
1634
  def AttachNet(self, multimaster):
1635
    """Reconnects the network.
1636

1637
    This method connects the network side of the device with a
1638
    specified multi-master flag. The device needs to be 'Standalone'
1639
    but have valid network configuration data.
1640

1641
    Args:
1642
      - multimaster: init the network in dual-primary mode
1643

1644
    """
1645
    if self.minor is None:
1646
      _ThrowError("drbd%d: device not attached in AttachNet", self._aminor)
1647

    
1648
    if None in (self._lhost, self._lport, self._rhost, self._rport):
1649
      _ThrowError("drbd%d: missing network info in AttachNet()", self.minor)
1650

    
1651
    status = self.GetProcStatus()
1652

    
1653
    if not status.is_standalone:
1654
      _ThrowError("drbd%d: device is not standalone in AttachNet", self.minor)
1655

    
1656
    self._AssembleNet(self.minor,
1657
                      (self._lhost, self._lport, self._rhost, self._rport),
1658
                      constants.DRBD_NET_PROTOCOL, dual_pri=multimaster,
1659
                      hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1660

    
1661
  def Attach(self):
1662
    """Check if our minor is configured.
1663

1664
    This doesn't do any device configurations - it only checks if the
1665
    minor is in a state different from Unconfigured.
1666

1667
    Note that this function will not change the state of the system in
1668
    any way (except in case of side-effects caused by reading from
1669
    /proc).
1670

1671
    """
1672
    used_devs = self.GetUsedDevs()
1673
    if self._aminor in used_devs:
1674
      minor = self._aminor
1675
    else:
1676
      minor = None
1677

    
1678
    self._SetFromMinor(minor)
1679
    return minor is not None
1680

    
1681
  def Assemble(self):
1682
    """Assemble the drbd.
1683

1684
    Method:
1685
      - if we have a configured device, we try to ensure that it matches
1686
        our config
1687
      - if not, we create it from zero
1688

1689
    """
1690
    super(DRBD8, self).Assemble()
1691

    
1692
    self.Attach()
1693
    if self.minor is None:
1694
      # local device completely unconfigured
1695
      self._FastAssemble()
1696
    else:
1697
      # we have to recheck the local and network status and try to fix
1698
      # the device
1699
      self._SlowAssemble()
1700

    
1701
  def _SlowAssemble(self):
1702
    """Assembles the DRBD device from a (partially) configured device.
1703

1704
    In case of partially attached (local device matches but no network
1705
    setup), we perform the network attach. If successful, we re-test
1706
    the attach if can return success.
1707

1708
    """
1709
    # TODO: Rewrite to not use a for loop just because there is 'break'
1710
    # pylint: disable-msg=W0631
1711
    net_data = (self._lhost, self._lport, self._rhost, self._rport)
1712
    for minor in (self._aminor,):
1713
      info = self._GetDevInfo(self._GetShowData(minor))
1714
      match_l = self._MatchesLocal(info)
1715
      match_r = self._MatchesNet(info)
1716

    
1717
      if match_l and match_r:
1718
        # everything matches
1719
        break
1720

    
1721
      if match_l and not match_r and "local_addr" not in info:
1722
        # disk matches, but not attached to network, attach and recheck
1723
        self._AssembleNet(minor, net_data, constants.DRBD_NET_PROTOCOL,
1724
                          hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1725
        if self._MatchesNet(self._GetDevInfo(self._GetShowData(minor))):
1726
          break
1727
        else:
1728
          _ThrowError("drbd%d: network attach successful, but 'drbdsetup"
1729
                      " show' disagrees", minor)
1730

    
1731
      if match_r and "local_dev" not in info:
1732
        # no local disk, but network attached and it matches
1733
        self._AssembleLocal(minor, self._children[0].dev_path,
1734
                            self._children[1].dev_path, self.size)
1735
        if self._MatchesNet(self._GetDevInfo(self._GetShowData(minor))):
1736
          break
1737
        else:
1738
          _ThrowError("drbd%d: disk attach successful, but 'drbdsetup"
1739
                      " show' disagrees", minor)
1740

    
1741
      # this case must be considered only if we actually have local
1742
      # storage, i.e. not in diskless mode, because all diskless
1743
      # devices are equal from the point of view of local
1744
      # configuration
1745
      if (match_l and "local_dev" in info and
1746
          not match_r and "local_addr" in info):
1747
        # strange case - the device network part points to somewhere
1748
        # else, even though its local storage is ours; as we own the
1749
        # drbd space, we try to disconnect from the remote peer and
1750
        # reconnect to our correct one
1751
        try:
1752
          self._ShutdownNet(minor)
1753
        except errors.BlockDeviceError, err:
1754
          _ThrowError("drbd%d: device has correct local storage, wrong"
1755
                      " remote peer and is unable to disconnect in order"
1756
                      " to attach to the correct peer: %s", minor, str(err))
1757
        # note: _AssembleNet also handles the case when we don't want
1758
        # local storage (i.e. one or more of the _[lr](host|port) is
1759
        # None)
1760
        self._AssembleNet(minor, net_data, constants.DRBD_NET_PROTOCOL,
1761
                          hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1762
        if self._MatchesNet(self._GetDevInfo(self._GetShowData(minor))):
1763
          break
1764
        else:
1765
          _ThrowError("drbd%d: network attach successful, but 'drbdsetup"
1766
                      " show' disagrees", minor)
1767

    
1768
    else:
1769
      minor = None
1770

    
1771
    self._SetFromMinor(minor)
1772
    if minor is None:
1773
      _ThrowError("drbd%d: cannot activate, unknown or unhandled reason",
1774
                  self._aminor)
1775

    
1776
  def _FastAssemble(self):
1777
    """Assemble the drbd device from zero.
1778

1779
    This is run when in Assemble we detect our minor is unused.
1780

1781
    """
1782
    minor = self._aminor
1783
    if self._children and self._children[0] and self._children[1]:
1784
      self._AssembleLocal(minor, self._children[0].dev_path,
1785
                          self._children[1].dev_path, self.size)
1786
    if self._lhost and self._lport and self._rhost and self._rport:
1787
      self._AssembleNet(minor,
1788
                        (self._lhost, self._lport, self._rhost, self._rport),
1789
                        constants.DRBD_NET_PROTOCOL,
1790
                        hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1791
    self._SetFromMinor(minor)
1792

    
1793
  @classmethod
1794
  def _ShutdownLocal(cls, minor):
1795
    """Detach from the local device.
1796

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

1800
    """
1801
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "detach"])
1802
    if result.failed:
1803
      _ThrowError("drbd%d: can't detach local disk: %s", minor, result.output)
1804

    
1805
  @classmethod
1806
  def _ShutdownNet(cls, minor):
1807
    """Disconnect from the remote peer.
1808

1809
    This fails if we don't have a local device.
1810

1811
    """
1812
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "disconnect"])
1813
    if result.failed:
1814
      _ThrowError("drbd%d: can't shutdown network: %s", minor, result.output)
1815

    
1816
  @classmethod
1817
  def _ShutdownAll(cls, minor):
1818
    """Deactivate the device.
1819

1820
    This will, of course, fail if the device is in use.
1821

1822
    """
1823
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "down"])
1824
    if result.failed:
1825
      _ThrowError("drbd%d: can't shutdown drbd device: %s",
1826
                  minor, result.output)
1827

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

1831
    """
1832
    if self.minor is None and not self.Attach():
1833
      logging.info("drbd%d: not attached during Shutdown()", self._aminor)
1834
      return
1835
    minor = self.minor
1836
    self.minor = None
1837
    self.dev_path = None
1838
    self._ShutdownAll(minor)
1839

    
1840
  def Remove(self):
1841
    """Stub remove for DRBD devices.
1842

1843
    """
1844
    self.Shutdown()
1845

    
1846
  @classmethod
1847
  def Create(cls, unique_id, children, size):
1848
    """Create a new DRBD8 device.
1849

1850
    Since DRBD devices are not created per se, just assembled, this
1851
    function only initializes the metadata.
1852

1853
    """
1854
    if len(children) != 2:
1855
      raise errors.ProgrammerError("Invalid setup for the drbd device")
1856
    # check that the minor is unused
1857
    aminor = unique_id[4]
1858
    proc_info = cls._MassageProcData(cls._GetProcData())
1859
    if aminor in proc_info:
1860
      status = DRBD8Status(proc_info[aminor])
1861
      in_use = status.is_in_use
1862
    else:
1863
      in_use = False
1864
    if in_use:
1865
      _ThrowError("drbd%d: minor is already in use at Create() time", aminor)
1866
    meta = children[1]
1867
    meta.Assemble()
1868
    if not meta.Attach():
1869
      _ThrowError("drbd%d: can't attach to meta device '%s'",
1870
                  aminor, meta)
1871
    cls._CheckMetaSize(meta.dev_path)
1872
    cls._InitMeta(aminor, meta.dev_path)
1873
    return cls(unique_id, children, size)
1874

    
1875
  def Grow(self, amount):
1876
    """Resize the DRBD device and its backing storage.
1877

1878
    """
1879
    if self.minor is None:
1880
      _ThrowError("drbd%d: Grow called while not attached", self._aminor)
1881
    if len(self._children) != 2 or None in self._children:
1882
      _ThrowError("drbd%d: cannot grow diskless device", self.minor)
1883
    self._children[0].Grow(amount)
1884
    result = utils.RunCmd(["drbdsetup", self.dev_path, "resize", "-s",
1885
                           "%dm" % (self.size + amount)])
1886
    if result.failed:
1887
      _ThrowError("drbd%d: resize failed: %s", self.minor, result.output)
1888

    
1889

    
1890
class FileStorage(BlockDev):
1891
  """File device.
1892

1893
  This class represents the a file storage backend device.
1894

1895
  The unique_id for the file device is a (file_driver, file_path) tuple.
1896

1897
  """
1898
  def __init__(self, unique_id, children, size):
1899
    """Initalizes a file device backend.
1900

1901
    """
1902
    if children:
1903
      raise errors.BlockDeviceError("Invalid setup for file device")
1904
    super(FileStorage, self).__init__(unique_id, children, size)
1905
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
1906
      raise ValueError("Invalid configuration data %s" % str(unique_id))
1907
    self.driver = unique_id[0]
1908
    self.dev_path = unique_id[1]
1909
    self.Attach()
1910

    
1911
  def Assemble(self):
1912
    """Assemble the device.
1913

1914
    Checks whether the file device exists, raises BlockDeviceError otherwise.
1915

1916
    """
1917
    if not os.path.exists(self.dev_path):
1918
      _ThrowError("File device '%s' does not exist" % self.dev_path)
1919

    
1920
  def Shutdown(self):
1921
    """Shutdown the device.
1922

1923
    This is a no-op for the file type, as we don't deactivate
1924
    the file on shutdown.
1925

1926
    """
1927
    pass
1928

    
1929
  def Open(self, force=False):
1930
    """Make the device ready for I/O.
1931

1932
    This is a no-op for the file type.
1933

1934
    """
1935
    pass
1936

    
1937
  def Close(self):
1938
    """Notifies that the device will no longer be used for I/O.
1939

1940
    This is a no-op for the file type.
1941

1942
    """
1943
    pass
1944

    
1945
  def Remove(self):
1946
    """Remove the file backing the block device.
1947

1948
    @rtype: boolean
1949
    @return: True if the removal was successful
1950

1951
    """
1952
    try:
1953
      os.remove(self.dev_path)
1954
    except OSError, err:
1955
      if err.errno != errno.ENOENT:
1956
        _ThrowError("Can't remove file '%s': %s", self.dev_path, err)
1957

    
1958
  def Rename(self, new_id):
1959
    """Renames the file.
1960

1961
    """
1962
    # TODO: implement rename for file-based storage
1963
    _ThrowError("Rename is not supported for file-based storage")
1964

    
1965
  def Grow(self, amount):
1966
    """Grow the file
1967

1968
    @param amount: the amount (in mebibytes) to grow with
1969

1970
    """
1971
    # Check that the file exists
1972
    self.Assemble()
1973
    current_size = self.GetActualSize()
1974
    new_size = current_size + amount * 1024 * 1024
1975
    assert new_size > current_size, "Cannot Grow with a negative amount"
1976
    try:
1977
      f = open(self.dev_path, "a+")
1978
      f.truncate(new_size)
1979
      f.close()
1980
    except EnvironmentError, err:
1981
      _ThrowError("Error in file growth: %", str(err))
1982

    
1983
  def Attach(self):
1984
    """Attach to an existing file.
1985

1986
    Check if this file already exists.
1987

1988
    @rtype: boolean
1989
    @return: True if file exists
1990

1991
    """
1992
    self.attached = os.path.exists(self.dev_path)
1993
    return self.attached
1994

    
1995
  def GetActualSize(self):
1996
    """Return the actual disk size.
1997

1998
    @note: the device needs to be active when this is called
1999

2000
    """
2001
    assert self.attached, "BlockDevice not attached in GetActualSize()"
2002
    try:
2003
      st = os.stat(self.dev_path)
2004
      return st.st_size
2005
    except OSError, err:
2006
      _ThrowError("Can't stat %s: %s", self.dev_path, err)
2007

    
2008
  @classmethod
2009
  def Create(cls, unique_id, children, size):
2010
    """Create a new file.
2011

2012
    @param size: the size of file in MiB
2013

2014
    @rtype: L{bdev.FileStorage}
2015
    @return: an instance of FileStorage
2016

2017
    """
2018
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
2019
      raise ValueError("Invalid configuration data %s" % str(unique_id))
2020
    dev_path = unique_id[1]
2021
    try:
2022
      fd = os.open(dev_path, os.O_RDWR | os.O_CREAT | os.O_EXCL)
2023
      f = os.fdopen(fd, "w")
2024
      f.truncate(size * 1024 * 1024)
2025
      f.close()
2026
    except EnvironmentError, err:
2027
      if err.errno == errno.EEXIST:
2028
        _ThrowError("File already existing: %s", dev_path)
2029
      _ThrowError("Error in file creation: %", str(err))
2030

    
2031
    return FileStorage(unique_id, children, size)
2032

    
2033

    
2034
DEV_MAP = {
2035
  constants.LD_LV: LogicalVolume,
2036
  constants.LD_DRBD8: DRBD8,
2037
  }
2038

    
2039
if constants.ENABLE_FILE_STORAGE:
2040
  DEV_MAP[constants.LD_FILE] = FileStorage
2041

    
2042

    
2043
def FindDevice(dev_type, unique_id, children, size):
2044
  """Search for an existing, assembled device.
2045

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

2049
  """
2050
  if dev_type not in DEV_MAP:
2051
    raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
2052
  device = DEV_MAP[dev_type](unique_id, children, size)
2053
  if not device.attached:
2054
    return None
2055
  return device
2056

    
2057

    
2058
def Assemble(dev_type, unique_id, children, size):
2059
  """Try to attach or assemble an existing device.
2060

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

2064
  """
2065
  if dev_type not in DEV_MAP:
2066
    raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
2067
  device = DEV_MAP[dev_type](unique_id, children, size)
2068
  device.Assemble()
2069
  return device
2070

    
2071

    
2072
def Create(dev_type, unique_id, children, size):
2073
  """Create a device.
2074

2075
  """
2076
  if dev_type not in DEV_MAP:
2077
    raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
2078
  device = DEV_MAP[dev_type].Create(unique_id, children, size)
2079
  return device