Statistics
| Branch: | Tag: | Revision:

root / lib / bdev.py @ fffe93e7

History | View | Annotate | Download (78.1 kB)

1
#
2
#
3

    
4
# Copyright (C) 2006, 2007, 2010, 2011 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 stat
28
import pyparsing as pyp
29
import os
30
import logging
31

    
32
from ganeti import utils
33
from ganeti import errors
34
from ganeti import constants
35
from ganeti import objects
36
from ganeti import compat
37
from ganeti import netutils
38

    
39

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

    
43

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

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

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

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

    
61

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

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

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

    
75

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

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

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

    
89

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

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

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

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

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

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

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

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

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

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

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

153
    """
154
    pass
155

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

159
    """
160
    raise NotImplementedError
161

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

165
    """
166
    raise NotImplementedError
167

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

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

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

179
    """
180
    raise NotImplementedError
181

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

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

189
    """
190
    raise NotImplementedError
191

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

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

197
    """
198
    raise NotImplementedError
199

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

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

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

209
    """
210
    raise NotImplementedError
211

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

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

219
    """
220
    raise NotImplementedError
221

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

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

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

    
234
  def PauseResumeSync(self, pause):
235
    """Pause/Resume the sync of the mirror.
236

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

239
    @param pause: Wheater to pause or resume
240

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

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

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

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

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

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

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

267
    @rtype: objects.BlockDevStatus
268

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

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

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

285
    @rtype: objects.BlockDevStatus
286

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

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

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

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

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

    
309
        is_degraded = is_degraded or child_status.is_degraded
310

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

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

    
324

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

328
    Only supported for some device types.
329

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

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

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

339
    """
340
    raise NotImplementedError
341

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

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

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

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

    
364

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
464
      data.append(splitted_fields)
465

    
466
    return data
467

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

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

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

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

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

    
496
    return data
497

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

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

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

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

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

    
527
    return data
528

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

655
    """
656
    pass
657

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

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

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

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

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

677
    @rtype: objects.BlockDevStatus
678

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

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

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

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

698
    """
699
    pass
700

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

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

706
    """
707
    pass
708

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

712
    @returns: tuple (vg, lv)
713

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

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

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

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

    
735
    return (self._vg_name, snap_name)
736

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

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

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

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

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

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

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

    
778

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

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

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

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

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

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

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

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

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

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

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

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

    
886

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
980
    return retval
981

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

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

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

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

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

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

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

    
1024
    return used_devs
1025

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

1029
    This sets our minor variable and our dev_path.
1030

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

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

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

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

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

1071
    This is not supported for drbd devices.
1072

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

    
1076

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

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

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

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

    
1093
  # timeout constants
1094
  _NET_RECONFIG_TIMEOUT = 60
1095

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
1216
    cls._PARSE_SHOW = bnf
1217

    
1218
    return bnf
1219

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1477
    This is the low-level implementation.
1478

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1547

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

1552

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

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

1559
    @rtype: objects.BlockDevStatus
1560

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
1653
      raise utils.RetryAgain()
1654

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

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

    
1669
      _ThrowError(msg, self.minor)
1670

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

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

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

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

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

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

    
1693
    status = self.GetProcStatus()
1694

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
1810
    else:
1811
      minor = None
1812

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1885
    """
1886
    self.Shutdown()
1887

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

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

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

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

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

    
1931

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

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

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

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

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

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

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

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

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

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

1968
    """
1969
    pass
1970

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

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

1976
    """
1977
    pass
1978

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

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

1984
    """
1985
    pass
1986

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

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

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

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

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

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

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

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

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

2028
    Check if this file already exists.
2029

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

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

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

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

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

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

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

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

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

    
2073
    return FileStorage(unique_id, children, size)
2074

    
2075

    
2076
class PersistentBlockDevice(BlockDev):
2077
  """A block device with persistent node
2078

2079
  May be either directly attached, or exposed through DM (e.g. dm-multipath).
2080
  udev helpers are probably required to give persistent, human-friendly
2081
  names.
2082

2083
  For the time being, pathnames are required to lie under /dev.
2084

2085
  """
2086
  def __init__(self, unique_id, children, size):
2087
    """Attaches to a static block device.
2088

2089
    The unique_id is a path under /dev.
2090

