Statistics
| Branch: | Tag: | Revision:

root / lib / bdev.py @ 7a380ddf

History | View | Annotate | Download (67.9 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 PauseResumeSync(self, pause):
234
    """Pause/Resume the sync of the mirror.
235

236
    In case this is not a mirroring device, this is no-op.
237

238
    @param pause: Wheater to pause or resume
239

240
    """
241
    result = True
242
    if self._children:
243
      for child in self._children:
244
        result = result and child.PauseResumeSync(pause)
245
    return result
246

    
247
  def GetSyncStatus(self):
248
    """Returns the sync status of the device.
249

250
    If this device is a mirroring device, this function returns the
251
    status of the mirror.
252

253
    If sync_percent is None, it means the device is not syncing.
254

255
    If estimated_time is None, it means we can't estimate
256
    the time needed, otherwise it's the time left in seconds.
257

258
    If is_degraded is True, it means the device is missing
259
    redundancy. This is usually a sign that something went wrong in
260
    the device setup, if sync_percent is None.
261

262
    The ldisk parameter represents the degradation of the local
263
    data. This is only valid for some devices, the rest will always
264
    return False (not degraded).
265

266
    @rtype: objects.BlockDevStatus
267

268
    """
269
    return objects.BlockDevStatus(dev_path=self.dev_path,
270
                                  major=self.major,
271
                                  minor=self.minor,
272
                                  sync_percent=None,
273
                                  estimated_time=None,
274
                                  is_degraded=False,
275
                                  ldisk_status=constants.LDS_OKAY)
276

    
277
  def CombinedSyncStatus(self):
278
    """Calculate the mirror status recursively for our children.
279

280
    The return value is the same as for `GetSyncStatus()` except the
281
    minimum percent and maximum time are calculated across our
282
    children.
283

284
    @rtype: objects.BlockDevStatus
285

286
    """
287
    status = self.GetSyncStatus()
288

    
289
    min_percent = status.sync_percent
290
    max_time = status.estimated_time
291
    is_degraded = status.is_degraded
292
    ldisk_status = status.ldisk_status
293

    
294
    if self._children:
295
      for child in self._children:
296
        child_status = child.GetSyncStatus()
297

    
298
        if min_percent is None:
299
          min_percent = child_status.sync_percent
300
        elif child_status.sync_percent is not None:
301
          min_percent = min(min_percent, child_status.sync_percent)
302

    
303
        if max_time is None:
304
          max_time = child_status.estimated_time
305
        elif child_status.estimated_time is not None:
306
          max_time = max(max_time, child_status.estimated_time)
307

    
308
        is_degraded = is_degraded or child_status.is_degraded
309

    
310
        if ldisk_status is None:
311
          ldisk_status = child_status.ldisk_status
312
        elif child_status.ldisk_status is not None:
313
          ldisk_status = max(ldisk_status, child_status.ldisk_status)
314

    
315
    return objects.BlockDevStatus(dev_path=self.dev_path,
316
                                  major=self.major,
317
                                  minor=self.minor,
318
                                  sync_percent=min_percent,
319
                                  estimated_time=max_time,
320
                                  is_degraded=is_degraded,
321
                                  ldisk_status=ldisk_status)
322

    
323

    
324
  def SetInfo(self, text):
325
    """Update metadata with info text.
326

327
    Only supported for some device types.
328

329
    """
330
    for child in self._children:
331
      child.SetInfo(text)
332

    
333
  def Grow(self, amount):
334
    """Grow the block device.
335

336
    @param amount: the amount (in mebibytes) to grow with
337

338
    """
339
    raise NotImplementedError
340

    
341
  def GetActualSize(self):
342
    """Return the actual disk size.
343

344
    @note: the device needs to be active when this is called
345

346
    """
347
    assert self.attached, "BlockDevice not attached in GetActualSize()"
348
    result = utils.RunCmd(["blockdev", "--getsize64", self.dev_path])
349
    if result.failed:
350
      _ThrowError("blockdev failed (%s): %s",
351
                  result.fail_reason, result.output)
352
    try:
353
      sz = int(result.output.strip())
354
    except (ValueError, TypeError), err:
355
      _ThrowError("Failed to parse blockdev output: %s", str(err))
356
    return sz
357

    
358
  def __repr__(self):
359
    return ("<%s: unique_id: %s, children: %s, %s:%s, %s>" %
360
            (self.__class__, self.unique_id, self._children,
361
             self.major, self.minor, self.dev_path))
362

    
363

    
364
class LogicalVolume(BlockDev):
365
  """Logical Volume block device.
366

367
  """
368
  _VALID_NAME_RE = re.compile("^[a-zA-Z0-9+_.-]*$")
369
  _INVALID_NAMES = frozenset([".", "..", "snapshot", "pvmove"])
370
  _INVALID_SUBSTRINGS = frozenset(["_mlog", "_mimage"])
371

    
372
  def __init__(self, unique_id, children, size):
373
    """Attaches to a LV device.
374

375
    The unique_id is a tuple (vg_name, lv_name)
376

377
    """
378
    super(LogicalVolume, self).__init__(unique_id, children, size)
379
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
380
      raise ValueError("Invalid configuration data %s" % str(unique_id))
381
    self._vg_name, self._lv_name = unique_id
382
    self._ValidateName(self._vg_name)
383
    self._ValidateName(self._lv_name)
384
    self.dev_path = utils.PathJoin("/dev", self._vg_name, self._lv_name)
385
    self._degraded = True
386
    self.major = self.minor = self.pe_size = self.stripe_count = None
387
    self.Attach()
388

    
389
  @classmethod
390
  def Create(cls, unique_id, children, size):
391
    """Create a new logical volume.
392

393
    """
394
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
395
      raise errors.ProgrammerError("Invalid configuration data %s" %
396
                                   str(unique_id))
397
    vg_name, lv_name = unique_id
398
    cls._ValidateName(vg_name)
399
    cls._ValidateName(lv_name)
400
    pvs_info = cls.GetPVInfo([vg_name])
401
    if not pvs_info:
402
      _ThrowError("Can't compute PV info for vg %s", vg_name)
403
    pvs_info.sort()
404
    pvs_info.reverse()
405

    
406
    pvlist = [ pv[1] for pv in pvs_info ]
407
    if compat.any(":" in v for v in pvlist):
408
      _ThrowError("Some of your PVs have the invalid character ':' in their"
409
                  " name, this is not supported - please filter them out"
410
                  " in lvm.conf using either 'filter' or 'preferred_names'")
411
    free_size = sum([ pv[0] for pv in pvs_info ])
412
    current_pvs = len(pvlist)
413
    stripes = min(current_pvs, constants.LVM_STRIPECOUNT)
414

    
415
    # The size constraint should have been checked from the master before
416
    # calling the create function.
417
    if free_size < size:
418
      _ThrowError("Not enough free space: required %s,"
419
                  " available %s", size, free_size)
420
    cmd = ["lvcreate", "-L%dm" % size, "-n%s" % lv_name]
421
    # If the free space is not well distributed, we won't be able to
422
    # create an optimally-striped volume; in that case, we want to try
423
    # with N, N-1, ..., 2, and finally 1 (non-stripped) number of
424
    # stripes
425
    for stripes_arg in range(stripes, 0, -1):
426
      result = utils.RunCmd(cmd + ["-i%d" % stripes_arg] + [vg_name] + pvlist)
427
      if not result.failed:
428
        break
429
    if result.failed:
430
      _ThrowError("LV create failed (%s): %s",
431
                  result.fail_reason, result.output)
432
    return LogicalVolume(unique_id, children, size)
433

    
434
  @staticmethod
435
  def _GetVolumeInfo(lvm_cmd, fields):
436
    """Returns LVM Volumen infos using lvm_cmd
437

438
    @param lvm_cmd: Should be one of "pvs", "vgs" or "lvs"
439
    @param fields: Fields to return
440
    @return: A list of dicts each with the parsed fields
441

442
    """
443
    if not fields:
444
      raise errors.ProgrammerError("No fields specified")
445

    
446
    sep = "|"
447
    cmd = [lvm_cmd, "--noheadings", "--nosuffix", "--units=m", "--unbuffered",
448
           "--separator=%s" % sep, "-o%s" % ",".join(fields)]
449

    
450
    result = utils.RunCmd(cmd)
451
    if result.failed:
452
      raise errors.CommandError("Can't get the volume information: %s - %s" %
453
                                (result.fail_reason, result.output))
454

    
455
    data = []
456
    for line in result.stdout.splitlines():
457
      splitted_fields = line.strip().split(sep)
458

    
459
      if len(fields) != len(splitted_fields):
460
        raise errors.CommandError("Can't parse %s output: line '%s'" %
461
                                  (lvm_cmd, line))
462

    
463
      data.append(splitted_fields)
464

    
465
    return data
466

    
467
  @classmethod
468
  def GetPVInfo(cls, vg_names, filter_allocatable=True):
469
    """Get the free space info for PVs in a volume group.
470

471
    @param vg_names: list of volume group names, if empty all will be returned
472
    @param filter_allocatable: whether to skip over unallocatable PVs
473

474
    @rtype: list
475
    @return: list of tuples (free_space, name) with free_space in mebibytes
476

477
    """
478
    try:
479
      info = cls._GetVolumeInfo("pvs", ["pv_name", "vg_name", "pv_free",
480
                                        "pv_attr"])
481
    except errors.GenericError, err:
482
      logging.error("Can't get PV information: %s", err)
483
      return None
484

    
485
    data = []
486
    for pv_name, vg_name, pv_free, pv_attr in info:
487
      # (possibly) skip over pvs which are not allocatable
488
      if filter_allocatable and pv_attr[0] != "a":
489
        continue
490
      # (possibly) skip over pvs which are not in the right volume group(s)
491
      if vg_names and vg_name not in vg_names:
492
        continue
493
      data.append((float(pv_free), pv_name, vg_name))
494

    
495
    return data
496

    
497
  @classmethod
498
  def GetVGInfo(cls, vg_names, filter_readonly=True):
499
    """Get the free space info for specific VGs.
500

501
    @param vg_names: list of volume group names, if empty all will be returned
502
    @param filter_readonly: whether to skip over readonly VGs
503

504
    @rtype: list
505
    @return: list of tuples (free_space, total_size, name) with free_space in
506
             MiB
507

508
    """
509
    try:
510
      info = cls._GetVolumeInfo("vgs", ["vg_name", "vg_free", "vg_attr",
511
                                        "vg_size"])
512
    except errors.GenericError, err:
513
      logging.error("Can't get VG information: %s", err)
514
      return None
515

    
516
    data = []
517
    for vg_name, vg_free, vg_attr, vg_size in info:
518
      # (possibly) skip over vgs which are not writable
519
      if filter_readonly and vg_attr[0] == "r":
520
        continue
521
      # (possibly) skip over vgs which are not in the right volume group(s)
522
      if vg_names and vg_name not in vg_names:
523
        continue
524
      data.append((float(vg_free), float(vg_size), vg_name))
525

    
526
    return data
527

    
528
  @classmethod
529
  def _ValidateName(cls, name):
530
    """Validates that a given name is valid as VG or LV name.
531

532
    The list of valid characters and restricted names is taken out of
533
    the lvm(8) manpage, with the simplification that we enforce both
534
    VG and LV restrictions on the names.
535

536
    """
537
    if (not cls._VALID_NAME_RE.match(name) or
538
        name in cls._INVALID_NAMES or
539
        compat.any(substring in name for substring in cls._INVALID_SUBSTRINGS)):
540
      _ThrowError("Invalid LVM name '%s'", name)
541

    
542
  def Remove(self):
543
    """Remove this logical volume.
544

545
    """
546
    if not self.minor and not self.Attach():
547
      # the LV does not exist
548
      return
549
    result = utils.RunCmd(["lvremove", "-f", "%s/%s" %
550
                           (self._vg_name, self._lv_name)])
551
    if result.failed:
552
      _ThrowError("Can't lvremove: %s - %s", result.fail_reason, result.output)
553

    
554
  def Rename(self, new_id):
555
    """Rename this logical volume.
556

557
    """
558
    if not isinstance(new_id, (tuple, list)) or len(new_id) != 2:
559
      raise errors.ProgrammerError("Invalid new logical id '%s'" % new_id)
560
    new_vg, new_name = new_id
561
    if new_vg != self._vg_name:
562
      raise errors.ProgrammerError("Can't move a logical volume across"
563
                                   " volume groups (from %s to to %s)" %
564
                                   (self._vg_name, new_vg))
565
    result = utils.RunCmd(["lvrename", new_vg, self._lv_name, new_name])
566
    if result.failed:
567
      _ThrowError("Failed to rename the logical volume: %s", result.output)
568
    self._lv_name = new_name
569
    self.dev_path = utils.PathJoin("/dev", self._vg_name, self._lv_name)
570

    
571
  def Attach(self):
572
    """Attach to an existing LV.
573

574
    This method will try to see if an existing and active LV exists
575
    which matches our name. If so, its major/minor will be
576
    recorded.
577

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

    
604
    status, major, minor, pe_size, stripes = out
605
    if len(status) != 6:
606
      logging.error("lvs lv_attr is not 6 characters (%s)", status)
607
      return False
608

    
609
    try:
610
      major = int(major)
611
      minor = int(minor)
612
    except (TypeError, ValueError), err:
613
      logging.error("lvs major/minor cannot be parsed: %s", str(err))
614

    
615
    try:
616
      pe_size = int(float(pe_size))
617
    except (TypeError, ValueError), err:
618
      logging.error("Can't parse vg extent size: %s", err)
619
      return False
620

    
621
    try:
622
      stripes = int(stripes)
623
    except (TypeError, ValueError), err:
624
      logging.error("Can't parse the number of stripes: %s", err)
625
      return False
626

    
627
    self.major = major
628
    self.minor = minor
629
    self.pe_size = pe_size
630
    self.stripe_count = stripes
631
    self._degraded = status[0] == 'v' # virtual volume, i.e. doesn't backing
632
                                      # storage
633
    self.attached = True
634
    return True
635

    
636
  def Assemble(self):
637
    """Assemble the device.
638

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

643
    """
644
    result = utils.RunCmd(["lvchange", "-ay", self.dev_path])
645
    if result.failed:
646
      _ThrowError("Can't activate lv %s: %s", self.dev_path, result.output)
647

    
648
  def Shutdown(self):
649
    """Shutdown the device.
650

651
    This is a no-op for the LV device type, as we don't deactivate the
652
    volumes on shutdown.
653

654
    """
655
    pass
656

    
657
  def GetSyncStatus(self):
658
    """Returns the sync status of the device.
659

660
    If this device is a mirroring device, this function returns the
661
    status of the mirror.
662

663
    For logical volumes, sync_percent and estimated_time are always
664
    None (no recovery in progress, as we don't handle the mirrored LV
665
    case). The is_degraded parameter is the inverse of the ldisk
666
    parameter.
667

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

674
    The status was already read in Attach, so we just return it.
675

676
    @rtype: objects.BlockDevStatus
677

678
    """
679
    if self._degraded:
680
      ldisk_status = constants.LDS_FAULTY
681
    else:
682
      ldisk_status = constants.LDS_OKAY
683

    
684
    return objects.BlockDevStatus(dev_path=self.dev_path,
685
                                  major=self.major,
686
                                  minor=self.minor,
687
                                  sync_percent=None,
688
                                  estimated_time=None,
689
                                  is_degraded=self._degraded,
690
                                  ldisk_status=ldisk_status)
691

    
692
  def Open(self, force=False):
693
    """Make the device ready for I/O.
694

695
    This is a no-op for the LV device type.
696

697
    """
698
    pass
699

    
700
  def Close(self):
701
    """Notifies that the device will no longer be used for I/O.
702

703
    This is a no-op for the LV device type.
704

705
    """
706
    pass
707

    
708
  def Snapshot(self, size):
709
    """Create a snapshot copy of an lvm block device.
710

711
    @returns: tuple (vg, lv)
712

713
    """
714
    snap_name = self._lv_name + ".snap"
715

    
716
    # remove existing snapshot if found
717
    snap = LogicalVolume((self._vg_name, snap_name), None, size)
718
    _IgnoreError(snap.Remove)
719

    
720
    vg_info = self.GetVGInfo([self._vg_name])
721
    if not vg_info:
722
      _ThrowError("Can't compute VG info for vg %s", self._vg_name)
723
    free_size, _, _ = vg_info[0]
724
    if free_size < size:
725
      _ThrowError("Not enough free space: required %s,"
726
                  " available %s", size, free_size)
727

    
728
    result = utils.RunCmd(["lvcreate", "-L%dm" % size, "-s",
729
                           "-n%s" % snap_name, self.dev_path])
730
    if result.failed:
731
      _ThrowError("command: %s error: %s - %s",
732
                  result.cmd, result.fail_reason, result.output)
733

    
734
    return (self._vg_name, snap_name)
735

    
736
  def SetInfo(self, text):
737
    """Update metadata with info text.
738

739
    """
740
    BlockDev.SetInfo(self, text)
741

    
742
    # Replace invalid characters
743
    text = re.sub('^[^A-Za-z0-9_+.]', '_', text)
744
    text = re.sub('[^-A-Za-z0-9_+.]', '_', text)
745

    
746
    # Only up to 128 characters are allowed
747
    text = text[:128]
748

    
749
    result = utils.RunCmd(["lvchange", "--addtag", text,
750
                           self.dev_path])
751
    if result.failed:
752
      _ThrowError("Command: %s error: %s - %s", result.cmd, result.fail_reason,
753
                  result.output)
754

    
755
  def Grow(self, amount):
756
    """Grow the logical volume.
757

758
    """
759
    if self.pe_size is None or self.stripe_count is None:
760
      if not self.Attach():
761
        _ThrowError("Can't attach to LV during Grow()")
762
    full_stripe_size = self.pe_size * self.stripe_count
763
    rest = amount % full_stripe_size
764
    if rest != 0:
765
      amount += full_stripe_size - rest
766
    # we try multiple algorithms since the 'best' ones might not have
767
    # space available in the right place, but later ones might (since
768
    # they have less constraints); also note that only recent LVM
769
    # supports 'cling'
770
    for alloc_policy in "contiguous", "cling", "normal":
771
      result = utils.RunCmd(["lvextend", "--alloc", alloc_policy,
772
                             "-L", "+%dm" % amount, self.dev_path])
773
      if not result.failed:
774
        return
775
    _ThrowError("Can't grow LV %s: %s", self.dev_path, result.output)
776

    
777

    
778
class DRBD8Status(object):
779
  """A DRBD status representation class.
780

781
  Note that this doesn't support unconfigured devices (cs:Unconfigured).
782

783
  """
784
  UNCONF_RE = re.compile(r"\s*[0-9]+:\s*cs:Unconfigured$")
785
  LINE_RE = re.compile(r"\s*[0-9]+:\s*cs:(\S+)\s+(?:st|ro):([^/]+)/(\S+)"
786
                       "\s+ds:([^/]+)/(\S+)\s+.*$")
787
  SYNC_RE = re.compile(r"^.*\ssync'ed:\s*([0-9.]+)%.*"
788
                       # Due to a bug in drbd in the kernel, introduced in
789
                       # commit 4b0715f096 (still unfixed as of 2011-08-22)
790
                       "(?:\s|M)"
791
                       "finish: ([0-9]+):([0-9]+):([0-9]+)\s.*$")
792

    
793
  CS_UNCONFIGURED = "Unconfigured"
794
  CS_STANDALONE = "StandAlone"
795
  CS_WFCONNECTION = "WFConnection"
796
  CS_WFREPORTPARAMS = "WFReportParams"
797
  CS_CONNECTED = "Connected"
798
  CS_STARTINGSYNCS = "StartingSyncS"
799
  CS_STARTINGSYNCT = "StartingSyncT"
800
  CS_WFBITMAPS = "WFBitMapS"
801
  CS_WFBITMAPT = "WFBitMapT"
802
  CS_WFSYNCUUID = "WFSyncUUID"
803
  CS_SYNCSOURCE = "SyncSource"
804
  CS_SYNCTARGET = "SyncTarget"
805
  CS_PAUSEDSYNCS = "PausedSyncS"
806
  CS_PAUSEDSYNCT = "PausedSyncT"
807
  CSET_SYNC = frozenset([
808
    CS_WFREPORTPARAMS,
809
    CS_STARTINGSYNCS,
810
    CS_STARTINGSYNCT,
811
    CS_WFBITMAPS,
812
    CS_WFBITMAPT,
813
    CS_WFSYNCUUID,
814
    CS_SYNCSOURCE,
815
    CS_SYNCTARGET,
816
    CS_PAUSEDSYNCS,
817
    CS_PAUSEDSYNCT,
818
    ])
819

    
820
  DS_DISKLESS = "Diskless"
821
  DS_ATTACHING = "Attaching" # transient state
822
  DS_FAILED = "Failed" # transient state, next: diskless
823
  DS_NEGOTIATING = "Negotiating" # transient state
824
  DS_INCONSISTENT = "Inconsistent" # while syncing or after creation
825
  DS_OUTDATED = "Outdated"
826
  DS_DUNKNOWN = "DUnknown" # shown for peer disk when not connected
827
  DS_CONSISTENT = "Consistent"
828
  DS_UPTODATE = "UpToDate" # normal state
829

    
830
  RO_PRIMARY = "Primary"
831
  RO_SECONDARY = "Secondary"
832
  RO_UNKNOWN = "Unknown"
833

    
834
  def __init__(self, procline):
835
    u = self.UNCONF_RE.match(procline)
836
    if u:
837
      self.cstatus = self.CS_UNCONFIGURED
838
      self.lrole = self.rrole = self.ldisk = self.rdisk = None
839
    else:
840
      m = self.LINE_RE.match(procline)
841
      if not m:
842
        raise errors.BlockDeviceError("Can't parse input data '%s'" % procline)
843
      self.cstatus = m.group(1)
844
      self.lrole = m.group(2)
845
      self.rrole = m.group(3)
846
      self.ldisk = m.group(4)
847
      self.rdisk = m.group(5)
848

    
849
    # end reading of data from the LINE_RE or UNCONF_RE
850

    
851
    self.is_standalone = self.cstatus == self.CS_STANDALONE
852
    self.is_wfconn = self.cstatus == self.CS_WFCONNECTION
853
    self.is_connected = self.cstatus == self.CS_CONNECTED
854
    self.is_primary = self.lrole == self.RO_PRIMARY
855
    self.is_secondary = self.lrole == self.RO_SECONDARY
856
    self.peer_primary = self.rrole == self.RO_PRIMARY
857
    self.peer_secondary = self.rrole == self.RO_SECONDARY
858
    self.both_primary = self.is_primary and self.peer_primary
859
    self.both_secondary = self.is_secondary and self.peer_secondary
860

    
861
    self.is_diskless = self.ldisk == self.DS_DISKLESS
862
    self.is_disk_uptodate = self.ldisk == self.DS_UPTODATE
863

    
864
    self.is_in_resync = self.cstatus in self.CSET_SYNC
865
    self.is_in_use = self.cstatus != self.CS_UNCONFIGURED
866

    
867
    m = self.SYNC_RE.match(procline)
868
    if m:
869
      self.sync_percent = float(m.group(1))
870
      hours = int(m.group(2))
871
      minutes = int(m.group(3))
872
      seconds = int(m.group(4))
873
      self.est_time = hours * 3600 + minutes * 60 + seconds
874
    else:
875
      # we have (in this if branch) no percent information, but if
876
      # we're resyncing we need to 'fake' a sync percent information,
877
      # as this is how cmdlib determines if it makes sense to wait for
878
      # resyncing or not
879
      if self.is_in_resync:
880
        self.sync_percent = 0
881
      else:
882
        self.sync_percent = None
883
      self.est_time = None
884

    
885

    
886
class BaseDRBD(BlockDev): # pylint: disable-msg=W0223
887
  """Base DRBD class.
888

889
  This class contains a few bits of common functionality between the
890
  0.7 and 8.x versions of DRBD.
891

892
  """
893
  _VERSION_RE = re.compile(r"^version: (\d+)\.(\d+)\.(\d+)(?:\.\d+)?"
894
                           r" \(api:(\d+)/proto:(\d+)(?:-(\d+))?\)")
895
  _VALID_LINE_RE = re.compile("^ *([0-9]+): cs:([^ ]+).*$")
896
  _UNUSED_LINE_RE = re.compile("^ *([0-9]+): cs:Unconfigured$")
897

    
898
  _DRBD_MAJOR = 147
899
  _ST_UNCONFIGURED = "Unconfigured"
900
  _ST_WFCONNECTION = "WFConnection"
901
  _ST_CONNECTED = "Connected"
902

    
903
  _STATUS_FILE = "/proc/drbd"
904
  _USERMODE_HELPER_FILE = "/sys/module/drbd/parameters/usermode_helper"
905

    
906
  @staticmethod
907
  def _GetProcData(filename=_STATUS_FILE):
908
    """Return data from /proc/drbd.
909

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

    
923
  @classmethod
924
  def _MassageProcData(cls, data):
925
    """Transform the output of _GetProdData into a nicer form.
926

927
    @return: a dictionary of minor: joined lines from /proc/drbd
928
        for that minor
929

930
    """
931
    results = {}
932
    old_minor = old_line = None
933
    for line in data:
934
      if not line: # completely empty lines, as can be returned by drbd8.0+
935
        continue
936
      lresult = cls._VALID_LINE_RE.match(line)
937
      if lresult is not None:
938
        if old_minor is not None:
939
          results[old_minor] = old_line
940
        old_minor = int(lresult.group(1))
941
        old_line = line
942
      else:
943
        if old_minor is not None:
944
          old_line += " " + line.strip()
945
    # add last line
946
    if old_minor is not None:
947
      results[old_minor] = old_line
948
    return results
949

    
950
  @classmethod
951
  def _GetVersion(cls, proc_data):
952
    """Return the DRBD version.
953

954
    This will return a dict with keys:
955
      - k_major
956
      - k_minor
957
      - k_point
958
      - api
959
      - proto
960
      - proto2 (only on drbd > 8.2.X)
961

962
    """
963
    first_line = proc_data[0].strip()
964
    version = cls._VERSION_RE.match(first_line)
965
    if not version:
966
      raise errors.BlockDeviceError("Can't parse DRBD version from '%s'" %
967
                                    first_line)
968

    
969
    values = version.groups()
970
    retval = {'k_major': int(values[0]),
971
              'k_minor': int(values[1]),
972
              'k_point': int(values[2]),
973
              'api': int(values[3]),
974
              'proto': int(values[4]),
975
             }
976
    if values[5] is not None:
977
      retval['proto2'] = values[5]
978

    
979
    return retval
980

    
981
  @staticmethod
982
  def GetUsermodeHelper(filename=_USERMODE_HELPER_FILE):
983
    """Returns DRBD usermode_helper currently set.
984

985
    """
986
    try:
987
      helper = utils.ReadFile(filename).splitlines()[0]
988
    except EnvironmentError, err:
989
      if err.errno == errno.ENOENT:
990
        _ThrowError("The file %s cannot be opened, check if the module"
991
                    " is loaded (%s)", filename, str(err))
992
      else:
993
        _ThrowError("Can't read DRBD helper file %s: %s", filename, str(err))
994
    if not helper:
995
      _ThrowError("Can't read any data from %s", filename)
996
    return helper
997

    
998
  @staticmethod
999
  def _DevPath(minor):
1000
    """Return the path to a drbd device for a given minor.
1001

1002
    """
1003
    return "/dev/drbd%d" % minor
1004

    
1005
  @classmethod
1006
  def GetUsedDevs(cls):
1007
    """Compute the list of used DRBD devices.
1008

1009
    """
1010
    data = cls._GetProcData()
1011

    
1012
    used_devs = {}
1013
    for line in data:
1014
      match = cls._VALID_LINE_RE.match(line)
1015
      if not match:
1016
        continue
1017
      minor = int(match.group(1))
1018
      state = match.group(2)
1019
      if state == cls._ST_UNCONFIGURED:
1020
        continue
1021
      used_devs[minor] = state, line
1022

    
1023
    return used_devs
1024

    
1025
  def _SetFromMinor(self, minor):
1026
    """Set our parameters based on the given minor.
1027

1028
    This sets our minor variable and our dev_path.
1029

1030
    """
1031
    if minor is None:
1032
      self.minor = self.dev_path = None
1033
      self.attached = False
1034
    else:
1035
      self.minor = minor
1036
      self.dev_path = self._DevPath(minor)
1037
      self.attached = True
1038

    
1039
  @staticmethod
1040
  def _CheckMetaSize(meta_device):
1041
    """Check if the given meta device looks like a valid one.
1042

1043
    This currently only check the size, which must be around
1044
    128MiB.
1045

1046
    """
1047
    result = utils.RunCmd(["blockdev", "--getsize", meta_device])
1048
    if result.failed:
1049
      _ThrowError("Failed to get device size: %s - %s",
1050
                  result.fail_reason, result.output)
1051
    try:
1052
      sectors = int(result.stdout)
1053
    except (TypeError, ValueError):
1054
      _ThrowError("Invalid output from blockdev: '%s'", result.stdout)
1055
    num_bytes = sectors * 512
1056
    if num_bytes < 128 * 1024 * 1024: # less than 128MiB
1057
      _ThrowError("Meta device too small (%.2fMib)", (num_bytes / 1024 / 1024))
1058
    # the maximum *valid* size of the meta device when living on top
1059
    # of LVM is hard to compute: it depends on the number of stripes
1060
    # and the PE size; e.g. a 2-stripe, 64MB PE will result in a 128MB
1061
    # (normal size), but an eight-stripe 128MB PE will result in a 1GB
1062
    # size meta device; as such, we restrict it to 1GB (a little bit
1063
    # too generous, but making assumptions about PE size is hard)
1064
    if num_bytes > 1024 * 1024 * 1024:
1065
      _ThrowError("Meta device too big (%.2fMiB)", (num_bytes / 1024 / 1024))
1066

    
1067
  def Rename(self, new_id):
1068
    """Rename a device.
1069

1070
    This is not supported for drbd devices.
1071

1072
    """
1073
    raise errors.ProgrammerError("Can't rename a drbd device")
1074

    
1075

    
1076
class DRBD8(BaseDRBD):
1077
  """DRBD v8.x block device.
1078

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

1083
  The unique_id for the drbd device is the (local_ip, local_port,
1084
  remote_ip, remote_port) tuple, and it must have two children: the
1085
  data device and the meta_device. The meta device is checked for
1086
  valid size and is zeroed on create.
1087

1088
  """
1089
  _MAX_MINORS = 255
1090
  _PARSE_SHOW = None
1091

    
1092
  # timeout constants
1093
  _NET_RECONFIG_TIMEOUT = 60
1094

    
1095
  def __init__(self, unique_id, children, size):
1096
    if children and children.count(None) > 0:
1097
      children = []
1098
    if len(children) not in (0, 2):
1099
      raise ValueError("Invalid configuration data %s" % str(children))
1100
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 6:
1101
      raise ValueError("Invalid configuration data %s" % str(unique_id))
1102
    (self._lhost, self._lport,
1103
     self._rhost, self._rport,
1104
     self._aminor, self._secret) = unique_id
1105
    if children:
1106
      if not _CanReadDevice(children[1].dev_path):
1107
        logging.info("drbd%s: Ignoring unreadable meta device", self._aminor)
1108
        children = []
1109
    super(DRBD8, self).__init__(unique_id, children, size)
1110
    self.major = self._DRBD_MAJOR
1111
    version = self._GetVersion(self._GetProcData())
1112
    if version['k_major'] != 8 :
1113
      _ThrowError("Mismatch in DRBD kernel version and requested ganeti"
1114
                  " usage: kernel is %s.%s, ganeti wants 8.x",
1115
                  version['k_major'], version['k_minor'])
1116

    
1117
    if (self._lhost is not None and self._lhost == self._rhost and
1118
        self._lport == self._rport):
1119
      raise ValueError("Invalid configuration data, same local/remote %s" %
1120
                       (unique_id,))
1121
    self.Attach()
1122

    
1123
  @classmethod
1124
  def _InitMeta(cls, minor, dev_path):
1125
    """Initialize a meta device.
1126

1127
    This will not work if the given minor is in use.
1128

1129
    """
1130
    result = utils.RunCmd(["drbdmeta", "--force", cls._DevPath(minor),
1131
                           "v08", dev_path, "0", "create-md"])
1132
    if result.failed:
1133
      _ThrowError("Can't initialize meta device: %s", result.output)
1134

    
1135
  @classmethod
1136
  def _FindUnusedMinor(cls):
1137
    """Find an unused DRBD device.
1138

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

1142
    """
1143
    data = cls._GetProcData()
1144

    
1145
    highest = None
1146
    for line in data:
1147
      match = cls._UNUSED_LINE_RE.match(line)
1148
      if match:
1149
        return int(match.group(1))
1150
      match = cls._VALID_LINE_RE.match(line)
1151
      if match:
1152
        minor = int(match.group(1))
1153
        highest = max(highest, minor)
1154
    if highest is None: # there are no minors in use at all
1155
      return 0
1156
    if highest >= cls._MAX_MINORS:
1157
      logging.error("Error: no free drbd minors!")
1158
      raise errors.BlockDeviceError("Can't find a free DRBD minor")
1159
    return highest + 1
1160

    
1161
  @classmethod
1162
  def _GetShowParser(cls):
1163
    """Return a parser for `drbd show` output.
1164

1165
    This will either create or return an already-create parser for the
1166
    output of the command `drbd show`.
1167

1168
    """
1169
    if cls._PARSE_SHOW is not None:
1170
      return cls._PARSE_SHOW
1171

    
1172
    # pyparsing setup
1173
    lbrace = pyp.Literal("{").suppress()
1174
    rbrace = pyp.Literal("}").suppress()
1175
    lbracket = pyp.Literal("[").suppress()
1176
    rbracket = pyp.Literal("]").suppress()
1177
    semi = pyp.Literal(";").suppress()
1178
    colon = pyp.Literal(":").suppress()
1179
    # this also converts the value to an int
1180
    number = pyp.Word(pyp.nums).setParseAction(lambda s, l, t: int(t[0]))
1181

    
1182
    comment = pyp.Literal ("#") + pyp.Optional(pyp.restOfLine)
1183
    defa = pyp.Literal("_is_default").suppress()
1184
    dbl_quote = pyp.Literal('"').suppress()
1185

    
1186
    keyword = pyp.Word(pyp.alphanums + '-')
1187

    
1188
    # value types
1189
    value = pyp.Word(pyp.alphanums + '_-/.:')
1190
    quoted = dbl_quote + pyp.CharsNotIn('"') + dbl_quote
1191
    ipv4_addr = (pyp.Optional(pyp.Literal("ipv4")).suppress() +
1192
                 pyp.Word(pyp.nums + ".") + colon + number)
1193
    ipv6_addr = (pyp.Optional(pyp.Literal("ipv6")).suppress() +
1194
                 pyp.Optional(lbracket) + pyp.Word(pyp.hexnums + ":") +
1195
                 pyp.Optional(rbracket) + colon + number)
1196
    # meta device, extended syntax
1197
    meta_value = ((value ^ quoted) + lbracket + number + rbracket)
1198
    # device name, extended syntax
1199
    device_value = pyp.Literal("minor").suppress() + number
1200

    
1201
    # a statement
1202
    stmt = (~rbrace + keyword + ~lbrace +
1203
            pyp.Optional(ipv4_addr ^ ipv6_addr ^ value ^ quoted ^ meta_value ^
1204
                         device_value) +
1205
            pyp.Optional(defa) + semi +
1206
            pyp.Optional(pyp.restOfLine).suppress())
1207

    
1208
    # an entire section
1209
    section_name = pyp.Word(pyp.alphas + '_')
1210
    section = section_name + lbrace + pyp.ZeroOrMore(pyp.Group(stmt)) + rbrace
1211

    
1212
    bnf = pyp.ZeroOrMore(pyp.Group(section ^ stmt))
1213
    bnf.ignore(comment)
1214

    
1215
    cls._PARSE_SHOW = bnf
1216

    
1217
    return bnf
1218

    
1219
  @classmethod
1220
  def _GetShowData(cls, minor):
1221
    """Return the `drbdsetup show` data for a minor.
1222

1223
    """
1224
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "show"])
1225
    if result.failed:
