Statistics
| Branch: | Tag: | Revision:

root / lib / bdev.py @ a3fffcc6

History | View | Annotate | Download (67.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 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
                       "\sfinish: ([0-9]+):([0-9]+):([0-9]+)\s.*$")
789

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

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

    
827
  RO_PRIMARY = "Primary"
828
  RO_SECONDARY = "Secondary"
829
  RO_UNKNOWN = "Unknown"
830

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

    
846
    # end reading of data from the LINE_RE or UNCONF_RE
847

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

    
858
    self.is_diskless = self.ldisk == self.DS_DISKLESS
859
    self.is_disk_uptodate = self.ldisk == self.DS_UPTODATE
860

    
861
    self.is_in_resync = self.cstatus in self.CSET_SYNC
862
    self.is_in_use = self.cstatus != self.CS_UNCONFIGURED
863

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

    
882

    
883
class BaseDRBD(BlockDev): # pylint: disable-msg=W0223
884
  """Base DRBD class.
885

886
  This class contains a few bits of common functionality between the
887
  0.7 and 8.x versions of DRBD.
888

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

    
895
  _DRBD_MAJOR = 147
896
  _ST_UNCONFIGURED = "Unconfigured"
897
  _ST_WFCONNECTION = "WFConnection"
898
  _ST_CONNECTED = "Connected"
899

    
900
  _STATUS_FILE = "/proc/drbd"
901
  _USERMODE_HELPER_FILE = "/sys/module/drbd/parameters/usermode_helper"
902

    
903
  @staticmethod
904
  def _GetProcData(filename=_STATUS_FILE):
905
    """Return data from /proc/drbd.
906

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

    
920
  @classmethod
921
  def _MassageProcData(cls, data):
922
    """Transform the output of _GetProdData into a nicer form.
923

924
    @return: a dictionary of minor: joined lines from /proc/drbd
925
        for that minor
926

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

    
947
  @classmethod
948
  def _GetVersion(cls, proc_data):
949
    """Return the DRBD version.
950

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

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

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

    
976
    return retval
977

    
978
  @staticmethod
979
  def GetUsermodeHelper(filename=_USERMODE_HELPER_FILE):
980
    """Returns DRBD usermode_helper currently set.
981

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

    
995
  @staticmethod
996
  def _DevPath(minor):
997
    """Return the path to a drbd device for a given minor.
998

999
    """
1000
    return "/dev/drbd%d" % minor
1001

    
1002
  @classmethod
1003
  def GetUsedDevs(cls):
1004
    """Compute the list of used DRBD devices.
1005

1006
    """
1007
    data = cls._GetProcData()
1008

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

    
1020
    return used_devs
1021

    
1022
  def _SetFromMinor(self, minor):
1023
    """Set our parameters based on the given minor.
1024

1025
    This sets our minor variable and our dev_path.
1026

1027
    """
1028
    if minor is None:
1029
      self.minor = self.dev_path = None
1030
      self.attached = False
1031
    else:
1032
      self.minor = minor
1033
      self.dev_path = self._DevPath(minor)
1034
      self.attached = True
1035

    
1036
  @staticmethod
1037
  def _CheckMetaSize(meta_device):
1038
    """Check if the given meta device looks like a valid one.
1039

1040
    This currently only check the size, which must be around
1041
    128MiB.
1042

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

    
1064
  def Rename(self, new_id):
1065
    """Rename a device.
1066

1067
    This is not supported for drbd devices.
1068

1069
    """
1070
    raise errors.ProgrammerError("Can't rename a drbd device")
1071

    
1072

    
1073
class DRBD8(BaseDRBD):
1074
  """DRBD v8.x block device.
1075

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

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

1085
  """
1086
  _MAX_MINORS = 255
1087
  _PARSE_SHOW = None
1088

    
1089
  # timeout constants
1090
  _NET_RECONFIG_TIMEOUT = 60
1091

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

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

    
1120
  @classmethod
1121
  def _InitMeta(cls, minor, dev_path):
1122
    """Initialize a meta device.
1123

1124
    This will not work if the given minor is in use.
1125

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

    
1132
  @classmethod
1133
  def _FindUnusedMinor(cls):
1134
    """Find an unused DRBD device.
1135

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

1139
    """
1140
    data = cls._GetProcData()
1141

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

    
1158
  @classmethod
1159
  def _GetShowParser(cls):
1160
    """Return a parser for `drbd show` output.
1161

1162
    This will either create or return an already-create parser for the