2091
    """
2092
    super(PersistentBlockDevice, self).__init__(unique_id, children, size)
2093
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
2094
      raise ValueError("Invalid configuration data %s" % str(unique_id))
2095
    self.dev_path = unique_id[1]
2096
    if not os.path.realpath(self.dev_path).startswith('/dev/'):
2097
      raise ValueError("Full path '%s' lies outside /dev" %
2098
                              os.path.realpath(self.dev_path))
2099
    # TODO: this is just a safety guard checking that we only deal with devices
2100
    # we know how to handle. In the future this will be integrated with
2101
    # external storage backends and possible values will probably be collected
2102
    # from the cluster configuration.
2103
    if unique_id[0] != constants.BLOCKDEV_DRIVER_MANUAL:
2104
      raise ValueError("Got persistent block device of invalid type: %s" %
2105
                       unique_id[0])
2106

    
2107
    self.major = self.minor = None
2108
    self.Attach()
2109

    
2110
  @classmethod
2111
  def Create(cls, unique_id, children, size):
2112
    """Create a new device
2113

2114
    This is a noop, we only return a PersistentBlockDevice instance
2115

2116
    """
2117
    return PersistentBlockDevice(unique_id, children, 0)
2118

    
2119
  def Remove(self):
2120
    """Remove a device
2121

2122
    This is a noop
2123

2124
    """
2125
    pass
2126

    
2127
  def Rename(self, new_id):
2128
    """Rename this device.
2129

2130
    """
2131
    _ThrowError("Rename is not supported for PersistentBlockDev storage")
2132

    
2133
  def Attach(self):
2134
    """Attach to an existing block device.
2135

2136

2137
    """
2138
    self.attached = False
2139
    try:
2140
      st = os.stat(self.dev_path)
2141
    except OSError, err:
2142
      logging.error("Error stat()'ing %s: %s", self.dev_path, str(err))
2143
      return False
2144

    
2145
    if not stat.S_ISBLK(st.st_mode):
2146
      logging.error("%s is not a block device", self.dev_path)
2147
      return False
2148

    
2149
    self.major = os.major(st.st_rdev)
2150
    self.minor = os.minor(st.st_rdev)
2151
    self.attached = True
2152

    
2153
    return True
2154

    
2155
  def Assemble(self):
2156
    """Assemble the device.
2157

2158
    """
2159
    pass
2160

    
2161
  def Shutdown(self):
2162
    """Shutdown the device.
2163

2164
    """
2165
    pass
2166

    
2167
  def Open(self, force=False):
2168
    """Make the device ready for I/O.
2169

2170
    """
2171
    pass
2172

    
2173
  def Close(self):
2174
    """Notifies that the device will no longer be used for I/O.
2175

2176
    """
2177
    pass
2178

    
2179
  def Grow(self, amount):
2180
    """Grow the logical volume.
2181

2182
    """
2183
    _ThrowError("Grow is not supported for PersistentBlockDev storage")
2184

    
2185

    
2186
class RADOSBlockDevice(BlockDev):
2187
  """A RADOS Block Device (rbd)
2188

2189
  This class implements the RADOS Block Device for the backend. You need
2190
  the rbd kernel driver, the RADOS Tools and a RADOS working cluster for
2191
  this to be functional.
2192

2193
  """
2194
  def __init__(self, unique_id, children, size):
2195
    """Attaches to an rbd device.
2196

2197
    """
2198
    super(RADOSBlockDevice, self).__init__(unique_id, children, size)
2199
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
2200
      raise ValueError("Invalid configuration data %s" % str(unique_id))
2201

    
2202
    self.unique_id = unique_id
2203

    
2204
    self.major = self.minor = None
2205
    self.Attach()
2206

    
2207
  @classmethod
2208
  def Create(cls, unique_id, children, size):
2209
    """Create a new rbd device
2210

2211
    Provision a new rbd image file inside a RADOS pool
2212

2213
    """
2214
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
2215
      raise errors.ProgrammerError("Invalid configuration data %s" %
2216
                                   str(unique_id))
2217
    rbd_pool, rbd_name = unique_id
2218

    
2219
    # Provision a new rbd Volume (Image) inside the RADOS cluster
2220
    cmd = "rbd create -p %s %s --size %s" % (rbd_pool, rbd_name, size)
2221
    result = utils.RunCmd(cmd)
2222
    if result.failed:
2223
      _ThrowError("rbd creation failed (%s): %s",
2224
                  result.fail_reason, result.output)
2225

    
2226
    return RADOSBlockDevice(unique_id, children, size)
2227

    
2228
  def Remove(self):
2229
    """Remove the rbd device
2230