1226
      logging.error("Can't display the drbd config: %s - %s",
1227
                    result.fail_reason, result.output)
1228
      return None
1229
    return result.stdout
1230

    
1231
  @classmethod
1232
  def _GetDevInfo(cls, out):
1233
    """Parse details about a given DRBD minor.
1234

1235
    This return, if available, the local backing device (as a path)
1236
    and the local and remote (ip, port) information from a string
1237
    containing the output of the `drbdsetup show` command as returned
1238
    by _GetShowData.
1239

1240
    """
1241
    data = {}
1242
    if not out:
1243
      return data
1244

    
1245
    bnf = cls._GetShowParser()
1246
    # run pyparse
1247

    
1248
    try:
1249
      results = bnf.parseString(out)
1250
    except pyp.ParseException, err:
1251
      _ThrowError("Can't parse drbdsetup show output: %s", str(err))
1252

    
1253
    # and massage the results into our desired format
1254
    for section in results:
1255
      sname = section[0]
1256
      if sname == "_this_host":
1257
        for lst in section[1:]:
1258
          if lst[0] == "disk":
1259
            data["local_dev"] = lst[1]
1260
          elif lst[0] == "meta-disk":
1261
            data["meta_dev"] = lst[1]
1262
            data["meta_index"] = lst[2]
