Statistics
| Branch: | Tag: | Revision:

root / lib / bdev.py @ 26d3fd2f

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
    """
698
    snap_name = self._lv_name + ".snap"
699

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

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

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

    
718
    return snap_name
719

    
720
  def SetInfo(self, text):
721
    """Update metadata with info text.
722

723
    """
724
    BlockDev.SetInfo(self, text)
725

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

    
730
    # Only up to 128 characters are allowed
731
    text = text[:128]
732

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

    
739
  def Grow(self, amount):
740
    """Grow the logical volume.
741

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

    
761

    
762
class DRBD8Status(object):
763
  """A DRBD status representation class.
764

765
  Note that this doesn't support unconfigured devices (cs:Unconfigured).
766

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

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

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

    
811
  RO_PRIMARY = "Primary"
812
  RO_SECONDARY = "Secondary"
813
  RO_UNKNOWN = "Unknown"
814

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

    
830
    # end reading of data from the LINE_RE or UNCONF_RE
831

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

    
842
    self.is_diskless = self.ldisk == self.DS_DISKLESS
843
    self.is_disk_uptodate = self.ldisk == self.DS_UPTODATE
844

    
845
    self.is_in_resync = self.cstatus in self.CSET_SYNC
846
    self.is_in_use = self.cstatus != self.CS_UNCONFIGURED
847

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

    
866

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

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

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

    
879
  _DRBD_MAJOR = 147
880
  _ST_UNCONFIGURED = "Unconfigured"
881
  _ST_WFCONNECTION = "WFConnection"
882
  _ST_CONNECTED = "Connected"
883

    
884
  _STATUS_FILE = "/proc/drbd"
885
  _USERMODE_HELPER_FILE = "/sys/module/drbd/parameters/usermode_helper"
886

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

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

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

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

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

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

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

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

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

    
960
    return retval
961

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

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

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

983
    """
984
    return "/dev/drbd%d" % minor
985

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

990
    """
991
    data = cls._GetProcData()
992

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

    
1004
    return used_devs
1005

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

1009
    This sets our minor variable and our dev_path.
1010

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

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

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

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

    
1048
  def Rename(self, new_id):
1049
    """Rename a device.
1050

1051
    This is not supported for drbd devices.
1052

1053
    """
1054
    raise errors.ProgrammerError("Can't rename a drbd device")
1055

    
1056

    
1057
class DRBD8(BaseDRBD):
1058
  """DRBD v8.x block device.
1059

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

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

1069
  """
1070
  _MAX_MINORS = 255
1071
  _PARSE_SHOW = None
1072

    
1073
  # timeout constants
1074
  _NET_RECONFIG_TIMEOUT = 60
1075

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

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

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

1108
    This will not work if the given minor is in use.
1109

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

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

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

1123
    """
1124
    data = cls._GetProcData()
1125

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

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

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

1149
    """
1150
    if cls._PARSE_SHOW is not None:
1151
      return cls._PARSE_SHOW
1152

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

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

    
1167
    keyword = pyp.Word(pyp.alphanums + '-')
1168

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

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

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

    
1193
    bnf = pyp.ZeroOrMore(pyp.Group(section ^ stmt))
1194
    bnf.ignore(comment)
1195

    
1196
    cls._PARSE_SHOW = bnf
1197

    
1198
    return bnf
1199

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

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

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

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

1221
    """
1222
    data = {}
1223
    if not out:
1224
      return data
1225

    
1226
    bnf = cls._GetShowParser()
1227
    # run pyparse
1228

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

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

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

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

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

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

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

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

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

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

    
1294
    if self._lhost is None:
1295
      return False
1296

    
1297
    if not ("local_addr" in info and
1298
            "remote_addr" in info):
1299
      return False
1300

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

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

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

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

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

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

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

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

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

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

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

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

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

    
1423
    self._AssembleLocal(self.minor, backend.dev_path, meta.dev_path, self.size)
1424
    self._children = devices
1425

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

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

    
1450
    self._ShutdownLocal(self.minor)
1451
    self._children = []
1452

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

1457
    This is the low-level implementation.
1458

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

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

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

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

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

    
1489
  def GetProcStatus(self):
1490
    """Return device data from /proc.
1491

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

    
1500
  def GetSyncStatus(self):
1501
    """Returns the sync status of the device.
1502

1503

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

1508

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

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

1515
    @rtype: objects.BlockDevStatus
1516

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

    
1521
    stats = self.GetProcStatus()
1522
    is_degraded = not stats.is_connected or not stats.is_disk_uptodate