1163
    output of the command `drbd show`.
1164

1165
    """
1166
    if cls._PARSE_SHOW is not None:
1167
      return cls._PARSE_SHOW
1168

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

    
1179
    comment = pyp.Literal ("#") + pyp.Optional(pyp.restOfLine)
1180
    defa = pyp.Literal("_is_default").suppress()
1181
    dbl_quote = pyp.Literal('"').suppress()
1182

    
1183
    keyword = pyp.Word(pyp.alphanums + '-')
1184

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

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

    
1205
    # an entire section
1206
    section_name = pyp.Word(pyp.alphas + '_')
1207
    section = section_name + lbrace + pyp.ZeroOrMore(pyp.Group(stmt)) + rbrace
1208

    
1209
    bnf = pyp.ZeroOrMore(pyp.Group(section ^ stmt))
1210
    bnf.ignore(comment)
1211

    
1212
    cls._PARSE_SHOW = bnf
1213

    
1214
    return bnf
1215

    
1216
  @classmethod
1217
  def _GetShowData(cls, minor):
1218
    """Return the `drbdsetup show` data for a minor.
1219

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

    
1228
  @classmethod
1229
  def _GetDevInfo(cls, out):
1230
    """Parse details about a given DRBD minor.
1231

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

1237
    """
1238
    data = {}
1239
    if not out:
1240
      return data
1241

    
1242
    bnf = cls._GetShowParser()
1243
    # run pyparse
1244

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

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

    
1268
  def _MatchesLocal(self, info):
1269
    """Test if our local config matches with an existing device.
1270

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

1276
    """
1277
    if self._children:
1278
      backend, meta = self._children
1279
    else:
1280
      backend = meta = None
1281

    
1282
    if backend is not None:
1283
      retval = ("local_dev" in info and info["local_dev"] == backend.dev_path)
1284
    else:
1285
      retval = ("local_dev" not in info)
1286

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

    
1297
  def _MatchesNet(self, info):
1298
    """Test if our network config matches with an existing device.
1299

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

1305
    """
1306
    if (((self._lhost is None and not ("local_addr" in info)) and
1307
         (self._rhost is None and not ("remote_addr" in info)))):
1308
      return True
1309

    
1310
    if self._lhost is None:
1311
      return False
1312

    
1313
    if not ("local_addr" in info and
1314
            "remote_addr" in info):
1315
      return False
1316

    
1317
    retval = (info["local_addr"] == (self._lhost, self._lport))
1318
    retval = (retval and
1319
              info["remote_addr"] == (self._rhost, self._rport))
1320
    return retval
1321

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

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

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

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

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

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

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

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

    
1410
      if (info["local_addr"] != (lhost, lport) or
1411
          info["remote_addr"] != (rhost, rport)):
1412
        raise utils.RetryAgain()
1413

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

    
1419
  def AddChildren(self, devices):
1420
    """Add a disk to the DRBD device.
1421

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

    
1439
    self._AssembleLocal(self.minor, backend.dev_path, meta.dev_path, self.size)
1440
    self._children = devices
1441

    
1442
  def RemoveChildren(self, devices):
1443
    """Detach the drbd device from local storage.
1444

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

    
1466
    self._ShutdownLocal(self.minor)
1467
    self._children = []
1468

    
1469
  @classmethod
1470
  def _SetMinorSyncSpeed(cls, minor, kbytes):
1471
    """Set the speed of the DRBD syncer.
1472

1473
    This is the low-level implementation.
1474

1475
    @type minor: int
1476
    @param minor: the drbd minor whose settings we change
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
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "syncer",
1484
                           "-r", "%d" % kbytes, "--create-device"])
1485
    if result.failed:
1486
      logging.error("Can't change syncer rate: %s - %s",
1487
                    result.fail_reason, result.output)
1488
    return not result.failed
1489

    
1490
  def SetSyncSpeed(self, kbytes):
1491
    """Set the speed of the DRBD syncer.
1492

1493
    @type kbytes: int
1494
    @param kbytes: the speed in kbytes/second
1495
    @rtype: boolean
1496
    @return: the success of the operation
1497

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

    
1505
  def PauseResumeSync(self, pause):
1506
    """Pauses or resumes the sync of a DRBD device.
1507

1508
    @param pause: Wether to pause or resume
1509
    @return: the success of the operation
1510

1511
    """
1512
    if self.minor is None:
1513
      logging.info("Not attached during PauseSync")
1514
      return False
1515

    
1516
    children_result = super(DRBD8, self).PauseResumeSync(pause)