1263
          elif lst[0] == "address":
1264
            data["local_addr"] = tuple(lst[1:])
1265
      elif sname == "_remote_host":
1266
        for lst in section[1:]:
1267
          if lst[0] == "address":
1268
            data["remote_addr"] = tuple(lst[1:])
1269
    return data
1270

    
1271
  def _MatchesLocal(self, info):
1272
    """Test if our local config matches with an existing device.
1273

1274
    The parameter should be as returned from `_GetDevInfo()`. This
1275
    method tests if our local backing device is the same as the one in
1276
    the info parameter, in effect testing if we look like the given
1277
    device.
1278

1279
    """
1280
    if self._children:
1281
      backend, meta = self._children
1282
    else:
1283
      backend = meta = None
1284

    
1285
    if backend is not None:
1286
      retval = ("local_dev" in info and info["local_dev"] == backend.dev_path)
1287
    else:
1288
      retval = ("local_dev" not in info)
1289

    
1290
    if meta is not None:
1291
      retval = retval and ("meta_dev" in info and
1292
                           info["meta_dev"] == meta.dev_path)
1293
      retval = retval and ("meta_index" in info and
1294
                           info["meta_index"] == 0)
1295
    else:
1296
      retval = retval and ("meta_dev" not in info and
1297
                           "meta_index" not in info)