2231
    """
2232
    rbd_pool, rbd_name = self.unique_id
2233

    
2234
    if not self.minor and not self.Attach():
2235
      # the rbd device doesn't exist
2236
      return
2237

    
2238
    # First shutdown the device (remove mappings).
2239
    self.Shutdown()
2240

    
2241
    # Remove the actual Volume (Image) from the RADOS cluster
2242
    cmd = "rbd rm -p %s %s" % (rbd_pool, rbd_name)
2243
    result = utils.RunCmd(cmd)
2244
    if result.failed:
2245
      _ThrowError("Can't remove Volume from cluster with rbd rm: %s - %s",
2246
                  result.fail_reason, result.output)
2247

    
2248
  def Rename(self, new_id):
2249
    """Rename this device.
2250

2251
    """
2252
    pass
2253

    
2254
  def Attach(self):
2255
    """Attach to an existing rbd device.
2256

2257
    This method maps the rbd volume that matches our name with
2258
    an rbd device and then attaches to this device
2259

2260
    """
2261
    self.attached = False
2262

    
2263
    # Map the rbd Volume to a block device under /dev
2264
    self.dev_path = self._MapVolumeToBlockdev(self.unique_id)
2265

    
2266
    try:
2267
      st = os.stat(self.dev_path)
2268
    except OSError, err:
2269
      logging.error("Error stat()'ing %s: %s", self.dev_path, str(err))
2270
      return False
2271

    
2272
    if not stat.S_ISBLK(st.st_mode):
2273
      logging.error("%s is not a block device", self.dev_path)
2274
      return False
2275

    
2276
    self.major = os.major(st.st_rdev)
2277
    self.minor = os.minor(st.st_rdev)
2278
    self.attached = True
2279

    
2280
    return True
2281

    
2282
  def _MapVolumeToBlockdev(self, unique_id):
2283
    """Maps existing rbd Volumes (Images) to block devices
2284

2285
       This method should be idempotent if the mapping already exists.
2286
       If the mapping exists it returns the device path.
2287

2288
       If the mapping doesn't exist:
2289
       Maps the existing Volume (Image) named `name' (inside the RADOS
2290
       pool named `pool'), to a block device e.g. /dev/rbd{X}. Then
2291
       returns it's device path.
2292

2293
    """
2294
    pool, name = unique_id
2295

    
2296
    # Check if the mapping already exists
2297
    cmd1 = "rbd showmapped"
2298
    result = utils.RunCmd(cmd1)
2299
    if result.failed:
2300
      _ThrowError("rbd showmapped failed (%s): %s",
2301
                  result.fail_reason, result.output)
2302
    else:
2303
      cmd2 = "echo '%s' | grep %s | grep %s" % (result.output, pool, name)
2304
      result = utils.RunCmd(cmd2)
2305
      if not result.failed:
2306
        # The mapping already exists.
2307
        # Parse the result and return the rbd device
2308
        try:
2309
          rbd_dev = re.search("(/dev/rbd\d+)", result.output).group(1)
2310
        except:
2311
          # maybe we can add an assert here
2312
          _ThrowError("You shouldn't get here")
2313

    
2314
        return rbd_dev
2315

    
2316
      else:
2317
        # The mapping doesn't exist. Create it
2318
        cmd = "rbd map -p %s %s" % (pool, name)
2319
        result = utils.RunCmd(cmd)
2320
        if result.failed:
2321
          _ThrowError("rbd map failed (%s): %s",
2322
                      result.fail_reason, result.output)
2323
        # Use rbd showmapped again to find the rbd device
2324
        # the image was mapped to
2325
        cmd3 = "rbd showmapped | grep %s | grep %s" % (pool, name)
2326
        result = utils.RunCmd(cmd3)
2327
        if result.failed:
2328
          _ThrowError("Can't find mapped device. "
2329
                      "rbd showmapped failed (%s): %s",
2330
                      result.fail_reason, result.output)
2331
        try:
2332
          rbd_dev = re.search("(/dev/rbd\d+)", result.output).group(1)
2333
        except:
2334
          # maybe we can add an assert here
2335
          _ThrowError("You shouldn't get here")
2336

    
2337
        # The device was successfully mapped. Return it
2338
        return rbd_dev
2339

    
2340
  def Assemble(self):
2341
    """Assemble the device.
2342

2343
    """
2344
    pass
2345

    
2346
  def Shutdown(self):
2347
    """Shutdown the device.
2348