1517

    
1518
    if pause:
1519
      cmd = "pause-sync"
1520
    else:
1521
      cmd = "resume-sync"
1522

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

    
1529
  def GetProcStatus(self):
1530
    """Return device data from /proc.
1531

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

    
1540
  def GetSyncStatus(self):
1541
    """Returns the sync status of the device.
1542

1543

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

1548

1549
    We set the is_degraded parameter to True on two conditions:
1550
    network not connected or local disk missing.
1551

1552
    We compute the ldisk parameter based on whether we have a local
1553
    disk or not.
1554

1555
    @rtype: objects.BlockDevStatus
1556

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

    
1561
    stats = self.GetProcStatus()
1562
    is_degraded = not stats.is_connected or not stats.is_disk_uptodate
1563

    
1564
    if stats.is_disk_uptodate:
1565
      ldisk_status = constants.LDS_OKAY
1566
    elif stats.is_diskless:
1567
      ldisk_status = constants.LDS_FAULTY
1568
    else:
1569
      ldisk_status = constants.LDS_UNKNOWN
1570

    
1571
    return objects.BlockDevStatus(dev_path=self.dev_path,
1572
                                  major=self.major,
1573
                                  minor=self.minor,
1574
                                  sync_percent=stats.sync_percent,
1575
                                  estimated_time=stats.est_time,
1576
                                  is_degraded=is_degraded,
1577
                                  ldisk_status=ldisk_status)
1578

    
1579
  def Open(self, force=False):
1580
    """Make the local state primary.
1581

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

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

    
1599
  def Close(self):
1600
    """Make the local state secondary.
1601

1602
    This will, of course, fail if the device is in use.
1603

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

    
1612
  def DisconnectNet(self):
1613
    """Removes network configuration.
1614

1615
    This method shutdowns the network side of the device.
1616

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

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

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

    
1633
    class _DisconnectStatus:
1634
      def __init__(self, ever_disconnected):
1635
        self.ever_disconnected = ever_disconnected
1636

    
1637
    dstatus = _DisconnectStatus(_IgnoreError(self._ShutdownNet, self.minor))
1638

    
1639
    def _WaitForDisconnect():
1640
      if self.GetProcStatus().is_standalone:
1641
        return
1642

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

    
1649
      raise utils.RetryAgain()
1650

    
1651
    # Keep start time
1652
    start_time = time.time()
1653

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

    
1665
      _ThrowError(msg, self.minor)
1666

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

    
1672
  def AttachNet(self, multimaster):
1673
    """Reconnects the network.
1674

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

1679
    Args:
1680
      - multimaster: init the network in dual-primary mode
1681

1682
    """
1683
    if self.minor is None:
1684
      _ThrowError("drbd%d: device not attached in AttachNet", self._aminor)
1685

    
1686
    if None in (self._lhost, self._lport, self._rhost, self._rport):
1687
      _ThrowError("drbd%d: missing network info in AttachNet()", self.minor)
1688

    
1689
    status = self.GetProcStatus()
1690

    
1691
    if not status.is_standalone:
1692
      _ThrowError("drbd%d: device is not standalone in AttachNet", self.minor)
1693

    
1694
    self._AssembleNet(self.minor,
1695
                      (self._lhost, self._lport, self._rhost, self._rport),
1696
                      constants.DRBD_NET_PROTOCOL, dual_pri=multimaster,
1697
                      hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1698

    
1699
  def Attach(self):
1700
    """Check if our minor is configured.
1701

1702
    This doesn't do any device configurations - it only checks if the
1703
    minor is in a state different from Unconfigured.
1704

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

1709
    """
1710
    used_devs = self.GetUsedDevs()
1711
    if self._aminor in used_devs:
1712
      minor = self._aminor
1713
    else:
1714
      minor = None
1715

    
1716
    self._SetFromMinor(minor)
1717
    return minor is not None
1718

    
1719
  def Assemble(self):
1720
    """Assemble the drbd.
1721

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

1727
    """
1728
    super(DRBD8, self).Assemble()
1729

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

    
1739
  def _SlowAssemble(self):
1740
    """Assembles the DRBD device from a (partially) configured device.
1741

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

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

    
1755
      if match_l and match_r:
1756
        # everything matches
1757
        break
1758

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

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

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

    
1806
    else:
1807
      minor = None
1808

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

    
1814
  def _FastAssemble(self):