1298
    return retval
1299

    
1300
  def _MatchesNet(self, info):
1301
    """Test if our network config matches with an existing device.
1302

1303
    The parameter should be as returned from `_GetDevInfo()`. This
1304
    method tests if our network configuration is the same as the one
1305
    in the info parameter, in effect testing if we look like the given
1306
    device.
1307

1308
    """
1309
    if (((self._lhost is None and not ("local_addr" in info)) and
1310
         (self._rhost is None and not ("remote_addr" in info)))):
1311
      return True
1312

    
1313
    if self._lhost is None:
1314
      return False
1315

    
1316
    if not ("local_addr" in info and
1317
            "remote_addr" in info):
1318
      return False
1319

    
1320
    retval = (info["local_addr"] == (self._lhost, self._lport))
1321
    retval = (retval and
1322
              info["remote_addr"] == (self._rhost, self._rport))
1323
    return retval
1324

    
1325
  @classmethod
1326
  def _AssembleLocal(cls, minor, backend, meta, size):
1327
    """Configure the local part of a DRBD device.
1328

1329
    """
1330
    args = ["drbdsetup", cls._DevPath(minor), "disk",
1331
            backend, meta, "0",
1332
            "-e", "detach",
1333
            "--create-device"]
1334
    if size:
1335
      args.extend(["-d", "%sm" % size])