1523

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

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

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

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

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

    
1559
  def Close(self):
1560
    """Make the local state secondary.
1561

1562
    This will, of course, fail if the device is in use.
1563

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

    
1572
  def DisconnectNet(self):
1573
    """Removes network configuration.
1574

1575
    This method shutdowns the network side of the device.
1576

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

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

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

    
1593
    class _DisconnectStatus:
1594
      def __init__(self, ever_disconnected):
1595
        self.ever_disconnected = ever_disconnected
1596

    
1597
    dstatus = _DisconnectStatus(_IgnoreError(self._ShutdownNet, self.minor))
1598

    
1599
    def _WaitForDisconnect():
1600
      if self.GetProcStatus().is_standalone:
1601
        return
1602

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

    
1609
      raise utils.RetryAgain()
1610

    
1611
    # Keep start time
1612
    start_time = time.time()
1613

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

    
1625
      _ThrowError(msg, self.minor)
1626

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

    
1632
  def AttachNet(self, multimaster):
1633
    """Reconnects the network.
1634

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

1639
    Args:
1640
      - multimaster: init the network in dual-primary mode
1641

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

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

    
1649
    status = self.GetProcStatus()
1650

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

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

    
1659
  def Attach(self):
1660
    """Check if our minor is configured.
1661

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

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

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

    
1676
    self._SetFromMinor(minor)
1677
    return minor is not None
1678

    
1679
  def Assemble(self):
1680
    """Assemble the drbd.
1681

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

1687
    """
1688
    super(DRBD8, self).Assemble()
1689

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

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

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

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

    
1715
      if match_l and match_r:
1716
        # everything matches
1717
        break
1718

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

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

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

    
1766
    else:
1767
      minor = None
1768

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

    
1774
  def _FastAssemble(self):
1775
    """Assemble the drbd device from zero.
1776

1777
    This is run when in Assemble we detect our minor is unused.
1778

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

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

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

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

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

1807
    This fails if we don't have a local device.
1808

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

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

1818
    This will, of course, fail if the device is in use.
1819

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

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

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

    
1838
  def Remove(self):
1839
    """Stub remove for DRBD devices.
1840

1841
    """
1842
    self.Shutdown()
1843

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

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

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

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

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

    
1887

    
1888
class FileStorage(BlockDev):
1889
  """File device.
1890

1891
  This class represents the a file storage backend device.
1892

1893
  The unique_id for the file device is a (file_driver, file_path) tuple.
1894

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

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

    
1909
  def Assemble(self):
1910
    """Assemble the device.
1911

1912
    Checks whether the file device exists, raises BlockDeviceError otherwise.
1913

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

    
1918
  def Shutdown(self):
1919
    """Shutdown the device.
1920

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

1924
    """
1925
    pass
1926

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

1930
    This is a no-op for the file type.
1931

1932
    """
1933
    pass
1934

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

1938
    This is a no-op for the file type.
1939

1940
    """
1941
    pass
1942

    
1943
  def Remove(self):
1944
    """Remove the file backing the block device.
1945

1946
    @rtype: boolean
1947
    @return: True if the removal was successful
1948

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

    
1956
  def Rename(self, new_id):
1957
    """Renames the file.
1958

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

    
1963
  def Grow(self, amount):
1964
    """Grow the file
1965

1966
    @param amount: the amount (in mebibytes) to grow with
1967

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

    
1981
  def Attach(self):
1982
    """Attach to an existing file.
1983

1984
    Check if this file already exists.
1985

1986
    @rtype: boolean
1987
    @return: True if file exists
1988

1989
    """
1990
    self.attached = os.path.exists(self.dev_path)
1991
    return self.attached
1992

    
1993
  def GetActualSize(self):
1994
    """Return the actual disk size.
1995

1996
    @note: the device needs to be active when this is called
1997

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

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

2010
    @param size: the size of file in MiB
2011

2012
    @rtype: L{bdev.FileStorage}
2013
    @return: an instance of FileStorage
2014

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

    
2029
    return FileStorage(unique_id, children, size)
2030

    
2031

    
2032
DEV_MAP = {
2033
  constants.LD_LV: LogicalVolume,
2034
  constants.LD_DRBD8: DRBD8,
2035
  }
2036

    
2037
if constants.ENABLE_FILE_STORAGE:
2038
  DEV_MAP[constants.LD_FILE] = FileStorage
2039

    
2040

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

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

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

    
2055

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

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

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

    
2069

    
2070
def Create(dev_type, unique_id, children, size):
2071
  """Create a device.
2072

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