1815
    """Assemble the drbd device from zero.
1816

1817
    This is run when in Assemble we detect our minor is unused.
1818

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

    
1831
  @classmethod
1832
  def _ShutdownLocal(cls, minor):
1833
    """Detach from the local device.
1834

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

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

    
1843
  @classmethod
1844
  def _ShutdownNet(cls, minor):
1845
    """Disconnect from the remote peer.
1846

1847
    This fails if we don't have a local device.
1848

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

    
1854
  @classmethod
1855
  def _ShutdownAll(cls, minor):
1856
    """Deactivate the device.
1857

1858
    This will, of course, fail if the device is in use.
1859

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

    
1866
  def Shutdown(self):
1867
    """Shutdown the DRBD device.
1868

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

    
1878
  def Remove(self):
1879
    """Stub remove for DRBD devices.
1880

1881
    """
1882
    self.Shutdown()
1883

    
1884
  @classmethod
1885
  def Create(cls, unique_id, children, size):
1886
    """Create a new DRBD8 device.
1887

1888
    Since DRBD devices are not created per se, just assembled, this
1889
    function only initializes the metadata.
1890

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

    
1913
  def Grow(self, amount):
1914
    """Resize the DRBD device and its backing storage.
1915

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

    
1927

    
1928
class FileStorage(BlockDev):
1929
  """File device.
1930

1931
  This class represents the a file storage backend device.
1932

1933
  The unique_id for the file device is a (file_driver, file_path) tuple.
1934

1935
  """
1936
  def __init__(self, unique_id, children, size):
1937
    """Initalizes a file device backend.
1938

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

    
1949
  def Assemble(self):
1950
    """Assemble the device.
1951

1952
    Checks whether the file device exists, raises BlockDeviceError otherwise.
1953

1954
    """
1955
    if not os.path.exists(self.dev_path):
1956
      _ThrowError("File device '%s' does not exist" % self.dev_path)
1957

    
1958
  def Shutdown(self):
1959
    """Shutdown the device.
1960

1961
    This is a no-op for the file type, as we don't deactivate
1962
    the file on shutdown.
1963

1964
    """
1965
    pass
1966

    
1967
  def Open(self, force=False):
1968
    """Make the device ready for I/O.
1969

1970
    This is a no-op for the file type.
1971

1972
    """
1973
    pass
1974

    
1975
  def Close(self):
1976
    """Notifies that the device will no longer be used for I/O.
1977

1978
    This is a no-op for the file type.
1979

1980
    """
1981
    pass
1982

    
1983
  def Remove(self):
1984
    """Remove the file backing the block device.
1985

1986
    @rtype: boolean
1987
    @return: True if the removal was successful
1988

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

    
1996
  def Rename(self, new_id):
1997
    """Renames the file.
1998

1999
    """
2000
    # TODO: implement rename for file-based storage
2001
    _ThrowError("Rename is not supported for file-based storage")
2002

    
2003
  def Grow(self, amount):
2004
    """Grow the file
2005

2006
    @param amount: the amount (in mebibytes) to grow with
2007

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

    
2021
  def Attach(self):
2022
    """Attach to an existing file.
2023

2024
    Check if this file already exists.
2025

2026
    @rtype: boolean
2027
    @return: True if file exists
2028

2029
    """
2030
    self.attached = os.path.exists(self.dev_path)
2031
    return self.attached
2032

    
2033
  def GetActualSize(self):
2034
    """Return the actual disk size.
2035

2036
    @note: the device needs to be active when this is called
2037

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

    
2046
  @classmethod
2047
  def Create(cls, unique_id, children, size):
2048
    """Create a new file.
2049

2050
    @param size: the size of file in MiB
2051

2052
    @rtype: L{bdev.FileStorage}
2053
    @return: an instance of FileStorage
2054

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

    
2069
    return FileStorage(unique_id, children, size)
2070

    
2071

    
2072
DEV_MAP = {
2073
  constants.LD_LV: LogicalVolume,
2074
  constants.LD_DRBD8: DRBD8,
2075
  }
2076

    
2077
if constants.ENABLE_FILE_STORAGE:
2078
  DEV_MAP[constants.LD_FILE] = FileStorage
2079

    
2080

    
2081
def FindDevice(dev_type, unique_id, children, size):
2082
  """Search for an existing, assembled device.
2083

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

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

    
2095

    
2096
def Assemble(dev_type, unique_id, children, size):
2097
  """Try to attach or assemble an existing device.
2098

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

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

    
2109

    
2110
def Create(dev_type, unique_id, children, size):
2111
  """Create a device.
2112

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