1336
    if not constants.DRBD_BARRIERS: # disable barriers, if configured so
1337
      version = cls._GetVersion(cls._GetProcData())
1338
      # various DRBD versions support different disk barrier options;
1339
      # what we aim here is to revert back to the 'drain' method of
1340
      # disk flushes and to disable metadata barriers, in effect going
1341
      # back to pre-8.0.7 behaviour
1342
      vmaj = version['k_major']
1343
      vmin = version['k_minor']
1344
      vrel = version['k_point']
1345
      assert vmaj == 8
1346
      if vmin == 0: # 8.0.x
1347
        if vrel >= 12:
1348
          args.extend(['-i', '-m'])
1349
      elif vmin == 2: # 8.2.x
1350
        if vrel >= 7:
1351
          args.extend(['-i', '-m'])
1352
      elif vmaj >= 3: # 8.3.x or newer
1353
        args.extend(['-i', '-a', 'm'])
1354
    result = utils.RunCmd(args)
1355
    if result.failed:
1356
      _ThrowError("drbd%d: can't attach local disk: %s", minor, result.output)
1357

    
1358
  @classmethod
1359
  def _AssembleNet(cls, minor, net_info, protocol,
1360
                   dual_pri=False, hmac=None, secret=None):
1361
    """Configure the network part of the device.
1362

1363
    """
1364
    lhost, lport, rhost, rport = net_info
1365
    if None in net_info:
1366
      # we don't want network connection and actually want to make
1367
      # sure its shutdown
1368
      cls._ShutdownNet(minor)
1369
      return
1370

    
1371
    # Workaround for a race condition. When DRBD is doing its dance to
1372
    # establish a connection with its peer, it also sends the
1373
    # synchronization speed over the wire. In some cases setting the
1374
    # sync speed only after setting up both sides can race with DRBD
1375
    # connecting, hence we set it here before telling DRBD anything
1376
    # about its peer.
1377
    cls._SetMinorSyncSpeed(minor, constants.SYNC_SPEED)
1378

    
1379
    if netutils.IP6Address.IsValid(lhost):
1380
      if not netutils.IP6Address.IsValid(rhost):
1381
        _ThrowError("drbd%d: can't connect ip %s to ip %s" %
1382
                    (minor, lhost, rhost))
1383
      family = "ipv6"
1384
    elif netutils.IP4Address.IsValid(lhost):
1385
      if not netutils.IP4Address.IsValid(rhost):
1386
        _ThrowError("drbd%d: can't connect ip %s to ip %s" %
1387
                    (minor, lhost, rhost))
1388
      family = "ipv4"
1389
    else:
1390
      _ThrowError("drbd%d: Invalid ip %s" % (minor, lhost))
1391

    
1392
    args = ["drbdsetup", cls._DevPath(minor), "net",
1393
            "%s:%s:%s" % (family, lhost, lport),
1394
            "%s:%s:%s" % (family, rhost, rport), protocol,
1395
            "-A", "discard-zero-changes",
1396
            "-B", "consensus",
1397
            "--create-device",
1398
            ]
1399
    if dual_pri:
1400
      args.append("-m")
1401
    if hmac and secret:
1402
      args.extend(["-a", hmac, "-x", secret])
1403
    result = utils.RunCmd(args)
1404
    if result.failed:
1405
      _ThrowError("drbd%d: can't setup network: %s - %s",
1406
                  minor, result.fail_reason, result.output)
1407

    
1408
    def _CheckNetworkConfig():