2349
    """
2350
    # Unmap the rbd device
2351
    # !doesn't unmap during migration because the
2352
    # !shutdown method is never called if the machine was up
2353
    # TODO: Fix cmdlib.py to shutdown the machine in the source node
2354
    # once the migration ends successfully
2355
    if not self.minor and not self.Attach():
2356
      # the rbd device doesn't exist
2357
      return
2358

    
2359
    # Unmap the block device from the Volume
2360
    self._UnmapVolumeFromBlockdev(self.unique_id)
2361

    
2362
    self.minor = None
2363
    self.dev_path = None
2364

    
2365
  def _UnmapVolumeFromBlockdev(self, unique_id):
2366
    """Unmaps the rbd device from the Volume it is mapped
2367

2368
    Unmaps the rbd device from the Volume (Image) it was
2369
    previously mapped to. This method should be idempotent if
2370
    the Volume isn't mapped.
2371

2372
    """
2373
    pool, name = unique_id
2374

    
2375
    # Check if the mapping already exists
2376
    cmd1 = "rbd showmapped"
2377
    result = utils.RunCmd(cmd1)
2378
    if result.failed:
2379
      _ThrowError("rbd showmapped failed [during unmap](%s): %s",
2380
                  result.fail_reason, result.output)
2381
    else:
2382
      cmd2 = "echo '%s' | grep %s | grep %s" % (result.output, pool, name)
2383
      result = utils.RunCmd(cmd2)
2384
      if not result.failed:
2385
        # The mapping already exists.
2386
        # Parse the result to find the rbd device
2387
        try:
2388
          rbd_dev = re.search("(/dev/rbd\d+)", result.output).group(1)
2389
        except:
2390
          # maybe we can add an assert here
2391
          _ThrowError("You shouldn't get here")
2392

    
2393
        # Unmap the rbd device
2394
        cmd = "rbd unmap %s" % rbd_dev
2395
        result = utils.RunCmd(cmd)
2396
        if result.failed:
2397
          _ThrowError("rbd unmap failed (%s): %s",
2398
                      result.fail_reason, result.output)
2399

    
2400
      else:
2401
        # The mapping doesn't exist. Do nothing
2402
        pass
2403

    
2404
  def Open(self, force=False):
2405
    """Make the device ready for I/O.
2406

2407
    """
2408
    pass
2409

    
2410
  def Close(self):
2411
    """Notifies that the device will no longer be used for I/O.
2412

2413
    """
2414
    pass
2415

    
2416
  def Grow(self, amount):
2417
    """Grow the volume.
2418

2419
    Grow the rbd Volume by `amount' megabytes.
2420
    Total block size will be: size + amount
2421

2422
    """
2423
    if not self.Attach():
2424
      _ThrowError("Can't attach to rbd device during Grow()")
2425

    
2426
    rbd_pool, rbd_name = self.unique_id
2427
    new_size = self.size + amount
2428

    
2429
    # Resize the rbd Volume (Image) inside the RADOS cluster
2430
    cmd = "rbd resize -p %s %s --size %s" % (rbd_pool, rbd_name, new_size)
2431
    result = utils.RunCmd(cmd)
2432
    if result.failed:
2433
      _ThrowError("rbd resize failed (%s): %s",
2434
                  result.fail_reason, result.output)
2435

    
2436

    
2437
DEV_MAP = {
2438
  constants.LD_LV: LogicalVolume,
2439
  constants.LD_DRBD8: DRBD8,
2440
  constants.LD_BLOCKDEV: PersistentBlockDevice,
2441
  constants.LD_RBD: RADOSBlockDevice,
2442
  }
2443

    
2444
if constants.ENABLE_FILE_STORAGE or constants.ENABLE_SHARED_FILE_STORAGE:
2445
  DEV_MAP[constants.LD_FILE] = FileStorage
2446

    
2447

    
2448
def FindDevice(dev_type, unique_id, children, size):
2449
  """Search for an existing, assembled device.
2450

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

2454
  """
2455
  if dev_type not in DEV_MAP:
2456
    raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
2457
  device = DEV_MAP[dev_type](unique_id, children, size)
2458
  if not device.attached:
2459
    return None
2460
  return device
2461

    
2462

    
2463
def Assemble(dev_type, unique_id, children, size):
2464
  """Try to attach or assemble an existing device.
2465

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

2469
  """
2470
  if dev_type not in DEV_MAP:
2471
    raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
2472
  device = DEV_MAP[dev_type](unique_id, children, size)
2473
  device.Assemble()
2474
  return device
2475

    
2476

    
2477
def Create(dev_type, unique_id, children, size):
2478
  """Create a device.
2479

2480
  """
2481
  if dev_type not in DEV_MAP:
2482
    raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
2483
  device = DEV_MAP[dev_type].Create(unique_id, children, size)
2484
  return device