1409
      info = cls._GetDevInfo(cls._GetShowData(minor))
1410
      if not "local_addr" in info or not "remote_addr" in info:
1411
        raise utils.RetryAgain()
1412

    
1413
      if (info["local_addr"] != (lhost, lport) or
1414
          info["remote_addr"] != (rhost, rport)):
1415
        raise utils.RetryAgain()
1416

    
1417
    try:
1418
      utils.Retry(_CheckNetworkConfig, 1.0, 10.0)
1419
    except utils.RetryTimeout:
1420
      _ThrowError("drbd%d: timeout while configuring network", minor)
1421

    
1422
  def AddChildren(self, devices):
1423
    """Add a disk to the DRBD device.
1424

1425
    """
1426
    if self.minor is None:
1427
      _ThrowError("drbd%d: can't attach to dbrd8 during AddChildren",
1428
                  self._aminor)
1429
    if len(devices) != 2:
1430
      _ThrowError("drbd%d: need two devices for AddChildren", self.minor)
1431
    info = self._GetDevInfo(self._GetShowData(self.minor))
1432
    if "local_dev" in info:
1433
      _ThrowError("drbd%d: already attached to a local disk", self.minor)
1434
    backend, meta = devices
1435
    if backend.dev_path is None or meta.dev_path is None:
1436
      _ThrowError("drbd%d: children not ready during AddChildren", self.minor)
1437
    backend.Open()
1438
    meta.Open()
1439
    self._CheckMetaSize(meta.dev_path)
1440
    self._InitMeta(self._FindUnusedMinor(), meta.dev_path)
1441

    
1442
    self._AssembleLocal(self.minor, backend.dev_path, meta.dev_path, self.size)
1443
    self._children = devices
1444

    
1445
  def RemoveChildren(self, devices):
1446
    """Detach the drbd device from local storage.
1447

1448
    """
1449
    if self.minor is None:
1450
      _ThrowError("drbd%d: can't attach to drbd8 during RemoveChildren",
1451
                  self._aminor)
1452
    # early return if we don't actually have backing storage
1453
    info = self._GetDevInfo(self._GetShowData(self.minor))
1454
    if "local_dev" not in info:
1455
      return
1456
    if len(self._children) != 2:
1457
      _ThrowError("drbd%d: we don't have two children: %s", self.minor,
1458
                  self._children)
1459
    if self._children.count(None) == 2: # we don't actually have children :)
1460
      logging.warning("drbd%d: requested detach while detached", self.minor)
1461
      return
1462
    if len(devices) != 2:
1463
      _ThrowError("drbd%d: we need two children in RemoveChildren", self.minor)
1464
    for child, dev in zip(self._children, devices):
1465
      if dev != child.dev_path:
1466
        _ThrowError("drbd%d: mismatch in local storage (%s != %s) in"
1467
                    " RemoveChildren", self.minor, dev, child.dev_path)
1468

    
1469
    self._ShutdownLocal(self.minor)
1470
    self._children = []
1471

    
1472
  @classmethod
1473
  def _SetMinorSyncSpeed(cls, minor, kbytes):
1474
    """Set the speed of the DRBD syncer.
1475

1476
    This is the low-level implementation.
1477

1478
    @type minor: int
1479
    @param minor: the drbd minor whose settings we change
1480
    @type kbytes: int
1481
    @param kbytes: the speed in kbytes/second
1482
    @rtype: boolean
1483
    @return: the success of the operation
1484

1485
    """
1486
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "syncer",
1487
                           "-r", "%d" % kbytes, "--create-device"])
1488
    if result.failed:
1489
      logging.error("Can't change syncer rate: %s - %s",
1490
                    result.fail_reason, result.output)
1491
    return not result.failed
1492

    
1493
  def SetSyncSpeed(self, kbytes):
1494
    """Set the speed of the DRBD syncer.
1495

1496
    @type kbytes: int
1497
    @param kbytes: the speed in kbytes/second
1498
    @rtype: boolean
1499
    @return: the success of the operation
1500

1501
    """
1502
    if self.minor is None:
1503
      logging.info("Not attached during SetSyncSpeed")
1504
      return False
1505
    children_result = super(DRBD8, self).SetSyncSpeed(kbytes)
1506
    return self._SetMinorSyncSpeed(self.minor, kbytes) and children_result
1507

    
1508
  def PauseResumeSync(self, pause):
1509
    """Pauses or resumes the sync of a DRBD device.
1510

1511
    @param pause: Wether to pause or resume
1512
    @return: the success of the operation
1513

1514
    """
1515
    if self.minor is None:
1516
      logging.info("Not attached during PauseSync")
1517
      return False
1518

    
1519
    children_result = super(DRBD8, self).PauseResumeSync(pause)
1520

    
1521
    if pause:
1522
      cmd = "pause-sync"
1523
    else:
1524
      cmd = "resume-sync"
1525

    
1526
    result = utils.RunCmd(["drbdsetup", self.dev_path, cmd])
1527
    if result.failed:
1528
      logging.error("Can't %s: %s - %s", cmd,
1529
                    result.fail_reason, result.output)
1530
    return not result.failed and children_result
1531

    
1532
  def GetProcStatus(self):
1533
    """Return device data from /proc.
1534

1535
    """
1536
    if self.minor is None:
1537
      _ThrowError("drbd%d: GetStats() called while not attached", self._aminor)
1538
    proc_info = self._MassageProcData(self._GetProcData())
1539
    if self.minor not in proc_info:
1540
      _ThrowError("drbd%d: can't find myself in /proc", self.minor)
1541
    return DRBD8Status(proc_info[self.minor])
1542

    
1543
  def GetSyncStatus(self):
1544
    """Returns the sync status of the device.
1545

1546

1547
    If sync_percent is None, it means all is ok
1548
    If estimated_time is None, it means we can't estimate
1549
    the time needed, otherwise it's the time left in seconds.
1550

1551

1552
    We set the is_degraded parameter to True on two conditions:
1553
    network not connected or local disk missing.
1554

1555
    We compute the ldisk parameter based on whether we have a local
1556
    disk or not.
1557

1558
    @rtype: objects.BlockDevStatus
1559

1560
    """
1561
    if self.minor is None and not self.Attach():
1562
      _ThrowError("drbd%d: can't Attach() in GetSyncStatus", self._aminor)
1563

    
1564
    stats = self.GetProcStatus()
1565
    is_degraded = not stats.is_connected or not stats.is_disk_uptodate
1566

    
1567
    if stats.is_disk_uptodate:
1568
      ldisk_status = constants.LDS_OKAY
1569
    elif stats.is_diskless:
1570
      ldisk_status = constants.LDS_FAULTY
1571
    else:
1572
      ldisk_status = constants.LDS_UNKNOWN
1573

    
1574
    return objects.BlockDevStatus(dev_path=self.dev_path,
1575
                                  major=self.major,
1576
                                  minor=self.minor,
1577
                                  sync_percent=stats.sync_percent,
1578
                                  estimated_time=stats.est_time,
1579
                                  is_degraded=is_degraded,
1580
                                  ldisk_status=ldisk_status)
1581

    
1582
  def Open(self, force=False):
1583
    """Make the local state primary.
1584

1585
    If the 'force' parameter is given, the '-o' option is passed to
1586
    drbdsetup. Since this is a potentially dangerous operation, the
1587
    force flag should be only given after creation, when it actually
1588
    is mandatory.
1589

1590
    """
1591
    if self.minor is None and not self.Attach():
1592
      logging.error("DRBD cannot attach to a device during open")
1593
      return False
1594
    cmd = ["drbdsetup", self.dev_path, "primary"]
1595
    if force:
1596
      cmd.append("-o")
1597
    result = utils.RunCmd(cmd)
1598
    if result.failed:
1599
      _ThrowError("drbd%d: can't make drbd device primary: %s", self.minor,
1600
                  result.output)
1601

    
1602
  def Close(self):
1603
    """Make the local state secondary.
1604

1605
    This will, of course, fail if the device is in use.
1606

1607
    """
1608
    if self.minor is None and not self.Attach():
1609
      _ThrowError("drbd%d: can't Attach() in Close()", self._aminor)
1610
    result = utils.RunCmd(["drbdsetup", self.dev_path, "secondary"])
1611
    if result.failed:
1612
      _ThrowError("drbd%d: can't switch drbd device to secondary: %s",
1613
                  self.minor, result.output)
1614

    
1615
  def DisconnectNet(self):
1616
    """Removes network configuration.
1617

1618
    This method shutdowns the network side of the device.
1619

1620
    The method will wait up to a hardcoded timeout for the device to
1621
    go into standalone after the 'disconnect' command before
1622
    re-configuring it, as sometimes it takes a while for the
1623
    disconnect to actually propagate and thus we might issue a 'net'
1624
    command while the device is still connected. If the device will
1625
    still be attached to the network and we time out, we raise an
1626
    exception.
1627

1628
    """
1629
    if self.minor is None:
1630
      _ThrowError("drbd%d: disk not attached in re-attach net", self._aminor)
1631

    
1632
    if None in (self._lhost, self._lport, self._rhost, self._rport):
1633
      _ThrowError("drbd%d: DRBD disk missing network info in"
1634
                  " DisconnectNet()", self.minor)
1635

    
1636
    class _DisconnectStatus:
1637
      def __init__(self, ever_disconnected):
1638
        self.ever_disconnected = ever_disconnected
1639

    
1640
    dstatus = _DisconnectStatus(_IgnoreError(self._ShutdownNet, self.minor))
1641

    
1642
    def _WaitForDisconnect():
1643
      if self.GetProcStatus().is_standalone:
1644
        return
1645

    
1646
      # retry the disconnect, it seems possible that due to a well-time
1647
      # disconnect on the peer, my disconnect command might be ignored and
1648
      # forgotten
1649
      dstatus.ever_disconnected = \
1650
        _IgnoreError(self._ShutdownNet, self.minor) or dstatus.ever_disconnected
1651

    
1652
      raise utils.RetryAgain()
1653

    
1654
    # Keep start time
1655
    start_time = time.time()
1656

    
1657
    try:
1658
      # Start delay at 100 milliseconds and grow up to 2 seconds
1659
      utils.Retry(_WaitForDisconnect, (0.1, 1.5, 2.0),
1660
                  self._NET_RECONFIG_TIMEOUT)
1661
    except utils.RetryTimeout:
1662
      if dstatus.ever_disconnected:
1663
        msg = ("drbd%d: device did not react to the"
1664
               " 'disconnect' command in a timely manner")
1665
      else:
1666
        msg = "drbd%d: can't shutdown network, even after multiple retries"
1667

    
1668
      _ThrowError(msg, self.minor)
1669

    
1670
    reconfig_time = time.time() - start_time
1671
    if reconfig_time > (self._NET_RECONFIG_TIMEOUT * 0.25):
1672
      logging.info("drbd%d: DisconnectNet: detach took %.3f seconds",
1673
                   self.minor, reconfig_time)
1674

    
1675
  def AttachNet(self, multimaster):
1676
    """Reconnects the network.
1677

1678
    This method connects the network side of the device with a
1679
    specified multi-master flag. The device needs to be 'Standalone'
1680
    but have valid network configuration data.
1681

1682
    Args:
1683
      - multimaster: init the network in dual-primary mode
1684

1685
    """
1686
    if self.minor is None:
1687
      _ThrowError("drbd%d: device not attached in AttachNet", self._aminor)
1688

    
1689
    if None in (self._lhost, self._lport, self._rhost, self._rport):
1690
      _ThrowError("drbd%d: missing network info in AttachNet()", self.minor)
1691

    
1692
    status = self.GetProcStatus()
1693

    
1694
    if not status.is_standalone:
1695
      _ThrowError("drbd%d: device is not standalone in AttachNet", self.minor)
1696

    
1697
    self._AssembleNet(self.minor,
1698
                      (self._lhost, self._lport, self._rhost, self._rport),
1699
                      constants.DRBD_NET_PROTOCOL, dual_pri=multimaster,
1700
                      hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1701

    
1702
  def Attach(self):
1703
    """Check if our minor is configured.
1704

1705
    This doesn't do any device configurations - it only checks if the
1706
    minor is in a state different from Unconfigured.
1707

1708
    Note that this function will not change the state of the system in
1709
    any way (except in case of side-effects caused by reading from
1710
    /proc).
1711

1712
    """
1713
    used_devs = self.GetUsedDevs()
1714
    if self._aminor in used_devs:
1715
      minor = self._aminor
1716
    else:
1717
      minor = None
1718

    
1719
    self._SetFromMinor(minor)
1720
    return minor is not None
1721

    
1722
  def Assemble(self):
1723
    """Assemble the drbd.
1724

1725
    Method:
1726
      - if we have a configured device, we try to ensure that it matches
1727
        our config
1728
      - if not, we create it from zero
1729

1730
    """
1731
    super(DRBD8, self).Assemble()
1732

    
1733
    self.Attach()
1734
    if self.minor is None:
1735
      # local device completely unconfigured
1736
      self._FastAssemble()
1737
    else:
1738
      # we have to recheck the local and network status and try to fix
1739
      # the device
1740
      self._SlowAssemble()
1741

    
1742
  def _SlowAssemble(self):
1743
    """Assembles the DRBD device from a (partially) configured device.
1744

1745
    In case of partially attached (local device matches but no network
1746
    setup), we perform the network attach. If successful, we re-test
1747
    the attach if can return success.
1748

1749
    """
1750
    # TODO: Rewrite to not use a for loop just because there is 'break'
1751
    # pylint: disable-msg=W0631
1752
    net_data = (self._lhost, self._lport, self._rhost, self._rport)
1753
    for minor in (self._aminor,):
1754
      info = self._GetDevInfo(self._GetShowData(minor))
1755
      match_l = self._MatchesLocal(info)
1756
      match_r = self._MatchesNet(info)
1757

    
1758
      if match_l and match_r:
1759
        # everything matches
1760
        break
1761

    
1762
      if match_l and not match_r and "local_addr" not in info:
1763
        # disk matches, but not attached to network, attach and recheck
1764
        self._AssembleNet(minor, net_data, constants.DRBD_NET_PROTOCOL,
1765
                          hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1766
        if self._MatchesNet(self._GetDevInfo(self._GetShowData(minor))):
1767
          break
1768
        else:
1769
          _ThrowError("drbd%d: network attach successful, but 'drbdsetup"
1770
                      " show' disagrees", minor)
1771

    
1772
      if match_r and "local_dev" not in info:
1773
        # no local disk, but network attached and it matches
1774
        self._AssembleLocal(minor, self._children[0].dev_path,
1775
                            self._children[1].dev_path, self.size)
1776
        if self._MatchesNet(self._GetDevInfo(self._GetShowData(minor))):
1777
          break
1778
        else:
1779
          _ThrowError("drbd%d: disk attach successful, but 'drbdsetup"
1780
                      " show' disagrees", minor)
1781

    
1782
      # this case must be considered only if we actually have local
1783
      # storage, i.e. not in diskless mode, because all diskless
1784
      # devices are equal from the point of view of local
1785
      # configuration
1786
      if (match_l and "local_dev" in info and
1787
          not match_r and "local_addr" in info):
1788
        # strange case - the device network part points to somewhere
1789
        # else, even though its local storage is ours; as we own the
1790
        # drbd space, we try to disconnect from the remote peer and
1791
        # reconnect to our correct one
1792
        try:
1793
          self._ShutdownNet(minor)
1794
        except errors.BlockDeviceError, err:
1795
          _ThrowError("drbd%d: device has correct local storage, wrong"
1796
                      " remote peer and is unable to disconnect in order"
1797
                      " to attach to the correct peer: %s", minor, str(err))
1798
        # note: _AssembleNet also handles the case when we don't want
1799
        # local storage (i.e. one or more of the _[lr](host|port) is
1800
        # None)
1801
        self._AssembleNet(minor, net_data, constants.DRBD_NET_PROTOCOL,
1802
                          hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1803
        if self._MatchesNet(self._GetDevInfo(self._GetShowData(minor))):
1804
          break
1805
        else:
1806
          _ThrowError("drbd%d: network attach successful, but 'drbdsetup"
1807
                      " show' disagrees", minor)
1808

    
1809
    else:
1810
      minor = None
1811

    
1812
    self._SetFromMinor(minor)
1813
    if minor is None:
1814
      _ThrowError("drbd%d: cannot activate, unknown or unhandled reason",
1815
                  self._aminor)
1816

    
1817
  def _FastAssemble(self):
1818
    """Assemble the drbd device from zero.
1819

1820
    This is run when in Assemble we detect our minor is unused.
1821

1822
    """
1823
    minor = self._aminor
1824
    if self._children and self._children[0] and self._children[1]:
1825
      self._AssembleLocal(minor, self._children[0].dev_path,
1826
                          self._children[1].dev_path, self.size)
1827
    if self._lhost and self._lport and self._rhost and self._rport:
1828
      self._AssembleNet(minor,
1829
                        (self._lhost, self._lport, self._rhost, self._rport),
1830
                        constants.DRBD_NET_PROTOCOL,
1831
                        hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1832
    self._SetFromMinor(minor)
1833

    
1834
  @classmethod
1835
  def _ShutdownLocal(cls, minor):
1836
    """Detach from the local device.
1837

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

1841
    """
1842
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "detach"])
1843
    if result.failed:
1844
      _ThrowError("drbd%d: can't detach local disk: %s", minor, result.output)
1845

    
1846
  @classmethod
1847
  def _ShutdownNet(cls, minor):
1848
    """Disconnect from the remote peer.
1849

1850
    This fails if we don't have a local device.
1851

1852
    """
1853
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "disconnect"])
1854
    if result.failed:
1855
      _ThrowError("drbd%d: can't shutdown network: %s", minor, result.output)
1856

    
1857
  @classmethod
1858
  def _ShutdownAll(cls, minor):
1859
    """Deactivate the device.
1860

1861
    This will, of course, fail if the device is in use.
1862

1863
    """
1864
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "down"])
1865
    if result.failed:
1866
      _ThrowError("drbd%d: can't shutdown drbd device: %s",
1867
                  minor, result.output)
1868

    
1869
  def Shutdown(self):
1870
    """Shutdown the DRBD device.
1871

1872
    """
1873
    if self.minor is None and not self.Attach():
1874
      logging.info("drbd%d: not attached during Shutdown()", self._aminor)
1875
      return
1876
    minor = self.minor
1877
    self.minor = None
1878
    self.dev_path = None
1879
    self._ShutdownAll(minor)
1880

    
1881
  def Remove(self):
1882
    """Stub remove for DRBD devices.
1883

1884
    """
1885
    self.Shutdown()
1886

    
1887
  @classmethod
1888
  def Create(cls, unique_id, children, size):
1889
    """Create a new DRBD8 device.
1890

1891
    Since DRBD devices are not created per se, just assembled, this
1892
    function only initializes the metadata.
1893

1894
    """
1895
    if len(children) != 2:
1896
      raise errors.ProgrammerError("Invalid setup for the drbd device")
1897
    # check that the minor is unused
1898
    aminor = unique_id[4]
1899
    proc_info = cls._MassageProcData(cls._GetProcData())
1900
    if aminor in proc_info:
1901
      status = DRBD8Status(proc_info[aminor])
1902
      in_use = status.is_in_use
1903
    else:
1904
      in_use = False
1905
    if in_use:
1906
      _ThrowError("drbd%d: minor is already in use at Create() time", aminor)
1907
    meta = children[1]
1908
    meta.Assemble()
1909
    if not meta.Attach():
1910
      _ThrowError("drbd%d: can't attach to meta device '%s'",
1911
                  aminor, meta)
1912
    cls._CheckMetaSize(meta.dev_path)
1913
    cls._InitMeta(aminor, meta.dev_path)
1914
    return cls(unique_id, children, size)
1915

    
1916
  def Grow(self, amount):
1917
    """Resize the DRBD device and its backing storage.
1918

1919
    """
1920
    if self.minor is None:
1921
      _ThrowError("drbd%d: Grow called while not attached", self._aminor)
1922
    if len(self._children) != 2 or None in self._children:
1923
      _ThrowError("drbd%d: cannot grow diskless device", self.minor)
1924
    self._children[0].Grow(amount)
1925
    result = utils.RunCmd(["drbdsetup", self.dev_path, "resize", "-s",
1926
                           "%dm" % (self.size + amount)])
1927
    if result.failed:
1928
      _ThrowError("drbd%d: resize failed: %s", self.minor, result.output)
1929

    
1930

    
1931
class FileStorage(BlockDev):
1932
  """File device.
1933

1934
  This class represents the a file storage backend device.
1935

1936
  The unique_id for the file device is a (file_driver, file_path) tuple.
1937

1938
  """
1939
  def __init__(self, unique_id, children, size):
1940
    """Initalizes a file device backend.
1941

1942
    """
1943
    if children:
1944
      raise errors.BlockDeviceError("Invalid setup for file device")
1945
    super(FileStorage, self).__init__(unique_id, children, size)
1946
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
1947
      raise ValueError("Invalid configuration data %s" % str(unique_id))
1948
    self.driver = unique_id[0]
1949
    self.dev_path = unique_id[1]
1950
    self.Attach()
1951

    
1952
  def Assemble(self):
1953
    """Assemble the device.
1954

1955
    Checks whether the file device exists, raises BlockDeviceError otherwise.
1956

1957
    """
1958
    if not os.path.exists(self.dev_path):
1959
      _ThrowError("File device '%s' does not exist" % self.dev_path)
1960

    
1961
  def Shutdown(self):
1962
    """Shutdown the device.
1963

1964
    This is a no-op for the file type, as we don't deactivate
1965
    the file on shutdown.
1966

1967
    """
1968
    pass
1969

    
1970
  def Open(self, force=False):
1971
    """Make the device ready for I/O.
1972

1973
    This is a no-op for the file type.
1974

1975
    """
1976
    pass
1977

    
1978
  def Close(self):
1979
    """Notifies that the device will no longer be used for I/O.
1980

1981
    This is a no-op for the file type.
1982

1983
    """
1984
    pass
1985

    
1986
  def Remove(self):
1987
    """Remove the file backing the block device.
1988

1989
    @rtype: boolean
1990
    @return: True if the removal was successful
1991

1992
    """
1993
    try:
1994
      os.remove(self.dev_path)
1995
    except OSError, err:
1996
      if err.errno != errno.ENOENT:
1997
        _ThrowError("Can't remove file '%s': %s", self.dev_path, err)
1998

    
1999
  def Rename(self, new_id):
2000
    """Renames the file.
2001

2002
    """
2003
    # TODO: implement rename for file-based storage
2004
    _ThrowError("Rename is not supported for file-based storage")
2005

    
2006
  def Grow(self, amount):
2007
    """Grow the file
2008

2009
    @param amount: the amount (in mebibytes) to grow with
2010

2011
    """
2012
    # Check that the file exists
2013
    self.Assemble()
2014
    current_size = self.GetActualSize()
2015
    new_size = current_size + amount * 1024 * 1024
2016
    assert new_size > current_size, "Cannot Grow with a negative amount"
2017
    try:
2018
      f = open(self.dev_path, "a+")
2019
      f.truncate(new_size)
2020
      f.close()
2021
    except EnvironmentError, err:
2022
      _ThrowError("Error in file growth: %", str(err))
2023

    
2024
  def Attach(self):
2025
    """Attach to an existing file.
2026

2027
    Check if this file already exists.
2028

2029
    @rtype: boolean
2030
    @return: True if file exists
2031

2032
    """
2033
    self.attached = os.path.exists(self.dev_path)
2034
    return self.attached
2035

    
2036
  def GetActualSize(self):
2037
    """Return the actual disk size.
2038

2039
    @note: the device needs to be active when this is called
2040

2041
    """
2042
    assert self.attached, "BlockDevice not attached in GetActualSize()"
2043
    try:
2044
      st = os.stat(self.dev_path)
2045
      return st.st_size
2046
    except OSError, err:
2047
      _ThrowError("Can't stat %s: %s", self.dev_path, err)
2048

    
2049
  @classmethod
2050
  def Create(cls, unique_id, children, size):
2051
    """Create a new file.
2052

2053
    @param size: the size of file in MiB
2054

2055
    @rtype: L{bdev.FileStorage}
2056
    @return: an instance of FileStorage
2057

2058
    """
2059
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
2060
      raise ValueError("Invalid configuration data %s" % str(unique_id))
2061
    dev_path = unique_id[1]
2062
    try:
2063
      fd = os.open(dev_path, os.O_RDWR | os.O_CREAT | os.O_EXCL)
2064
      f = os.fdopen(fd, "w")
2065
      f.truncate(size * 1024 * 1024)
2066
      f.close()
2067
    except EnvironmentError, err:
2068
      if err.errno == errno.EEXIST:
2069
        _ThrowError("File already existing: %s", dev_path)
2070
      _ThrowError("Error in file creation: %", str(err))
2071

    
2072
    return FileStorage(unique_id, children, size)
2073

    
2074

    
2075
DEV_MAP = {
2076
  constants.LD_LV: LogicalVolume,
2077
  constants.LD_DRBD8: DRBD8,
2078
  }
2079

    
2080
if constants.ENABLE_FILE_STORAGE:
2081
  DEV_MAP[constants.LD_FILE] = FileStorage
2082

    
2083

    
2084
def FindDevice(dev_type, unique_id, children, size):
2085
  """Search for an existing, assembled device.
2086

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

2090
  """
2091
  if dev_type not in DEV_MAP:
2092
    raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
2093
  device = DEV_MAP[dev_type](unique_id, children, size)
2094
  if not device.attached:
2095
    return None
2096
  return device
2097

    
2098

    
2099
def Assemble(dev_type, unique_id, children, size):
2100
  """Try to attach or assemble an existing device.
2101

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

2105
  """
2106
  if dev_type not in DEV_MAP:
2107
    raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
2108
  device = DEV_MAP[dev_type](unique_id, children, size)
2109
  device.Assemble()
2110
  return device
2111

    
2112

    
2113
def Create(dev_type, unique_id, children, size):
2114
  """Create a device.
2115

2116
  """
2117
  if dev_type not in DEV_MAP:
2118
    raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
2119
  device = DEV_MAP[dev_type].Create(unique_id, children, size)
2120
  return device